@cosxai/ui 0.25.0 → 1.0.0-alpha.1

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/NOTICE +11 -0
  3. package/README.md +52 -0
  4. package/package.json +57 -7
  5. package/src/brand.css +13 -0
  6. package/src/components/Badge.tsx +68 -0
  7. package/src/components/Button.tsx +178 -0
  8. package/src/components/Card.tsx +42 -0
  9. package/src/components/Checkbox.tsx +67 -0
  10. package/src/components/Dialog.tsx +111 -0
  11. package/src/components/Field.tsx +66 -0
  12. package/src/components/Figure.tsx +30 -0
  13. package/src/components/Icon.tsx +32 -0
  14. package/src/components/IconButton.tsx +55 -0
  15. package/src/components/Input.tsx +74 -0
  16. package/src/components/Logo.tsx +62 -0
  17. package/src/components/Marker.tsx +26 -0
  18. package/src/components/MetaLabel.tsx +38 -0
  19. package/src/components/Radio.tsx +72 -0
  20. package/src/components/Select.tsx +351 -0
  21. package/src/components/Spinner.tsx +20 -0
  22. package/src/components/Switch.tsx +67 -0
  23. package/src/components/Table.tsx +196 -0
  24. package/src/components/Tabs.tsx +124 -0
  25. package/src/components/Tag.tsx +35 -0
  26. package/src/components/Textarea.tsx +46 -0
  27. package/src/components/ThemeProvider.tsx +80 -0
  28. package/src/components/Toast.tsx +159 -0
  29. package/src/components/Tooltip.tsx +57 -0
  30. package/src/components/core.test.tsx +242 -0
  31. package/src/components/forms-control.ts +36 -0
  32. package/src/components/forms.test.tsx +280 -0
  33. package/src/components/logo-art.ts +4 -0
  34. package/src/components/overlays.test.tsx +245 -0
  35. package/src/components/useButtonAction.ts +57 -0
  36. package/src/fonts.css +20 -0
  37. package/src/index.ts +34 -24
  38. package/src/lib/cn.ts +23 -6
  39. package/src/modes.css +10 -0
  40. package/src/styles.css +19 -0
  41. package/src/theme.css +143 -0
  42. package/src/tokens/colors.css +112 -0
  43. package/src/tokens/compat.css +36 -0
  44. package/src/tokens/elevation.css +14 -0
  45. package/src/tokens/motion.css +42 -0
  46. package/src/tokens/spacing.css +55 -0
  47. package/src/tokens/typography-cjk.css +42 -0
  48. package/src/tokens/typography.css +81 -0
  49. package/src/actionbar/ActionBar.tsx +0 -837
  50. package/src/actionbar/ActionBarButton.tsx +0 -111
  51. package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
  52. package/src/actionbar/ActionBarModeHandle.tsx +0 -171
  53. package/src/actionbar/ActionBarProvider.tsx +0 -91
  54. package/src/actionbar/actionbar-context.ts +0 -45
  55. package/src/actionbar/index.ts +0 -28
  56. package/src/actionbar/types.ts +0 -147
  57. package/src/actionbar/useActionBarItems.ts +0 -60
  58. package/src/actionbar/useActionBarMode.ts +0 -144
  59. package/src/actionbar/useActionBarPanel.ts +0 -56
  60. package/src/actionbar/useActionBarStatusDot.ts +0 -49
  61. package/src/actionbar/useActionBarToast.ts +0 -33
  62. package/src/ambient/AmbientBackdrop.tsx +0 -74
  63. package/src/ambient/CommandInput.tsx +0 -107
  64. package/src/ambient/SuperbarStrip.tsx +0 -36
  65. package/src/ambient/index.ts +0 -6
  66. package/src/bento/BentoCell.tsx +0 -66
  67. package/src/bento/BentoGrid.tsx +0 -42
  68. package/src/bento/index.ts +0 -2
  69. package/src/command/CommandPalette.tsx +0 -329
  70. package/src/command/CommandProvider.tsx +0 -57
  71. package/src/command/command-context.ts +0 -12
  72. package/src/command/index.ts +0 -6
  73. package/src/command/rank.ts +0 -50
  74. package/src/command/types.ts +0 -37
  75. package/src/command/useCommandSource.ts +0 -37
  76. package/src/dialogs/DialogsProvider.tsx +0 -216
  77. package/src/dialogs/Modal.tsx +0 -260
  78. package/src/dialogs/Toast.tsx +0 -85
  79. package/src/dialogs/dialogs-context.ts +0 -6
  80. package/src/dialogs/index.ts +0 -10
  81. package/src/dialogs/types.ts +0 -37
  82. package/src/dialogs/useDialogs.ts +0 -8
  83. package/src/editorial/EditorialSpotlight.tsx +0 -63
  84. package/src/editorial/Folio.tsx +0 -52
  85. package/src/editorial/PlateMarker.tsx +0 -33
  86. package/src/editorial/RomanSection.tsx +0 -65
  87. package/src/editorial/RunningMarginalia.tsx +0 -65
  88. package/src/editorial/index.ts +0 -10
  89. package/src/hooks/index.ts +0 -5
  90. package/src/hooks/useKeyboardHotkey.ts +0 -93
  91. package/src/hooks/useReducedMotion.ts +0 -20
  92. package/src/hooks/useViewport.ts +0 -61
  93. package/src/layout/Breadcrumb.tsx +0 -74
  94. package/src/layout/LeftNavRail.tsx +0 -139
  95. package/src/layout/MobileTabBar.tsx +0 -101
  96. package/src/layout/NavItem.tsx +0 -128
  97. package/src/layout/NavSearchTrigger.tsx +0 -88
  98. package/src/layout/NavSection.tsx +0 -40
  99. package/src/layout/RightSidebarPanel.tsx +0 -111
  100. package/src/layout/Shell.tsx +0 -91
  101. package/src/layout/SidePanel.tsx +0 -316
  102. package/src/layout/StickyBanner.tsx +0 -83
  103. package/src/layout/Topbar.tsx +0 -68
  104. package/src/layout/index.ts +0 -24
  105. package/src/layout/useNavRailState.ts +0 -69
  106. package/src/lib/time-utils.ts +0 -44
  107. package/src/neobrutalism/Marquee.tsx +0 -81
  108. package/src/neobrutalism/Sticker.tsx +0 -71
  109. package/src/neobrutalism/index.ts +0 -4
  110. package/src/primitives/Avatar.tsx +0 -53
  111. package/src/primitives/Button.tsx +0 -67
  112. package/src/primitives/Card.tsx +0 -41
  113. package/src/primitives/Checkbox.tsx +0 -87
  114. package/src/primitives/Chip.tsx +0 -191
  115. package/src/primitives/Combobox.tsx +0 -507
  116. package/src/primitives/CopyField.tsx +0 -139
  117. package/src/primitives/CountBadge.tsx +0 -50
  118. package/src/primitives/Input.tsx +0 -204
  119. package/src/primitives/Kbd.tsx +0 -45
  120. package/src/primitives/MentionCombobox.tsx +0 -612
  121. package/src/primitives/MultiCombobox.tsx +0 -327
  122. package/src/primitives/PageHeader.tsx +0 -77
  123. package/src/primitives/Radio.tsx +0 -131
  124. package/src/primitives/SegmentedControl.tsx +0 -135
  125. package/src/primitives/Select.tsx +0 -779
  126. package/src/primitives/SignaturePad.tsx +0 -461
  127. package/src/primitives/Tag.tsx +0 -56
  128. package/src/primitives/Textarea.tsx +0 -62
  129. package/src/primitives/ToggleSwitch.tsx +0 -79
  130. package/src/primitives/Tooltip.tsx +0 -213
  131. package/src/primitives/index.ts +0 -53
  132. package/src/pwa/InstallPromptBanner.tsx +0 -132
  133. package/src/pwa/index.ts +0 -4
  134. package/src/pwa/manifest.template.json +0 -20
  135. package/src/pwa/registerSW.ts +0 -55
  136. package/src/riso/Halftone.tsx +0 -85
  137. package/src/riso/Misregister.tsx +0 -63
  138. package/src/riso/RisoStamp.tsx +0 -76
  139. package/src/riso/index.ts +0 -3
  140. package/src/sketch/HandUnderline.tsx +0 -53
  141. package/src/sketch/RoughArrow.tsx +0 -91
  142. package/src/sketch/RoughBox.tsx +0 -73
  143. package/src/sketch/StickyNote.tsx +0 -56
  144. package/src/sketch/index.ts +0 -4
  145. package/src/styles/base.css +0 -186
  146. package/src/styles/chrome-ambient.css +0 -235
  147. package/src/styles/chrome-bento.css +0 -192
  148. package/src/styles/chrome-editorial.css +0 -168
  149. package/src/styles/chrome-neobrutalism.css +0 -326
  150. package/src/styles/chrome-riso.css +0 -339
  151. package/src/styles/chrome-sketch.css +0 -362
  152. package/src/styles/chrome-swiss.css +0 -265
  153. package/src/styles/chrome-terminal.css +0 -272
  154. package/src/styles/fonts.css +0 -1034
  155. package/src/styles/index.css +0 -343
  156. package/src/styles/tokens.css +0 -941
  157. package/src/terminal/AsciiBox.tsx +0 -65
  158. package/src/terminal/BrailleSpinner.tsx +0 -46
  159. package/src/terminal/index.ts +0 -4
  160. package/src/theme/ThemeProvider.tsx +0 -99
  161. package/src/theme/index.ts +0 -6
  162. package/src/theme/inline-script.ts +0 -40
  163. package/src/theme/theme-context.ts +0 -7
  164. package/src/theme/types.ts +0 -39
  165. package/src/theme/useTheme.ts +0 -8
