@cosxai/ui 0.25.0 → 1.0.0-alpha.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.
- package/CHANGELOG.md +577 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +33 -7
- package/src/fonts.css +20 -0
- package/src/index.ts +8 -25
- package/src/lib/cn.ts +23 -6
- package/src/styles.css +17 -0
- package/src/theme.css +132 -0
- package/src/tokens/colors.css +112 -0
- package/src/tokens/compat.css +36 -0
- package/src/tokens/elevation.css +14 -0
- package/src/tokens/motion.css +42 -0
- package/src/tokens/spacing.css +55 -0
- package/src/tokens/typography-cjk.css +42 -0
- package/src/tokens/typography.css +81 -0
- package/src/actionbar/ActionBar.tsx +0 -837
- package/src/actionbar/ActionBarButton.tsx +0 -111
- package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
- package/src/actionbar/ActionBarModeHandle.tsx +0 -171
- package/src/actionbar/ActionBarProvider.tsx +0 -91
- package/src/actionbar/actionbar-context.ts +0 -45
- package/src/actionbar/index.ts +0 -28
- package/src/actionbar/types.ts +0 -147
- package/src/actionbar/useActionBarItems.ts +0 -60
- package/src/actionbar/useActionBarMode.ts +0 -144
- package/src/actionbar/useActionBarPanel.ts +0 -56
- package/src/actionbar/useActionBarStatusDot.ts +0 -49
- package/src/actionbar/useActionBarToast.ts +0 -33
- package/src/ambient/AmbientBackdrop.tsx +0 -74
- package/src/ambient/CommandInput.tsx +0 -107
- package/src/ambient/SuperbarStrip.tsx +0 -36
- package/src/ambient/index.ts +0 -6
- package/src/bento/BentoCell.tsx +0 -66
- package/src/bento/BentoGrid.tsx +0 -42
- package/src/bento/index.ts +0 -2
- package/src/command/CommandPalette.tsx +0 -329
- package/src/command/CommandProvider.tsx +0 -57
- package/src/command/command-context.ts +0 -12
- package/src/command/index.ts +0 -6
- package/src/command/rank.ts +0 -50
- package/src/command/types.ts +0 -37
- package/src/command/useCommandSource.ts +0 -37
- package/src/dialogs/DialogsProvider.tsx +0 -216
- package/src/dialogs/Modal.tsx +0 -260
- package/src/dialogs/Toast.tsx +0 -85
- package/src/dialogs/dialogs-context.ts +0 -6
- package/src/dialogs/index.ts +0 -10
- package/src/dialogs/types.ts +0 -37
- package/src/dialogs/useDialogs.ts +0 -8
- package/src/editorial/EditorialSpotlight.tsx +0 -63
- package/src/editorial/Folio.tsx +0 -52
- package/src/editorial/PlateMarker.tsx +0 -33
- package/src/editorial/RomanSection.tsx +0 -65
- package/src/editorial/RunningMarginalia.tsx +0 -65
- package/src/editorial/index.ts +0 -10
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useKeyboardHotkey.ts +0 -93
- package/src/hooks/useReducedMotion.ts +0 -20
- package/src/hooks/useViewport.ts +0 -61
- package/src/layout/Breadcrumb.tsx +0 -74
- package/src/layout/LeftNavRail.tsx +0 -139
- package/src/layout/MobileTabBar.tsx +0 -101
- package/src/layout/NavItem.tsx +0 -128
- package/src/layout/NavSearchTrigger.tsx +0 -88
- package/src/layout/NavSection.tsx +0 -40
- package/src/layout/RightSidebarPanel.tsx +0 -111
- package/src/layout/Shell.tsx +0 -91
- package/src/layout/SidePanel.tsx +0 -316
- package/src/layout/StickyBanner.tsx +0 -83
- package/src/layout/Topbar.tsx +0 -68
- package/src/layout/index.ts +0 -24
- package/src/layout/useNavRailState.ts +0 -69
- package/src/lib/time-utils.ts +0 -44
- package/src/neobrutalism/Marquee.tsx +0 -81
- package/src/neobrutalism/Sticker.tsx +0 -71
- package/src/neobrutalism/index.ts +0 -4
- package/src/primitives/Avatar.tsx +0 -53
- package/src/primitives/Button.tsx +0 -67
- package/src/primitives/Card.tsx +0 -41
- package/src/primitives/Checkbox.tsx +0 -87
- package/src/primitives/Chip.tsx +0 -191
- package/src/primitives/Combobox.tsx +0 -507
- package/src/primitives/CopyField.tsx +0 -139
- package/src/primitives/CountBadge.tsx +0 -50
- package/src/primitives/Input.tsx +0 -204
- package/src/primitives/Kbd.tsx +0 -45
- package/src/primitives/MentionCombobox.tsx +0 -612
- package/src/primitives/MultiCombobox.tsx +0 -327
- package/src/primitives/PageHeader.tsx +0 -77
- package/src/primitives/Radio.tsx +0 -131
- package/src/primitives/SegmentedControl.tsx +0 -135
- package/src/primitives/Select.tsx +0 -779
- package/src/primitives/SignaturePad.tsx +0 -461
- package/src/primitives/Tag.tsx +0 -56
- package/src/primitives/Textarea.tsx +0 -62
- package/src/primitives/ToggleSwitch.tsx +0 -79
- package/src/primitives/Tooltip.tsx +0 -213
- package/src/primitives/index.ts +0 -53
- package/src/pwa/InstallPromptBanner.tsx +0 -132
- package/src/pwa/index.ts +0 -4
- package/src/pwa/manifest.template.json +0 -20
- package/src/pwa/registerSW.ts +0 -55
- package/src/riso/Halftone.tsx +0 -85
- package/src/riso/Misregister.tsx +0 -63
- package/src/riso/RisoStamp.tsx +0 -76
- package/src/riso/index.ts +0 -3
- package/src/sketch/HandUnderline.tsx +0 -53
- package/src/sketch/RoughArrow.tsx +0 -91
- package/src/sketch/RoughBox.tsx +0 -73
- package/src/sketch/StickyNote.tsx +0 -56
- package/src/sketch/index.ts +0 -4
- package/src/styles/base.css +0 -186
- package/src/styles/chrome-ambient.css +0 -235
- package/src/styles/chrome-bento.css +0 -192
- package/src/styles/chrome-editorial.css +0 -168
- package/src/styles/chrome-neobrutalism.css +0 -326
- package/src/styles/chrome-riso.css +0 -339
- package/src/styles/chrome-sketch.css +0 -362
- package/src/styles/chrome-swiss.css +0 -265
- package/src/styles/chrome-terminal.css +0 -272
- package/src/styles/fonts.css +0 -1034
- package/src/styles/index.css +0 -343
- package/src/styles/tokens.css +0 -941
- package/src/terminal/AsciiBox.tsx +0 -65
- package/src/terminal/BrailleSpinner.tsx +0 -46
- package/src/terminal/index.ts +0 -4
- package/src/theme/ThemeProvider.tsx +0 -99
- package/src/theme/index.ts +0 -6
- package/src/theme/inline-script.ts +0 -40
- package/src/theme/theme-context.ts +0 -7
- package/src/theme/types.ts +0 -39
- 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
|
-
}
|
package/src/editorial/Folio.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/editorial/index.ts
DELETED
|
@@ -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";
|
package/src/hooks/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/hooks/useViewport.ts
DELETED
|
@@ -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
|
-
}
|