The reworked artifact went mobile-first; this ports its shell, minus the
quirks it shipped with.
Routes. #lesson #today #settings #words #learn #sent #grammar #cj #hangul
#drill, each a history entry, so the phone's back gesture works. The word
sheet, a popover and the review screen are layers: each owns an entry while
open, and Back closes the topmost before it leaves a route — the artifact
read window.__onBack but never set it. shell/history.ts holds the rules,
free of React and tested against a history whose traversals land late, as a
browser's do. Unlike the artifact's router:
· re-tapping the current destination pushes nothing;
· leaving a route closes what is open on it, rewinding its entries first;
· a sub-page's ← goes back only when history leads to its parent, and
otherwise becomes the parent — history.length > 1 let ← leave the app.
Shell. The page never scrolls; each route owns one scroller, in svh. A
bottom bar under 600px, a 76px rail to 840px, a labelled rail beyond, with
the artifact's icons and a due badge on 복습. Routes mount on first visit and
stay mounted, so a draft or a drill survives a trip elsewhere. Settings,
the conjugation trainer and the reading drill are pages of their own; 학습
is a hub. The design tokens gain the layout set and a second register:
reference panels stay square, what a thumb works is rounded.
복습 is its own screen: tap anywhere to reveal, grades in the thumb zone,
and an empty queue says so instead of doing nothing. Its pool is "my
units" once there are twenty such words, as in the artifact — plus his own
words, which the artifact dropped from review at that point.
The lesson is a column that fits the screen: roadmap strip, conversation,
composer with the quick-reply chips, and a ··· menu for focus and clearing.
The word list is docked from 840px; below that it is a sheet with peek,
half and full detents and a drag handle, whose height comes off the shell
so the exercise above shrinks rather than being covered. Focusing an answer
drops it to peek; a new exercise takes it down from full.
Fixed on the way: the log now follows a reply by what the learner did, not
by distance — its own scroll event arrived after the stream had added more
than the threshold, which read as scrolling away and stopped the follow
mid-reply. Sending re-sticks. The keyboard's focus guard moves to
pointerdown; React 19 attaches touchstart passively, so preventDefault()
there was ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
219 lines
3.4 KiB
CSS
219 lines
3.4 KiB
CSS
/* The word list: docked column, sheet, and the rows they share. */
|
|
|
|
.sheet {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 110;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: var(--sheet-h, 0px);
|
|
max-height: 88svh;
|
|
padding-bottom: var(--safe-b);
|
|
background: var(--paper);
|
|
border-top: 1px solid var(--line2);
|
|
border-radius: 16px 16px 0 0;
|
|
box-shadow: 0 -8px 30px -18px rgba(0, 0, 0, 0.5);
|
|
transition: height var(--dur-sheet) var(--ease);
|
|
overflow: hidden;
|
|
}
|
|
|
|
:root[data-sheet-drag] .sheet,
|
|
:root[data-sheet-drag] .app {
|
|
transition: none;
|
|
}
|
|
|
|
.sheet-grab {
|
|
flex: none;
|
|
height: 22px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
touch-action: none;
|
|
cursor: grab;
|
|
}
|
|
|
|
.sheet-grab i {
|
|
display: block;
|
|
width: 38px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: var(--line2);
|
|
}
|
|
|
|
.sheet-h {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 0 14px 9px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.railcol .sheet-h {
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.sheet-h h2 {
|
|
font-size: 14.5px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sheet-h .note {
|
|
font-size: 12px;
|
|
color: var(--ink3);
|
|
}
|
|
|
|
.sheet-acts {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.sheet-b {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.scrim {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 105;
|
|
background: rgba(0, 0, 0, 0.34);
|
|
}
|
|
|
|
@media (min-width: 840px) {
|
|
.sheet,
|
|
.scrim {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
/* The docked column, on wide screens only. */
|
|
.railcol {
|
|
display: none;
|
|
}
|
|
|
|
@media (min-width: 840px) {
|
|
.railcol {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 302px;
|
|
flex: none;
|
|
min-height: 0;
|
|
border-left: 1px solid var(--line);
|
|
background: var(--paper);
|
|
}
|
|
.railcol .sheet-b {
|
|
padding-bottom: 16px;
|
|
}
|
|
}
|
|
|
|
.wr-search {
|
|
flex: none;
|
|
display: flex;
|
|
gap: 7px;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.wr-search input {
|
|
flex: 1;
|
|
width: 100%;
|
|
padding: 9px 11px;
|
|
border-radius: 9px;
|
|
}
|
|
|
|
.wr-cover {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 4px 12px;
|
|
border-bottom: 1px solid var(--line);
|
|
font-size: 12px;
|
|
color: var(--ink3);
|
|
}
|
|
|
|
.wr-cover .toggle {
|
|
min-height: 34px;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: var(--ink3);
|
|
}
|
|
|
|
.wr-cover .toggle input {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.wr-cover .n {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.wr-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
min-height: 52px;
|
|
padding: 10px 13px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.wr-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.wr-k {
|
|
min-width: 66px;
|
|
flex-shrink: 0;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
word-break: keep-all;
|
|
}
|
|
|
|
/* A word looked up in this exercise. */
|
|
.wr-row[data-peeked="1"] .wr-k {
|
|
text-decoration: underline;
|
|
text-decoration-color: var(--hwang);
|
|
text-decoration-thickness: 2px;
|
|
text-underline-offset: 0.24em;
|
|
}
|
|
|
|
.wr-m {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 14px;
|
|
color: var(--ink);
|
|
text-align: left;
|
|
}
|
|
|
|
.wr-m i {
|
|
display: block;
|
|
margin-top: 1px;
|
|
font-style: normal;
|
|
font-size: 12px;
|
|
color: var(--ink3);
|
|
}
|
|
|
|
/* Covered. The gloss is not in the DOM at all — only this label is. */
|
|
.wr-m.hid {
|
|
min-height: 30px;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
background: var(--sunk);
|
|
color: var(--ink3);
|
|
font-size: 11.5px;
|
|
}
|
|
|
|
.wr-m.hid:hover {
|
|
color: var(--jade);
|
|
}
|
|
|
|
.sheet-b .empty {
|
|
padding: 22px 16px;
|
|
}
|