@@ -1,63 +0,0 @@
1
- import type { ReactNode, CSSProperties } from "react";
2
-
3
- // Local "spotlight" inversion for editorial chrome — the
4
- // dark-canvas-with-cream-cards pattern from the brief, scoped to
5
- // a single section rather than the whole page.
6
- //
7
- // Sets local CSS vars on a wrapper so every descendant component
8
- // re-reads its tokens from the inverted palette. Pure CSS variable
9
- // re-binding; no JS or context needed. Used inside any chrome —
10
- // the kit doesn't require editorial to be active for the wrapper
11
- // to work — but the visual lineage (cream surface + coral accent)
12
- // reads most strongly under editorial.
13
-
14
- export interface EditorialSpotlightProps {
15
- children: ReactNode;
16
- // Optional inner padding. Default 32 px.
17
- padding?: number | string;
18
- className?: string;
19
- style?: CSSProperties;
20
- }
21
-
22
- export function EditorialSpotlight({
23
- children,
24
- padding = 32,
25
- className,
26
- style,
27
- }: EditorialSpotlightProps) {
28
- return (
29
- <div
30
- className={className}
31
- data-ck-spotlight
32
- style={
33
- {
34
- // Dark canvas surrounding cream-paper cards.
35
- background: "#0E0E0E",
36
- color: "#F4EFE0",
37
- padding,
38
- borderRadius: "var(--ck-radius-md)",
39
- // Rebind tokens so descendant components (Button, Card,
40
- // Tag, etc.) read inverted values. Card surface stays
41
- // cream; everything else flips to dark.
42
- ["--ck-bg-canvas"]: "#0E0E0E",
43
- ["--ck-bg-surface"]: "#F4EFE0",
44
- ["--ck-bg-surface-2"]: "#E5DECB",
45
- ["--ck-bg-muted"]: "#1F1F1F",
46
- ["--ck-text-primary"]: "#F4EFE0",
47
- ["--ck-text-secondary"]: "#C9C3B5",
48
- ["--ck-text-tertiary"]: "#8F897D",
49
- ["--ck-text-inverse"]: "#0F0F0F",
50
- ["--ck-border-subtle"]: "rgba(244, 239, 224, 0.18)",
51
- ["--ck-border-strong"]: "rgba(244, 239, 224, 0.32)",
52
- ...style,
53
- } as CSSProperties
54
- }
55
- >
56
- {/* Inner wrapper re-binds the text colour back to dark
57
- ink so anything sitting on the cream cards inside reads
58
- correctly. <Card> children get this for free via the
59
- token rebinding on .ck-card. */}
60
- {children}
61
- </div>
62
- );
63
- }
@@ -1,52 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- // Folio bar — thin hairline rule above a row of editorial
4
- // metadata. Sits at the top of every section in the editorial
5
- // chrome ("SECTION TITLE · CROSSREF · PLATE 0X" on the left,
6
- // "00X / 008" page number on the right).
7
- //
8
- // Renders fine under any chrome but only feels "right" under
9
- // editorial — the hairline + tracked metadata pair are the
10
- // editorial language, neutral in other chromes.
11
-
12
- export interface FolioProps {
13
- // Left-aligned metadata fragments. Joined with the editorial
14
- // separator (" · ").
15
- left: (string | ReactNode)[];
16
- // Right-aligned fragment (typically the page number).
17
- right?: string | ReactNode;
18
- className?: string;
19
- }
20
-
21
- export function Folio({ left, right, className }: FolioProps) {
22
- return (
23
- <header
24
- className={className}
25
- style={{
26
- display: "flex",
27
- alignItems: "center",
28
- gap: 12,
29
- padding: "10px 0",
30
- borderTop: "1px solid var(--ck-text-primary)",
31
- borderBottom: "1px solid var(--ck-text-primary)",
32
- margin: "0 0 24px",
33
- font: "500 10px/1.2 var(--ck-font-sans)",
34
- letterSpacing: "0.16em",
35
- textTransform: "uppercase",
36
- color: "var(--ck-text-primary)",
37
- }}
38
- >
39
- <div style={{ flex: 1, minWidth: 0, display: "flex", flexWrap: "wrap", gap: "0 8px" }}>
40
- {left.map((seg, i) => (
41
- <span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
42
- {i > 0 && <span style={{ color: "var(--ck-text-tertiary)" }}>·</span>}
43
- <span>{seg}</span>
44
- </span>
45
- ))}
46
- </div>
47
- {right && (
48
- <div style={{ flex: "none", color: "var(--ck-text-secondary)" }}>{right}</div>
49
- )}
50
- </header>
51
- );
52
- }
@@ -1,33 +0,0 @@
1
- // "N° 01" / "N° 02" plate numbering — sits in the top-left of
2
- // editorial cards and stat blocks. Uses small caps for the prefix
3
- // and a tabular numeral so a column of plates aligns visually.
4
-
5
- export interface PlateMarkerProps {
6
- // The numeral, 1-indexed. Zero-padded to 2 digits ("01").
7
- n: number;
8
- // "N°" by default. Override to "Plate", "Vol.", "Issue", etc.
9
- prefix?: string;
10
- className?: string;
11
- }
12
-
13
- export function PlateMarker({ n, prefix = "N°", className }: PlateMarkerProps) {
14
- const padded = n < 10 ? `0${n}` : String(n);
15
- return (
16
- <span
17
- className={className}
18
- style={{
19
- display: "inline-flex",
20
- alignItems: "baseline",
21
- gap: 4,
22
- font: "500 10px/1 var(--ck-font-sans)",
23
- letterSpacing: "0.18em",
24
- textTransform: "uppercase",
25
- color: "var(--ck-text-tertiary)",
26
- fontVariantNumeric: "tabular-nums",
27
- }}
28
- >
29
- <span style={{ fontStyle: "italic" }}>{prefix}</span>
30
- <span style={{ color: "var(--ck-text-primary)" }}>{padded}</span>
31
- </span>
32
- );
33
- }
@@ -1,65 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- // Section break with a Roman-numeral marker. "I.", "II.", ...
4
- // in oversized serif italic, followed by a thin hairline rule.
5
- // Use to delimit major content slabs inside an editorial page.
6
-
7
- const ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
8
-
9
- export interface RomanSectionProps {
10
- // 1-indexed numeral. Anything above XII falls back to the
11
- // arabic numeral so we don't have to ship a full converter.
12
- n: number;
13
- title: ReactNode;
14
- // Optional eyebrow (rendered above the numeral).
15
- eyebrow?: string;
16
- }
17
-
18
- export function RomanSection({ n, title, eyebrow }: RomanSectionProps) {
19
- const numeral = n >= 1 && n <= ROMAN.length ? ROMAN[n - 1] : String(n);
20
- return (
21
- <section style={{ margin: "56px 0 32px" }}>
22
- {eyebrow && (
23
- <div
24
- style={{
25
- font: "500 10px/1 var(--ck-font-sans)",
26
- letterSpacing: "0.18em",
27
- textTransform: "uppercase",
28
- color: "var(--ck-text-tertiary)",
29
- marginBottom: 8,
30
- }}
31
- >
32
- — {eyebrow}
33
- </div>
34
- )}
35
- <div style={{ display: "flex", alignItems: "baseline", gap: 16, marginBottom: 12 }}>
36
- <span
37
- style={{
38
- font: "500 italic 64px/1 var(--ck-font-serif)",
39
- color: "var(--ck-text-primary)",
40
- letterSpacing: "-0.02em",
41
- }}
42
- >
43
- {numeral}.
44
- </span>
45
- <h2
46
- style={{
47
- font: "500 28px/1.1 var(--ck-font-display)",
48
- color: "var(--ck-text-primary)",
49
- margin: 0,
50
- letterSpacing: "-0.01em",
51
- }}
52
- >
53
- {title}
54
- </h2>
55
- </div>
56
- <hr
57
- style={{
58
- border: "none",
59
- borderTop: "1px solid var(--ck-text-primary)",
60
- margin: 0,
61
- }}
62
- />
63
- </section>
64
- );
65
- }
@@ -1,65 +0,0 @@
1
- import type { CSSProperties } from "react";
2
-
3
- // Vertical tracked-uppercase tagline in a far-side gutter — pure
4
- // decoration that sets the magazine tone. Pin to either "left" or
5
- // "right" edge of the viewport. Repeats the tagline string so it
6
- // fills the column regardless of height.
7
- //
8
- // Skip on phones (the gutter doesn't exist on a 360-px-wide page).
9
-
10
- export interface RunningMarginaliaProps {
11
- // The tagline to repeat. Don't include separators — the
12
- // component injects " × " between repetitions.
13
- text: string;
14
- // Which edge to pin to. Default left.
15
- side?: "left" | "right";
16
- // px offset from the edge. Default 24.
17
- inset?: number;
18
- // How many repetitions to print. Default 8 — plenty for any
19
- // viewport at the default font size.
20
- repeat?: number;
21
- // Hide below this viewport width (px). Default 1024.
22
- hideBelow?: number;
23
- className?: string;
24
- }
25
-
26
- export function RunningMarginalia({
27
- text,
28
- side = "left",
29
- inset = 24,
30
- repeat = 8,
31
- hideBelow = 1024,
32
- className,
33
- }: RunningMarginaliaProps) {
34
- const display: CSSProperties =
35
- typeof window !== "undefined" && window.innerWidth < hideBelow
36
- ? { display: "none" }
37
- : {};
38
- const fragments = new Array(repeat).fill(text).join(" × ");
39
- return (
40
- <aside
41
- aria-hidden
42
- className={className}
43
- style={{
44
- position: "fixed",
45
- top: "50%",
46
- [side]: inset,
47
- transform:
48
- side === "left"
49
- ? "translateY(-50%) rotate(-90deg)"
50
- : "translateY(-50%) rotate(90deg)",
51
- transformOrigin: side === "left" ? "left center" : "right center",
52
- font: "500 10px/1 var(--ck-font-sans)",
53
- letterSpacing: "0.36em",
54
- textTransform: "uppercase",
55
- color: "var(--ck-text-tertiary)",
56
- whiteSpace: "nowrap",
57
- pointerEvents: "none",
58
- zIndex: 5,
59
- ...display,
60
- }}
61
- >
62
- {fragments}
63
- </aside>
64
- );
65
- }
@@ -1,10 +0,0 @@
1
- export { Folio } from "./Folio";
2
- export type { FolioProps } from "./Folio";
3
- export { PlateMarker } from "./PlateMarker";
4
- export type { PlateMarkerProps } from "./PlateMarker";
5
- export { RunningMarginalia } from "./RunningMarginalia";
6
- export type { RunningMarginaliaProps } from "./RunningMarginalia";
7
- export { RomanSection } from "./RomanSection";
8
- export type { RomanSectionProps } from "./RomanSection";
9
- export { EditorialSpotlight } from "./EditorialSpotlight";
10
- export type { EditorialSpotlightProps } from "./EditorialSpotlight";
@@ -1,5 +0,0 @@
1
- export { useViewport } from "./useViewport";
2
- export type { Viewport, ViewportBreakpoints } from "./useViewport";
3
- export { useReducedMotion } from "./useReducedMotion";
4
- export { useKeyboardHotkey } from "./useKeyboardHotkey";
5
- export type { UseKeyboardHotkeyOptions } from "./useKeyboardHotkey";
@@ -1,93 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
-
3
- // Single-key hotkey listener with the standard conflict guards:
4
- // - skipInputs (default true) — ignore when an input /
5
- // textarea / select / contenteditable is focused
6
- // - skipModifiers (default true) — let browser shortcuts
7
- // (Cmd+S, Ctrl+W, etc.) through
8
- // - skipWhenModalOpen (default true) — don't fire when a
9
- // <Modal> (anything with role="dialog") is mounted
10
- //
11
- // `key` is a single character (case-insensitive) or a special key
12
- // name ("Escape", "Enter", "ArrowUp", etc.). For modifier
13
- // combinations use `mod: true` (Cmd on Mac, Ctrl elsewhere).
14
- //
15
- // Handler refs are kept fresh via a ref-mirror — call sites don't
16
- // need to memoise the callback.
17
-
18
- export interface UseKeyboardHotkeyOptions {
19
- // Combine with Cmd (Mac) / Ctrl (other). Default false.
20
- mod?: boolean;
21
- skipInputs?: boolean;
22
- skipModifiers?: boolean; // ignored when `mod: true`
23
- skipWhenModalOpen?: boolean;
24
- // Disable without unmounting (toggle off without remount).
25
- enabled?: boolean;
26
- }
27
-
28
- const isMac = () =>
29
- typeof navigator !== "undefined" &&
30
- /Mac|iPhone|iPad|iPod/.test(navigator.platform);
31
-
32
- export function useKeyboardHotkey(
33
- key: string,
34
- handler: (e: KeyboardEvent) => void,
35
- options: UseKeyboardHotkeyOptions = {},
36
- ) {
37
- const {
38
- mod = false,
39
- skipInputs = true,
40
- skipModifiers = true,
41
- skipWhenModalOpen = true,
42
- enabled = true,
43
- } = options;
44
-
45
- const handlerRef = useRef(handler);
46
- handlerRef.current = handler;
47
-
48
- useEffect(() => {
49
- if (!enabled) return;
50
- const target = key.toLowerCase();
51
- const onKey = (e: KeyboardEvent) => {
52
- if (e.key.toLowerCase() !== target) return;
53
- if (mod) {
54
- const wantedMod = isMac() ? e.metaKey : e.ctrlKey;
55
- if (!wantedMod) return;
56
- if (e.shiftKey || e.altKey) return;
57
- } else if (skipModifiers && (e.metaKey || e.ctrlKey || e.altKey)) {
58
- return;
59
- }
60
- if (skipInputs) {
61
- const ae = document.activeElement as HTMLElement | null;
62
- const tag = ae?.tagName;
63
- // Containers marked data-hotkey-passthrough host editables
64
- // the user never types into (e.g. a canvas spreadsheet's
65
- // hidden cell-editor focus trap) — hotkeys stay live there.
66
- // Mirrors the same convention in consumer apps.
67
- const passthrough = !!ae?.closest?.('[data-hotkey-passthrough="true"]');
68
- if (
69
- !passthrough &&
70
- (tag === "INPUT" ||
71
- tag === "TEXTAREA" ||
72
- tag === "SELECT" ||
73
- ae?.isContentEditable)
74
- ) {
75
- return;
76
- }
77
- }
78
- if (skipWhenModalOpen && document.querySelector('[role="dialog"]')) {
79
- return;
80
- }
81
- // The key is OURS once every guard passed — swallow its default
82
- // action. Without this, a hotkey that opens a dialog with an
83
- // autofocused input sees the same keystroke land as text ("s"
84
- // typed into the invite search, meta QA 2026-08-19): the
85
- // browser's text-insertion runs AFTER keydown, by which time
86
- // focus has moved into the new input.
87
- e.preventDefault();
88
- handlerRef.current(e);
89
- };
90
- window.addEventListener("keydown", onKey);
91
- return () => window.removeEventListener("keydown", onKey);
92
- }, [key, mod, skipInputs, skipModifiers, skipWhenModalOpen, enabled]);
93
- }
@@ -1,20 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- // Boolean subscription to the OS-level `prefers-reduced-motion`
4
- // query. Use in component code that wants to short-circuit
5
- // non-essential animations (the kit's CSS already respects the
6
- // query via `@media (prefers-reduced-motion: reduce)` so most
7
- // components don't need this — only call sites that wrap their
8
- // own JS-driven motion (canvas animations, scroll-jacking, etc.)).
9
-
10
- export function useReducedMotion(): boolean {
11
- const [prefers, setPrefers] = useState(false);
12
- useEffect(() => {
13
- const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
14
- setPrefers(mq.matches);
15
- const onChange = (e: MediaQueryListEvent) => setPrefers(e.matches);
16
- mq.addEventListener("change", onChange);
17
- return () => mq.removeEventListener("change", onChange);
18
- }, []);
19
- return prefers;
20
- }
@@ -1,61 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- // Viewport size + named breakpoints. Layout components subscribe to
4
- // this to switch desktop ↔ mobile shells, force-collapse the nav
5
- // rail below a width threshold, etc.
6
- //
7
- // Single window listener for all consumers (mounted once per call
8
- // site — keep usage minimal). For pages with many consumers, lift
9
- // to a context if perf is ever an issue; not needed yet.
10
-
11
- export interface Viewport {
12
- width: number;
13
- height: number;
14
- // Below this, render the mobile shell (bottom tab bar + slide-out nav).
15
- isPhone: boolean;
16
- // Below this, force-collapse the left nav rail to icon-only.
17
- isNarrow: boolean;
18
- // Below this, hide the right rail entirely.
19
- hidesRightRail: boolean;
20
- }
21
-
22
- export interface ViewportBreakpoints {
23
- phone: number;
24
- narrow: number;
25
- rightRail: number;
26
- }
27
-
28
- const DEFAULTS: ViewportBreakpoints = {
29
- phone: 768,
30
- narrow: 1100,
31
- rightRail: 1280,
32
- };
33
-
34
- function snapshot(b: ViewportBreakpoints): Viewport {
35
- if (typeof window === "undefined") {
36
- return { width: 1440, height: 900, isPhone: false, isNarrow: false, hidesRightRail: false };
37
- }
38
- const w = window.innerWidth;
39
- const h = window.innerHeight;
40
- return {
41
- width: w,
42
- height: h,
43
- isPhone: w < b.phone,
44
- isNarrow: w < b.narrow,
45
- hidesRightRail: w < b.rightRail,
46
- };
47
- }
48
-
49
- export function useViewport(breakpoints: Partial<ViewportBreakpoints> = {}): Viewport {
50
- const b: ViewportBreakpoints = { ...DEFAULTS, ...breakpoints };
51
- const [vp, setVp] = useState<Viewport>(() => snapshot(b));
52
- useEffect(() => {
53
- const onResize = () => setVp(snapshot(b));
54
- onResize();
55
- window.addEventListener("resize", onResize);
56
- return () => window.removeEventListener("resize", onResize);
57
- // Breakpoints are stable across renders for any sane caller; stringify
58
- // to avoid unstable-object-identity re-runs without losing reactivity.
59
- }, [b.phone, b.narrow, b.rightRail]);
60
- return vp;
61
- }
@@ -1,74 +0,0 @@
1
- import { Fragment, type ReactNode } from "react";
2
-
3
- // Crumb item — either a label (terminal node, no link) or a label
4
- // with a click handler. The kit doesn't ship router integration —
5
- // consumers pass plain onClick or wrap children in their own router's
6
- // link primitive (e.g. <NavLink to={...}>{label}</NavLink>).
7
-
8
- export interface CrumbItem {
9
- label: string;
10
- onClick?: () => void;
11
- // Custom render — escape hatch for "wrap this crumb in <Link>".
12
- // When set, label/onClick are ignored.
13
- render?: ReactNode;
14
- }
15
-
16
- export interface BreadcrumbProps {
17
- items: ReadonlyArray<string | CrumbItem>;
18
- // Slot to the right of the last crumb (e.g. doc-meta popover trigger).
19
- accessory?: ReactNode;
20
- className?: string;
21
- }
22
-
23
- function normalise(item: string | CrumbItem): CrumbItem {
24
- return typeof item === "string" ? { label: item } : item;
25
- }
26
-
27
- // Phase-2 breadcrumb: simple chain with separator dots. The full
28
- // middle-collapse + narrow-viewport popover fallback from deck-kit
29
- // is deferred to a later iteration once the kit's surface stabilises.
30
- // For now, long chains wrap; consumers should avoid >4 crumbs.
31
- export function Breadcrumb({ items, accessory, className }: BreadcrumbProps) {
32
- const list = items.map(normalise);
33
- return (
34
- <nav
35
- aria-label="Breadcrumb"
36
- className={className}
37
- style={{
38
- display: "flex",
39
- alignItems: "center",
40
- gap: 6,
41
- font: "400 12px/1 var(--ck-font-mono)",
42
- color: "var(--ck-text-tertiary)",
43
- letterSpacing: "0.04em",
44
- textTransform: "uppercase",
45
- minWidth: 0,
46
- flex: "1 1 auto",
47
- }}
48
- >
49
- {list.map((crumb, i) => {
50
- const isLast = i === list.length - 1;
51
- return (
52
- <Fragment key={i}>
53
- {i > 0 && (
54
- <span aria-hidden style={{ color: "var(--ck-text-disabled)" }}>·</span>
55
- )}
56
- <span
57
- style={{
58
- color: isLast ? "var(--ck-text-primary)" : "var(--ck-text-tertiary)",
59
- whiteSpace: "nowrap",
60
- overflow: "hidden",
61
- textOverflow: "ellipsis",
62
- cursor: crumb.onClick ? "pointer" : "default",
63
- }}
64
- onClick={crumb.onClick}
65
- >
66
- {crumb.render ?? crumb.label}
67
- </span>
68
- </Fragment>
69
- );
70
- })}
71
- {accessory && <span style={{ marginLeft: 4 }}>{accessory}</span>}
72
- </nav>
73
- );
74
- }