clio-design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,731 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode, MouseEvent, InputHTMLAttributes, ChangeEventHandler, CSSProperties, TextareaHTMLAttributes, HTMLAttributes, DragEventHandler, ElementType, MouseEventHandler } from 'react';
3
+
4
+ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'title' | 'type'> {
5
+ /** Visual treatment. 'nav' is the segmented top-bar tab style (Learn/Practice/Review). */
6
+ variant?: 'primary' | 'dark' | 'secondary' | 'ghost' | 'nav';
7
+ size?: 'sm' | 'md';
8
+ /** For variant="nav" — whether this tab is the active one. */
9
+ active?: boolean;
10
+ disabled?: boolean;
11
+ /** Optional leading glyph, e.g. an ↥ import icon. Rendered aria-hidden — pass meaningful text via children, not icon. */
12
+ icon?: ReactNode;
13
+ children?: ReactNode;
14
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
15
+ title?: string;
16
+ }
17
+ /**
18
+ * Button — Clio's pill-shaped mono-label action button.
19
+ * Variants: primary (filled accent), dark (filled ink-900, e.g. DOWNLOAD),
20
+ * secondary (outlined), ghost (text-only), nav (segmented top-nav tab).
21
+ */
22
+ declare function Button({ variant, size, active, disabled, icon, children, onClick, title, ...rest }: ButtonProps): react.JSX.Element;
23
+
24
+ interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type' | 'title'> {
25
+ size?: number;
26
+ children?: ReactNode;
27
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
28
+ /** Also used as the accessible name (via aria-label) — required so glyph-only buttons are never announced by their raw unicode content. */
29
+ title: string;
30
+ /** Adds a hairline border + surface fill (used for ⚙ manage-tags, ⇥ import/export). */
31
+ bordered?: boolean;
32
+ hoverColor?: string;
33
+ disabled?: boolean;
34
+ /** Corner radius token. Defaults to the standard control radius; row-level menu triggers use the smaller 'var(--radius-sm)'. */
35
+ radius?: string;
36
+ }
37
+ /**
38
+ * IconButton — circular glyph-only button (⋯ menu trigger, ⚙ manage tags,
39
+ * chevron collapse toggle, back arrow).
40
+ */
41
+ declare function IconButton({ size, children, onClick, title, bordered, hoverColor, disabled, radius, onMouseEnter, onMouseLeave, onFocus, onBlur, ...rest }: IconButtonProps): react.JSX.Element;
42
+
43
+ interface TagChipProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'color' | 'type'> {
44
+ label: string;
45
+ /** Tag or topic color — tints when inactive, fills solid when active. */
46
+ color?: string;
47
+ active?: boolean;
48
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
49
+ removable?: boolean;
50
+ onRemove?: () => void;
51
+ size?: 'sm' | 'md';
52
+ }
53
+ /**
54
+ * TagChip — mono capsule tag (#exam-prep). Two states: inactive (tinted)
55
+ * and active/selected (solid fill, white text) used in tag filter bars.
56
+ */
57
+ declare function TagChip({ label, color, active, onClick, removable, onRemove, size, ...rest }: TagChipProps): react.JSX.Element;
58
+
59
+ interface BadgeProps {
60
+ variant?: 'topic' | 'mono' | 'eyebrow' | 'count';
61
+ color?: string;
62
+ tint?: string;
63
+ children?: ReactNode;
64
+ }
65
+ /**
66
+ * Badge — small labeled pill/rect used across the product:
67
+ * - 'topic' : rounded-rect topic-color label on note headers (e.g. MACHINE LEARNING)
68
+ * - 'mono' : neutral rounded-rect mono badge (e.g. MULTIPLE CHOICE problem type)
69
+ * - 'eyebrow' : borderless mono accent label above a content card (e.g. INTERACTIVE, CODE)
70
+ * - 'count' : small circular counter (review due-count)
71
+ */
72
+ declare function Badge({ variant, children, color, tint }: BadgeProps): react.JSX.Element;
73
+
74
+ interface SolvedStatusButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type'> {
75
+ solved?: boolean;
76
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
77
+ }
78
+ /**
79
+ * SolvedStatusButton — Practice problem's status toggle. Two states only:
80
+ * unsolved (neutral outline, "MARK SOLVED") and solved (green tint, "✓ SOLVED").
81
+ * FLAGGED IN README: exact green was inferred from the app's shared success
82
+ * color (#059669) — confirm this is intentional vs. a distinct status hue.
83
+ */
84
+ declare function SolvedStatusButton({ solved, onClick, ...rest }: SolvedStatusButtonProps): react.JSX.Element;
85
+
86
+ interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'style' | 'aria-label' | 'type'> {
87
+ value: string;
88
+ onChange: ChangeEventHandler<HTMLInputElement>;
89
+ placeholder?: string;
90
+ mono?: boolean;
91
+ autoFocus?: boolean;
92
+ style?: CSSProperties;
93
+ /** Accessible name. The source design has no visible label for this field — pass this when the placeholder alone isn't a sufficient accessible name. */
94
+ 'aria-label'?: string;
95
+ /** Restricted to text-like input types — this component's value/onChange contract assumes a string, which checkbox/radio/file don't provide. */
96
+ type?: 'text' | 'search' | 'email' | 'password' | 'tel' | 'url';
97
+ }
98
+ /** TextInput — single-line text field (rename inputs, tag search). */
99
+ declare function TextInput({ value, onChange, placeholder, mono, autoFocus, style, 'aria-label': ariaLabel, type, ...rest }: TextInputProps): react.JSX.Element;
100
+
101
+ interface TextAreaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'style' | 'aria-label'> {
102
+ value: string;
103
+ onChange: ChangeEventHandler<HTMLTextAreaElement>;
104
+ placeholder?: string;
105
+ rows?: number;
106
+ mono?: boolean;
107
+ /** Accessible name. The source design has no visible label for this field — pass this when the placeholder alone isn't a sufficient accessible name. */
108
+ 'aria-label'?: string;
109
+ }
110
+ /** TextArea — multi-line field (import/export paste box). */
111
+ declare function TextArea({ value, onChange, placeholder, rows, mono, 'aria-label': ariaLabel, ...rest }: TextAreaProps): react.JSX.Element;
112
+
113
+ interface AvatarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
114
+ initial?: string;
115
+ size?: number;
116
+ dark?: boolean;
117
+ style?: CSSProperties;
118
+ }
119
+ /** Avatar — user initial circle in the topbar (dark mono initial on tint). */
120
+ declare function Avatar({ initial, size, dark, style, ...rest }: AvatarProps): react.JSX.Element;
121
+
122
+ interface ColorSwatchProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'color' | 'type' | 'title'> {
123
+ color: string;
124
+ active?: boolean;
125
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
126
+ size?: number;
127
+ title?: string;
128
+ }
129
+ /** ColorSwatch — small circular color-picker swatch used in recolor rows. */
130
+ declare const ColorSwatch: react.ForwardRefExoticComponent<ColorSwatchProps & react.RefAttributes<HTMLButtonElement>>;
131
+
132
+ interface DragHandleProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'onDragStart' | 'title' | 'style'> {
133
+ onDragStart?: DragEventHandler<HTMLSpanElement>;
134
+ title?: string;
135
+ style?: CSSProperties;
136
+ }
137
+ /**
138
+ * DragHandle — the ⋮⋮ grip used to reorder topic/note rows.
139
+ *
140
+ * KNOWN GAP: this uses HTML5 native drag-and-drop (the `draggable` attribute),
141
+ * which has no keyboard equivalent — keyboard-only and screen-reader users
142
+ * cannot trigger it. It's marked aria-hidden so it isn't announced as a
143
+ * false, non-operable affordance in the meantime. Real keyboard reordering
144
+ * (e.g. Space to lift, arrow keys to move, a live region announcing the new
145
+ * position) needs actual reorder state to wire into, which belongs in the
146
+ * list that owns it (Sidebar/TopicRow/NoteRow) — revisit this when those
147
+ * are built, not in this leaf component alone.
148
+ */
149
+ declare function DragHandle({ onDragStart, title, style, ...rest }: DragHandleProps): react.JSX.Element;
150
+
151
+ type NavTabId = 'notes' | 'practice' | 'review';
152
+ interface NavTabsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'onChange'> {
153
+ active?: NavTabId;
154
+ onChange?: (id: NavTabId) => void;
155
+ reviewCount?: number;
156
+ }
157
+ /**
158
+ * NavTabs — top-bar Learn / Practice / Review segmented control, with an
159
+ * optional due-count badge on the Review tab.
160
+ *
161
+ * Implements the ARIA tabs pattern (role="tablist"/"tab", aria-selected,
162
+ * roving tabindex + arrow-key navigation) rather than aria-current, since
163
+ * these buttons swap the active view in place — aria-current is intended for
164
+ * navigation links to different pages/locations, not view-switching controls.
165
+ */
166
+ declare function NavTabs({ active, onChange, reviewCount, ...rest }: NavTabsProps): react.JSX.Element;
167
+
168
+ interface SidebarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
169
+ collapsed?: boolean;
170
+ onToggle?: () => void;
171
+ /** Eyebrow section label, e.g. "TOPICS" or "CONCEPTS". */
172
+ label?: string;
173
+ width?: number;
174
+ children?: ReactNode;
175
+ borderSide?: 'right' | 'none';
176
+ style?: CSSProperties;
177
+ }
178
+ /**
179
+ * Sidebar — collapsible panel shell used for both the topics column and the
180
+ * notes column on desktop. Collapsed state shrinks to a 26px chevron rail;
181
+ * expanded shows a scrollable content area. Pass a section label + children.
182
+ */
183
+ declare function Sidebar({ collapsed, onToggle, label, width, children, borderSide, style, ...rest }: SidebarProps): react.JSX.Element;
184
+
185
+ interface TopicRowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'style' | 'color'> {
186
+ name: string;
187
+ color: string;
188
+ notesLabel: string;
189
+ selected?: boolean;
190
+ onClick?: () => void;
191
+ onMenuClick?: (event: MouseEvent<HTMLButtonElement>) => void;
192
+ draggable?: boolean;
193
+ onDragStart?: DragEventHandler<HTMLSpanElement>;
194
+ /** Mobile topics-screen layout omits the drag handle + indent. */
195
+ dense?: boolean;
196
+ style?: CSSProperties;
197
+ }
198
+ /**
199
+ * TopicRow — a single row in the topics sidebar (desktop) / topics screen
200
+ * (mobile). Shows color dot, name, note count, drag handle, and a hover-
201
+ * revealed ⋯ menu. `selected` drives the sunken background + bold text.
202
+ *
203
+ * The selectable area (dot + name + note count) is a real <button>, not a
204
+ * click handler on the outer row: the row also hosts the drag handle and the
205
+ * ⋯ menu button, and an ARIA `role="button"` on the outer element would
206
+ * improperly nest interactive descendants inside another interactive
207
+ * element. A native button sidesteps that entirely.
208
+ */
209
+ declare function TopicRow({ name, color, notesLabel, selected, onClick, onMenuClick, draggable, onDragStart, dense, style, ...rest }: TopicRowProps): react.JSX.Element;
210
+
211
+ interface NoteRowTag {
212
+ label: string;
213
+ color: string;
214
+ }
215
+ interface NoteRowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'style'> {
216
+ title: string;
217
+ meta: string;
218
+ selected?: boolean;
219
+ showTopic?: boolean;
220
+ topicColor?: string;
221
+ topicName?: string;
222
+ tags?: NoteRowTag[];
223
+ onClick?: () => void;
224
+ onMenuClick?: (event: MouseEvent<HTMLButtonElement>) => void;
225
+ draggable?: boolean;
226
+ onDragStart?: DragEventHandler<HTMLSpanElement>;
227
+ style?: CSSProperties;
228
+ }
229
+ /**
230
+ * NoteRow — a single concept/note row in the notes list (desktop sidebar
231
+ * column 2) / concepts screen (mobile). Optional topic dot+label (shown
232
+ * when listing across topics, e.g. tag-filtered results) and tag chips.
233
+ *
234
+ * Like TopicRow, the selectable area is a real <button> rather than a click
235
+ * handler on the outer row, since the row also hosts the drag handle and the
236
+ * ⋯ menu button as separate interactive controls.
237
+ */
238
+ declare function NoteRow({ title, meta, selected, showTopic, topicColor, topicName, tags, onClick, onMenuClick, draggable, onDragStart, style, ...rest }: NoteRowProps): react.JSX.Element;
239
+
240
+ interface ContextMenuItem {
241
+ label?: string;
242
+ onClick?: () => void;
243
+ danger?: boolean;
244
+ icon?: ReactNode;
245
+ divider?: boolean;
246
+ }
247
+ interface ContextMenuSwatch {
248
+ color: string;
249
+ active?: boolean;
250
+ pick: () => void;
251
+ }
252
+ interface ContextMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onClick' | 'style' | 'role' | 'tabIndex'> {
253
+ items: ContextMenuItem[];
254
+ /** Recolor swatch row, e.g. topic/tag color picker. */
255
+ swatches?: ContextMenuSwatch[];
256
+ width?: number;
257
+ style?: CSSProperties;
258
+ /** Called on Escape — the menu itself doesn't track open/closed state; the parent (typically toggled by a row's ⋯ button) owns that. */
259
+ onClose?: () => void;
260
+ /** Accessible label for the menu, since its contents vary by context (e.g. "Topic options"). */
261
+ 'aria-label'?: string;
262
+ }
263
+ /**
264
+ * ContextMenu — desktop floating dropdown (topic ⋯, note ⋯, manage tags).
265
+ * Pass `items`: array of { label, onClick, danger? } or { divider: true },
266
+ * plus optional `swatches` (color-recolor row) and `header`.
267
+ *
268
+ * Implements the ARIA menu pattern: role="menu"/"menuitem", roving tabindex,
269
+ * Arrow/Home/End navigation, and Escape-to-close — none of which the source
270
+ * design has, since it only renders the dropdown's visual chrome.
271
+ */
272
+ declare function ContextMenu({ items, swatches, width, style, onClose, 'aria-label': ariaLabel, ...rest }: ContextMenuProps): react.JSX.Element;
273
+
274
+ interface BottomSheetProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'onClick' | 'role' | 'aria-modal' | 'tabIndex'> {
275
+ open: boolean;
276
+ onClose?: () => void;
277
+ children?: ReactNode;
278
+ maxHeight?: string | number;
279
+ style?: CSSProperties;
280
+ /** Accessible name for the sheet, since its content varies by context (e.g. "Topic actions"). Prefer aria-labelledby if the sheet's content has its own visible heading. */
281
+ 'aria-label'?: string;
282
+ }
283
+ /**
284
+ * BottomSheet — mobile modal pattern: dims the screen, slides a rounded-top
285
+ * panel up from the bottom with a drag-handle bar. Used for topic/note
286
+ * action sheets, manage-tags, add-tag, and the TOC outline.
287
+ *
288
+ * A true modal dialog: role="dialog" + aria-modal, focus moves into the
289
+ * sheet on open and is trapped there, Escape closes it, and focus returns to
290
+ * whatever triggered it on close (see internal/useDialog). The source design
291
+ * has none of this — it's presentational chrome only.
292
+ */
293
+ declare function BottomSheet({ open, onClose, children, maxHeight, style, 'aria-label': ariaLabel, ...rest }: BottomSheetProps): react.JSX.Element | null;
294
+
295
+ type ImportExportTab = 'import' | 'export';
296
+ interface ImportExportModalProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'onClick' | 'role' | 'aria-modal' | 'tabIndex'> {
297
+ open: boolean;
298
+ tab?: ImportExportTab;
299
+ onTabChange?: (tab: ImportExportTab) => void;
300
+ onClose?: () => void;
301
+ children?: ReactNode;
302
+ }
303
+ /**
304
+ * ImportExportModal — desktop centered modal (mobile uses a full-screen
305
+ * variant, same content structure with a header row instead of an X close).
306
+ *
307
+ * A true modal dialog: role="dialog" + aria-modal, focus trapped inside and
308
+ * moved in on open / restored on close (internal/useDialog), Escape closes,
309
+ * and page scroll is locked while open (unlike BottomSheet, this overlay is
310
+ * `position: fixed; inset: 0` over the whole viewport, so background scroll
311
+ * genuinely needs to be suppressed here). The Import/Export toggle uses the
312
+ * ARIA tabs pattern (role="tablist"/"tab", aria-selected, roving tabindex +
313
+ * arrow-key navigation) since it swaps the modal's content in place, same
314
+ * reasoning as NavTabs. None of this exists in the source design.
315
+ */
316
+ declare function ImportExportModal({ open, tab, onTabChange, onClose, children, ...rest }: ImportExportModalProps): react.JSX.Element | null;
317
+ interface ExportRowProps {
318
+ title: string;
319
+ sub: string;
320
+ onCopy?: () => void;
321
+ copyLabel?: string;
322
+ onDownload?: () => void;
323
+ }
324
+ /** ExportRow — one exportable-item row inside the EXPORT tab (This note, Topic, Full backup). */
325
+ declare function ExportRow({ title, sub, onCopy, copyLabel, onDownload }: ExportRowProps): react.JSX.Element;
326
+
327
+ interface TocItem {
328
+ label: string;
329
+ active?: boolean;
330
+ go?: () => void;
331
+ }
332
+ interface TocRailProps extends Omit<HTMLAttributes<HTMLElement>, 'onMouseEnter' | 'onMouseLeave' | 'onFocus' | 'onBlur' | 'aria-label' | 'style'> {
333
+ items: TocItem[];
334
+ /** Hovering (or focusing, via keyboard) the rail expands dots into labels. */
335
+ hovered?: boolean;
336
+ onHoverIn?: () => void;
337
+ onHoverOut?: () => void;
338
+ 'aria-label'?: string;
339
+ style?: CSSProperties;
340
+ }
341
+ /**
342
+ * TocRail — desktop floating-dot outline rail (scroll-spy), hover to reveal
343
+ * labels.
344
+ *
345
+ * The source design only expands labels on mouse hover (onMouseEnter/
346
+ * onMouseLeave on the wrapping div) — a keyboard user tabbing through the
347
+ * rail's buttons never sees the labels, only bare dots. Also triggers the
348
+ * same expansion on focus-within (with a relatedTarget check so tabbing
349
+ * between the rail's own buttons doesn't flicker collapsed/expanded), and
350
+ * wraps the items in nav > ol so screen readers announce it as a real list
351
+ * inside a labeled navigation landmark, with aria-current marking the
352
+ * active section (this is exactly what aria-current is for — the current
353
+ * location within a browsing context — unlike a view-switching control).
354
+ * role="list" on the <ol> works around a long-standing Safari/VoiceOver
355
+ * quirk where `list-style: none` drops the implicit list semantics.
356
+ */
357
+ declare function TocRail({ items, hovered, onHoverIn, onHoverOut, 'aria-label': ariaLabel, style, ...rest }: TocRailProps): react.JSX.Element;
358
+
359
+ interface TocFabProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type'> {
360
+ onClick?: () => void;
361
+ /** Accessible name — the glyph alone ("☰") has no meaning to a screen reader. */
362
+ 'aria-label'?: string;
363
+ style?: CSSProperties;
364
+ }
365
+ /** TocFab — mobile floating action button that opens the TOC bottom sheet. */
366
+ declare function TocFab({ onClick, 'aria-label': ariaLabel, style, ...rest }: TocFabProps): react.JSX.Element;
367
+
368
+ interface TocSheetListProps {
369
+ items: TocItem[];
370
+ }
371
+ /**
372
+ * TocSheetList — the outline list rendered inside a BottomSheet on mobile.
373
+ *
374
+ * Wrapped in nav > ol (screen readers announce it as a real list inside a
375
+ * navigation landmark) with the visible "OUTLINE" eyebrow as the landmark's
376
+ * accessible name via aria-labelledby, rather than a redundant separate
377
+ * aria-label repeating the same text. aria-current marks the active section
378
+ * — this is exactly what aria-current is for (current location within a
379
+ * browsing context), matching TocRail. role="list" on the <ol> works around
380
+ * a long-standing Safari/VoiceOver quirk where `list-style: none` drops the
381
+ * implicit list semantics.
382
+ */
383
+ declare function TocSheetList({ items }: TocSheetListProps): react.JSX.Element;
384
+
385
+ interface CardShellProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
386
+ children?: ReactNode;
387
+ dark?: boolean;
388
+ tint?: boolean;
389
+ style?: CSSProperties;
390
+ }
391
+ /** CardShell — shared white-surface wrapper most content-card types use. */
392
+ declare function CardShell({ children, dark, tint, style, ...rest }: CardShellProps): react.JSX.Element;
393
+
394
+ interface CardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
395
+ /** e.g. "INTERACTIVE", "WALKTHROUGH", "CODE", "COMPARE", "CHECKPOINT" */
396
+ eyebrow: string;
397
+ eyebrowColor?: string;
398
+ title?: string;
399
+ /**
400
+ * Element the title renders as. Defaults to a plain `span` (matches the
401
+ * source design exactly). Content cards sit at an arbitrary point in a
402
+ * note's document outline that only the consuming app knows — this
403
+ * library can't safely guess a heading level (h2 vs h3 vs h4) without
404
+ * risking an inconsistent or skipped-level heading hierarchy. Pass e.g.
405
+ * "h3" here when the consumer knows the right level and wants the title
406
+ * reachable via screen-reader heading navigation.
407
+ */
408
+ as?: ElementType;
409
+ subtitle?: string;
410
+ right?: ReactNode;
411
+ dark?: boolean;
412
+ style?: CSSProperties;
413
+ }
414
+ /** CardHeader — eyebrow + title row shared by interactive/walkthrough/code/table/quiz/image/related/references cards. */
415
+ declare function CardHeader({ eyebrow, eyebrowColor, title, as: TitleTag, subtitle, right, dark, style, ...rest }: CardHeaderProps): react.JSX.Element;
416
+
417
+ interface TextBlockProps extends Omit<HTMLAttributes<HTMLParagraphElement>, 'style' | 'children'> {
418
+ text: string;
419
+ style?: CSSProperties;
420
+ }
421
+ /** TextBlock — plain paragraph content block (no card shell, no eyebrow). */
422
+ declare function TextBlock({ text, style, ...rest }: TextBlockProps): react.JSX.Element;
423
+
424
+ interface EquationBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
425
+ /** Raw TeX/LaTeX source — real product renders via KaTeX (`<ka-tex>`). */
426
+ tex: string;
427
+ style?: CSSProperties;
428
+ }
429
+ /**
430
+ * EquationBlock — display-math content block. Renders bare (no card shell).
431
+ * The real product uses a `<ka-tex>` custom element backed by KaTeX; this
432
+ * design-system stand-in renders the raw TeX in an italic serif style so
433
+ * the layout/spacing is representative without a KaTeX runtime dependency.
434
+ * Consumers should mount the actual `ka-tex` element (see readme.md).
435
+ */
436
+ declare function EquationBlock({ tex, style, ...rest }: EquationBlockProps): react.JSX.Element;
437
+
438
+ interface IntuitionCalloutProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
439
+ text: string;
440
+ /** e.g. "INTUITION" or "THIRD LAW" for a named-principle variant. */
441
+ label?: string;
442
+ style?: CSSProperties;
443
+ }
444
+ /** IntuitionCallout — tinted amber-ochre inline callout box (plain-language intuition, no card shell/border-radius-2xl but its own padded box). */
445
+ declare function IntuitionCallout({ text, label, style, ...rest }: IntuitionCalloutProps): react.JSX.Element;
446
+
447
+ interface WalkthroughStep {
448
+ tex: string;
449
+ note: string;
450
+ }
451
+ interface WalkthroughCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
452
+ title: string;
453
+ steps: WalkthroughStep[];
454
+ currentStep: number;
455
+ onPrev?: () => void;
456
+ onNext?: () => void;
457
+ style?: CSSProperties;
458
+ }
459
+ /**
460
+ * WalkthroughCard — numbered step-through derivation. Steps reveal one at
461
+ * a time via Back/Next (earlier steps stay visible, dimmed).
462
+ *
463
+ * Two fixes over the source: the Next button now gets a real `disabled`
464
+ * attribute at the last step (it previously only dimmed via opacity while
465
+ * remaining fully clickable — inconsistent with Back, which already
466
+ * disables for real at the first step, and would let Next keep firing past
467
+ * the end). The revealed-steps list is wrapped in an aria-live region so
468
+ * screen reader users are told a new step appeared after Next/Back, rather
469
+ * than needing to rediscover it themselves.
470
+ */
471
+ declare function WalkthroughCard({ title, steps, currentStep, onPrev, onNext, style, ...rest }: WalkthroughCardProps): react.JSX.Element;
472
+
473
+ interface CodeCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
474
+ title: string;
475
+ lang: string;
476
+ code: string;
477
+ copyLabel?: string;
478
+ onCopy?: () => void;
479
+ style?: CSSProperties;
480
+ }
481
+ /** CodeCard — dark code block with copy button + optional My-note annotation trigger. */
482
+ declare function CodeCard({ title, lang, code, copyLabel, onCopy, style, ...rest }: CodeCardProps): react.JSX.Element;
483
+
484
+ interface TableCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
485
+ title: string;
486
+ header: string[];
487
+ rows: string[][];
488
+ style?: CSSProperties;
489
+ }
490
+ /**
491
+ * TableCard — "COMPARE" content block; a labeled comparison table.
492
+ *
493
+ * The source markup rendered every cell — header row included — as `<td>`
494
+ * inside a single flat `<tr>` list, with no `<thead>`/`<tbody>` split: a
495
+ * screen reader navigating the table got no column-header announcements at
496
+ * all. This shape is actually a two-way table (header[0] is blank because
497
+ * it's the corner above the row-label column — rows[i][0] labels that row,
498
+ * e.g. "Memory", the same way header[1..] label columns, e.g. "SGD"). Fixed
499
+ * to `<thead><tr><th scope="col">` for the header row and `<th scope="row">`
500
+ * for each row's first cell — the standard markup for a table with both row
501
+ * and column headers. The empty corner header cell gets a visually-hidden
502
+ * label (real content tables always pass a non-empty `header[0]` in
503
+ * practice, but an empty `<th>` fails an accessibility check regardless of
504
+ * why it's empty, so this covers that case defensively too).
505
+ */
506
+ declare function TableCard({ title, header, rows, style, ...rest }: TableCardProps): react.JSX.Element;
507
+
508
+ interface QuizCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
509
+ question: string;
510
+ answerOpen: boolean;
511
+ answer: string;
512
+ quizLabel: string;
513
+ onToggleQuiz?: () => void;
514
+ addFlashLabel: string;
515
+ onAddFlash?: () => void;
516
+ style?: CSSProperties;
517
+ }
518
+ /**
519
+ * QuizCard — "CHECKPOINT" self-test block: question, reveal-answer toggle,
520
+ * add-to-flashcards. Uses the accent-tinted background (mixes with the
521
+ * active topic color).
522
+ *
523
+ * The reveal-answer button is a disclosure widget (it shows/hides the
524
+ * answer panel below), so it now carries aria-expanded and aria-controls —
525
+ * the source only changed the button's visible text (quizLabel), giving
526
+ * assistive tech no programmatic signal of the panel's state or its
527
+ * relationship to the toggle, same gap fixed on Sidebar's chevron earlier.
528
+ */
529
+ declare function QuizCard({ question, answerOpen, answer, quizLabel, onToggleQuiz, addFlashLabel, onAddFlash, style, ...rest }: QuizCardProps): react.JSX.Element;
530
+
531
+ interface ImageCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
532
+ /** "UPLOAD" (user's own work) or "FIGURE" (author-provided diagram). */
533
+ label?: 'UPLOAD' | 'FIGURE';
534
+ title: string;
535
+ /** Unique id — required for the drop to persist (see image-slot.js). */
536
+ slotId: string;
537
+ placeholder?: string;
538
+ height?: number;
539
+ caption?: string;
540
+ style?: CSSProperties;
541
+ }
542
+ /**
543
+ * ImageCard — "UPLOAD"/"FIGURE" content block wrapping the product's
544
+ * `<image-slot>` web component (drag-and-drop image placeholder).
545
+ *
546
+ * `<image-slot>` is an opaque vendored utility, not a component this library
547
+ * owns — its internal accessibility (accessible name, keyboard operability
548
+ * of the drop zone) lives in that script, out of this port's scope. See
549
+ * readme.md for the empty/filled state spec, flagged there as ambiguous.
550
+ */
551
+ declare function ImageCard({ label, title, slotId, placeholder, height, caption, style, ...rest }: ImageCardProps): react.JSX.Element;
552
+
553
+ interface RelatedCardItem {
554
+ label: string;
555
+ color: string;
556
+ onClick?: (event: MouseEvent) => void;
557
+ /** Renders as a real link (enabling native middle-click/Cmd-click "open in new tab") instead of a button. */
558
+ href?: string;
559
+ /** Render as a custom element/component — e.g. a router's Link — instead of the default <a>/<button>. Receives href and onClick. */
560
+ as?: ElementType;
561
+ }
562
+ interface RelatedCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
563
+ items: RelatedCardItem[];
564
+ style?: CSSProperties;
565
+ }
566
+ /**
567
+ * RelatedCard — "RELATED" content block linking to other concepts.
568
+ *
569
+ * Items are wrapped in a real <ul role="list">/<li> instead of bare
570
+ * <button>s, so screen readers announce "list of N items" and let a user
571
+ * navigate it as a list; the trailing "›" glyph is aria-hidden since it's
572
+ * purely decorative next to each item's own visible label.
573
+ *
574
+ * Each item renders as a real <a href> when `href` is given (so
575
+ * middle-click/Cmd-click "open in new tab" works natively, which a <button>
576
+ * can never support), or as a fully custom element via `as` (e.g. a router's
577
+ * Link component) — falling back to a plain <button> only when neither is
578
+ * given.
579
+ */
580
+ declare function RelatedCard({ items, style, ...rest }: RelatedCardProps): react.JSX.Element;
581
+
582
+ interface ReferencesCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
583
+ items: string[];
584
+ style?: CSSProperties;
585
+ }
586
+ /**
587
+ * ReferencesCard — "REFERENCES" content block: a simple citation list.
588
+ *
589
+ * Items are wrapped in a real <ul role="list">/<li> instead of bare <div>s,
590
+ * so screen readers announce "list of N items"; the leading "›" glyph is
591
+ * aria-hidden since it's a purely decorative bullet marker.
592
+ */
593
+ declare function ReferencesCard({ items, style, ...rest }: ReferencesCardProps): react.JSX.Element;
594
+
595
+ interface AnnotationPanelProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange' | 'placeholder' | 'style' | 'aria-labelledby' | 'rows'> {
596
+ open: boolean;
597
+ value: string;
598
+ onChange: ChangeEventHandler<HTMLTextAreaElement>;
599
+ placeholder?: string;
600
+ }
601
+ /**
602
+ * AnnotationPanel — the "MY NOTE" panel appended to every content-card and
603
+ * practice-problem type. Always amber, regardless of the active topic
604
+ * accent. Font is the hand-script Kalam — the ONLY place it's used.
605
+ *
606
+ * The visible "MY NOTE" label had no programmatic association with the
607
+ * textarea below it (just adjacent markup) — linked via aria-labelledby so
608
+ * the textarea has a real accessible name instead of none. Native textarea
609
+ * attributes (disabled, maxLength, onKeyDown, onBlur, etc.) spread onto the
610
+ * textarea itself, since that's the one interactive element here — the
611
+ * outer wrapper div has no independent need for them.
612
+ */
613
+ declare function AnnotationPanel({ open, value, onChange, placeholder, ...rest }: AnnotationPanelProps): react.JSX.Element | null;
614
+
615
+ interface AnnotationToggleProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type'> {
616
+ hasText?: boolean;
617
+ open?: boolean;
618
+ onClick?: MouseEventHandler<HTMLButtonElement>;
619
+ /** Row is hovered — reveals the otherwise-invisible toggle. */
620
+ hot?: boolean;
621
+ }
622
+ /**
623
+ * AnnotationToggle — the small "+ My note" trigger button shown in a card's
624
+ * header row.
625
+ *
626
+ * The source only reveals this button when the parent row reports `hot`
627
+ * (mouse hover). A keyboard user tabbing to the button directly (without
628
+ * the row ever being "hot") would land on an invisible-but-focusable
629
+ * control with no visual indication it exists until they arrive at it.
630
+ * Reusing the shared useHover hook makes the button also reveal itself on
631
+ * its own hover/focus, so tabbing to it makes it visible the same way
632
+ * hovering the row does.
633
+ */
634
+ declare function AnnotationToggle({ hasText, open, onClick, hot, onMouseEnter, onMouseLeave, onFocus, onBlur, ...rest }: AnnotationToggleProps): react.JSX.Element;
635
+
636
+ interface McqOptionProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type' | 'disabled'> {
637
+ label: string;
638
+ /** 'default' before pick; 'correct'/'wrong' after pick (shown on both the chosen option and the true correct one). */
639
+ state?: 'default' | 'correct' | 'wrong';
640
+ onClick?: () => void;
641
+ }
642
+ /**
643
+ * McqOption — a single multiple-choice option row. States: default, hover,
644
+ * picked-correct, picked-wrong.
645
+ *
646
+ * Two fixes over the source: the ✓/✕ mark was the ONLY signal of an
647
+ * option's correctness, conveyed purely as a raw glyph character with no
648
+ * semantic backing — some screen readers won't announce it meaningfully at
649
+ * all, and unlike a decorative icon next to a text label, hiding it outright
650
+ * would remove the only indicator entirely. Kept the mark visible but added
651
+ * adjacent visually-hidden text ("Correct"/"Incorrect") so the state reaches
652
+ * assistive tech in words, not just a symbol. Also: once a state is set
653
+ * (the question has been answered), the option is no longer meant to be
654
+ * re-clickable, but the source left every option fully interactive
655
+ * regardless of state — added a real `disabled` once state !== 'default'.
656
+ */
657
+ declare function McqOption({ label, state, onClick, ...rest }: McqOptionProps): react.JSX.Element;
658
+
659
+ interface FillBlankInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
660
+ value: string;
661
+ onChange: ChangeEventHandler<HTMLInputElement>;
662
+ checked?: boolean;
663
+ correct?: boolean;
664
+ onCheck?: () => void;
665
+ resultLabel?: string;
666
+ style?: CSSProperties;
667
+ }
668
+ /**
669
+ * FillBlankInput — fill-in-the-blank answer row with Check button + result
670
+ * state.
671
+ *
672
+ * Reuses the TextInput component (with a style override for the
673
+ * correct/incorrect border color) instead of a bare hand-rolled <input>. The
674
+ * input had no accessible name at all beyond its placeholder (not reliable
675
+ * across browsers/screen readers) — added one, plus aria-invalid reflecting
676
+ * the check result programmatically, not just via border color. The result
677
+ * panel is wrapped in an aria-live region so screen reader users hear the
678
+ * outcome as soon as Check is pressed, rather than needing to find it.
679
+ */
680
+ declare function FillBlankInput({ value, onChange, checked, correct, onCheck, resultLabel, style, ...rest }: FillBlankInputProps): react.JSX.Element;
681
+
682
+ interface ScenarioBoxProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
683
+ text: string;
684
+ style?: CSSProperties;
685
+ }
686
+ /** ScenarioBox — amber-tinted scenario framing shown above a practice prompt. */
687
+ declare function ScenarioBox({ text, style, ...rest }: ScenarioBoxProps): react.JSX.Element;
688
+
689
+ interface FlashcardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick' | 'style' | 'type' | 'aria-pressed'> {
690
+ flipped: boolean;
691
+ front: string;
692
+ back: string;
693
+ /** e.g. "BOX 3" or "NEW" */
694
+ boxLabel: string;
695
+ onFlip?: () => void;
696
+ }
697
+ /**
698
+ * Flashcard — flippable review card (front prompt / back answer) + box
699
+ * label.
700
+ *
701
+ * Added aria-pressed (the flip button is a real toggle — flipped/unflipped
702
+ * — so this is the correct ARIA state for it) and wrapped the revealed
703
+ * content in an aria-live region so screen reader users hear the answer as
704
+ * soon as the card flips, rather than needing to re-navigate into the
705
+ * button to discover it changed.
706
+ */
707
+ declare function Flashcard({ flipped, front, back, boxLabel, onFlip, ...rest }: FlashcardProps): react.JSX.Element;
708
+
709
+ interface RateButtonsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'role' | 'aria-label'> {
710
+ /** Only enabled once the card is flipped. */
711
+ enabled?: boolean;
712
+ onAgain?: () => void;
713
+ onGood?: () => void;
714
+ onEasy?: () => void;
715
+ style?: CSSProperties;
716
+ }
717
+ /**
718
+ * RateButtons — Again / Good / Easy spaced-repetition rating row, revealed
719
+ * once flipped.
720
+ *
721
+ * The source "disabled" the row purely via `opacity` + `pointer-events:
722
+ * none` on the wrapping div, with no `disabled` attribute on the buttons
723
+ * themselves. `pointer-events: none` blocks mouse clicks but NOT keyboard
724
+ * activation — a keyboard user could Tab to these buttons and press
725
+ * Enter/Space to rate a card before flipping it, bypassing the entire
726
+ * intended flow. Added a real `disabled` to each button so it's actually
727
+ * inert, not just unclickable by mouse.
728
+ */
729
+ declare function RateButtons({ enabled, onAgain, onGood, onEasy, style, ...rest }: RateButtonsProps): react.JSX.Element;
730
+
731
+ export { AnnotationPanel, type AnnotationPanelProps, AnnotationToggle, type AnnotationToggleProps, Avatar, type AvatarProps, Badge, type BadgeProps, BottomSheet, type BottomSheetProps, Button, type ButtonProps, CardHeader, type CardHeaderProps, CardShell, type CardShellProps, CodeCard, type CodeCardProps, ColorSwatch, type ColorSwatchProps, ContextMenu, type ContextMenuItem, type ContextMenuProps, type ContextMenuSwatch, DragHandle, type DragHandleProps, EquationBlock, type EquationBlockProps, ExportRow, type ExportRowProps, FillBlankInput, type FillBlankInputProps, Flashcard, type FlashcardProps, IconButton, type IconButtonProps, ImageCard, type ImageCardProps, ImportExportModal, type ImportExportModalProps, type ImportExportTab, IntuitionCallout, type IntuitionCalloutProps, McqOption, type McqOptionProps, type NavTabId, NavTabs, type NavTabsProps, NoteRow, type NoteRowProps, type NoteRowTag, QuizCard, type QuizCardProps, RateButtons, type RateButtonsProps, ReferencesCard, type ReferencesCardProps, RelatedCard, type RelatedCardItem, type RelatedCardProps, ScenarioBox, type ScenarioBoxProps, Sidebar, type SidebarProps, SolvedStatusButton, type SolvedStatusButtonProps, TableCard, type TableCardProps, TagChip, type TagChipProps, TextArea, type TextAreaProps, TextBlock, type TextBlockProps, TextInput, type TextInputProps, TocFab, type TocFabProps, type TocItem, TocRail, type TocRailProps, TocSheetList, type TocSheetListProps, TopicRow, type TopicRowProps, WalkthroughCard, type WalkthroughCardProps, type WalkthroughStep };