@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.
- package/CHANGELOG.md +597 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +57 -7
- package/src/brand.css +13 -0
- package/src/components/Badge.tsx +68 -0
- package/src/components/Button.tsx +178 -0
- package/src/components/Card.tsx +42 -0
- package/src/components/Checkbox.tsx +67 -0
- package/src/components/Dialog.tsx +111 -0
- package/src/components/Field.tsx +66 -0
- package/src/components/Figure.tsx +30 -0
- package/src/components/Icon.tsx +32 -0
- package/src/components/IconButton.tsx +55 -0
- package/src/components/Input.tsx +74 -0
- package/src/components/Logo.tsx +62 -0
- package/src/components/Marker.tsx +26 -0
- package/src/components/MetaLabel.tsx +38 -0
- package/src/components/Radio.tsx +72 -0
- package/src/components/Select.tsx +351 -0
- package/src/components/Spinner.tsx +20 -0
- package/src/components/Switch.tsx +67 -0
- package/src/components/Table.tsx +196 -0
- package/src/components/Tabs.tsx +124 -0
- package/src/components/Tag.tsx +35 -0
- package/src/components/Textarea.tsx +46 -0
- package/src/components/ThemeProvider.tsx +80 -0
- package/src/components/Toast.tsx +159 -0
- package/src/components/Tooltip.tsx +57 -0
- package/src/components/core.test.tsx +242 -0
- package/src/components/forms-control.ts +36 -0
- package/src/components/forms.test.tsx +280 -0
- package/src/components/logo-art.ts +4 -0
- package/src/components/overlays.test.tsx +245 -0
- package/src/components/useButtonAction.ts +57 -0
- package/src/fonts.css +20 -0
- package/src/index.ts +34 -24
- package/src/lib/cn.ts +23 -6
- package/src/modes.css +10 -0
- package/src/styles.css +19 -0
- package/src/theme.css +143 -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,36 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
// Thin gradient strip — the Superbar pattern reusable as a
|
|
4
|
-
// horizontal divider, a tab underline, or a progress bar fill.
|
|
5
|
-
// 1-2 px tall by default; override via `thickness`.
|
|
6
|
-
|
|
7
|
-
export interface SuperbarStripProps {
|
|
8
|
-
// px height. Default 1 (hairline divider).
|
|
9
|
-
thickness?: number;
|
|
10
|
-
// CSS width — defaults to 100%.
|
|
11
|
-
width?: string | number;
|
|
12
|
-
className?: string;
|
|
13
|
-
style?: CSSProperties;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function SuperbarStrip({
|
|
17
|
-
thickness = 1,
|
|
18
|
-
width = "100%",
|
|
19
|
-
className,
|
|
20
|
-
style,
|
|
21
|
-
}: SuperbarStripProps) {
|
|
22
|
-
return (
|
|
23
|
-
<div
|
|
24
|
-
aria-hidden
|
|
25
|
-
className={className}
|
|
26
|
-
style={{
|
|
27
|
-
width,
|
|
28
|
-
height: thickness,
|
|
29
|
-
background: "var(--ck-superbar)",
|
|
30
|
-
borderRadius: thickness >= 2 ? thickness / 2 : 0,
|
|
31
|
-
opacity: 0.85,
|
|
32
|
-
...style,
|
|
33
|
-
}}
|
|
34
|
-
/>
|
|
35
|
-
);
|
|
36
|
-
}
|
package/src/ambient/index.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { CommandInput } from "./CommandInput";
|
|
2
|
-
export type { CommandInputProps } from "./CommandInput";
|
|
3
|
-
export { SuperbarStrip } from "./SuperbarStrip";
|
|
4
|
-
export type { SuperbarStripProps } from "./SuperbarStrip";
|
|
5
|
-
export { AmbientBackdrop } from "./AmbientBackdrop";
|
|
6
|
-
export type { AmbientBackdropProps } from "./AmbientBackdrop";
|
package/src/bento/BentoCell.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties, MouseEventHandler, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// One cell inside a <BentoGrid>. Spans configurable column + row
|
|
4
|
-
// counts; surface styling matches the chrome (22px radius +
|
|
5
|
-
// soft multi-layer shadow under bento). `interactive` enables the
|
|
6
|
-
// hover-lift signature animation defined in chrome-bento.css.
|
|
7
|
-
|
|
8
|
-
export interface BentoCellProps {
|
|
9
|
-
children?: ReactNode;
|
|
10
|
-
colSpan?: number;
|
|
11
|
-
rowSpan?: number;
|
|
12
|
-
// Override the cell's background. Useful for media + gradient
|
|
13
|
-
// cells. Defaults to var(--ck-bg-surface).
|
|
14
|
-
background?: string;
|
|
15
|
-
// Override text color (for high-contrast gradient cells).
|
|
16
|
-
color?: string;
|
|
17
|
-
// Padding inside the cell. Defaults to 20px.
|
|
18
|
-
padding?: number | string;
|
|
19
|
-
// Apply the hover-lift effect. Defaults to false.
|
|
20
|
-
interactive?: boolean;
|
|
21
|
-
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
22
|
-
className?: string;
|
|
23
|
-
style?: CSSProperties;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function BentoCell({
|
|
27
|
-
children,
|
|
28
|
-
colSpan = 1,
|
|
29
|
-
rowSpan = 1,
|
|
30
|
-
background,
|
|
31
|
-
color,
|
|
32
|
-
padding = 20,
|
|
33
|
-
interactive = false,
|
|
34
|
-
onClick,
|
|
35
|
-
className,
|
|
36
|
-
style,
|
|
37
|
-
}: BentoCellProps) {
|
|
38
|
-
return (
|
|
39
|
-
<div
|
|
40
|
-
data-ck-bento-cell=""
|
|
41
|
-
data-interactive={interactive ? "true" : undefined}
|
|
42
|
-
onClick={onClick}
|
|
43
|
-
className={className}
|
|
44
|
-
role={onClick ? "button" : undefined}
|
|
45
|
-
tabIndex={onClick ? 0 : undefined}
|
|
46
|
-
style={{
|
|
47
|
-
gridColumn: `span ${colSpan} / span ${colSpan}`,
|
|
48
|
-
gridRow: `span ${rowSpan} / span ${rowSpan}`,
|
|
49
|
-
background: background ?? "var(--ck-bg-surface)",
|
|
50
|
-
color,
|
|
51
|
-
border: "1px solid var(--ck-border-subtle)",
|
|
52
|
-
borderRadius: "var(--ck-radius-lg, 22px)",
|
|
53
|
-
boxShadow: "var(--ck-shadow-2)",
|
|
54
|
-
padding,
|
|
55
|
-
overflow: "hidden",
|
|
56
|
-
position: "relative",
|
|
57
|
-
cursor: onClick ? "pointer" : undefined,
|
|
58
|
-
display: "flex",
|
|
59
|
-
flexDirection: "column",
|
|
60
|
-
...style,
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
{children}
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
package/src/bento/BentoGrid.tsx
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// 12-column CSS grid wrapper. Cells set their own colSpan/rowSpan
|
|
4
|
-
// via <BentoCell>. The grid clamps span values to the column count
|
|
5
|
-
// so a 4-wide cell inside an 8-col grid won't overflow.
|
|
6
|
-
|
|
7
|
-
export interface BentoGridProps {
|
|
8
|
-
children?: ReactNode;
|
|
9
|
-
// Column count. Defaults to 12. Pass 6 / 8 for tighter sections.
|
|
10
|
-
cols?: number;
|
|
11
|
-
// Gap between cells. Defaults to 16px.
|
|
12
|
-
gap?: number;
|
|
13
|
-
// Minimum row height. Cells span discrete rows of this height.
|
|
14
|
-
rowMinHeight?: number;
|
|
15
|
-
className?: string;
|
|
16
|
-
style?: CSSProperties;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function BentoGrid({
|
|
20
|
-
children,
|
|
21
|
-
cols = 12,
|
|
22
|
-
gap = 16,
|
|
23
|
-
rowMinHeight = 120,
|
|
24
|
-
className,
|
|
25
|
-
style,
|
|
26
|
-
}: BentoGridProps) {
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
data-ck-bento-grid=""
|
|
30
|
-
className={className}
|
|
31
|
-
style={{
|
|
32
|
-
display: "grid",
|
|
33
|
-
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
34
|
-
gridAutoRows: `minmax(${rowMinHeight}px, auto)`,
|
|
35
|
-
gap,
|
|
36
|
-
...style,
|
|
37
|
-
}}
|
|
38
|
-
>
|
|
39
|
-
{children}
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
}
|
package/src/bento/index.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useContext,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type CSSProperties,
|
|
8
|
-
} from "react";
|
|
9
|
-
import { createPortal } from "react-dom";
|
|
10
|
-
import { CommandContext } from "./command-context";
|
|
11
|
-
import { rankItems } from "./rank";
|
|
12
|
-
import { Kbd } from "../primitives/Kbd";
|
|
13
|
-
import type { CommandItem } from "./types";
|
|
14
|
-
|
|
15
|
-
// The palette UI. Mounts inside <CommandProvider>; reads items from
|
|
16
|
-
// the registry, filters, groups, navigates. Arrow keys + Enter +
|
|
17
|
-
// Esc bound while open. Cmd+K toggle lives in the provider.
|
|
18
|
-
|
|
19
|
-
export interface CommandPaletteProps {
|
|
20
|
-
// Order in which groups appear. Items in groups not listed
|
|
21
|
-
// here come last in registration order.
|
|
22
|
-
groupOrder?: string[];
|
|
23
|
-
placeholder?: string;
|
|
24
|
-
// Fires whenever the query changes — on every keystroke and on the
|
|
25
|
-
// reset-to-empty when the palette opens. Lets a consumer drive an
|
|
26
|
-
// ASYNC command source (debounce → fetch → register the results as
|
|
27
|
-
// items via useCommandSource) that the built-in client-side filter
|
|
28
|
-
// can't provide on its own. Optional + backward compatible: omit it
|
|
29
|
-
// and the palette behaves exactly as before (static items only).
|
|
30
|
-
onQueryChange?: ((query: string) => void) | undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function CommandPalette({
|
|
34
|
-
groupOrder = [],
|
|
35
|
-
placeholder = "Type a command or search…",
|
|
36
|
-
onQueryChange,
|
|
37
|
-
}: CommandPaletteProps) {
|
|
38
|
-
const ctx = useContext(CommandContext);
|
|
39
|
-
if (!ctx) throw new Error("<CommandPalette> must be inside <CommandProvider>");
|
|
40
|
-
const { open, setOpen, items } = ctx;
|
|
41
|
-
const [q, setQ] = useState("");
|
|
42
|
-
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
43
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
44
|
-
const listRef = useRef<HTMLDivElement>(null);
|
|
45
|
-
// Latest onQueryChange held in a ref so the open-reset effect can
|
|
46
|
-
// notify it WITHOUT taking it as a dep — an inline callback changes
|
|
47
|
-
// identity every render, which would otherwise re-run the effect and
|
|
48
|
-
// clear the query on every keystroke.
|
|
49
|
-
const onQueryChangeRef = useRef(onQueryChange);
|
|
50
|
-
onQueryChangeRef.current = onQueryChange;
|
|
51
|
-
|
|
52
|
-
// Reset query + focus on every open.
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (open) {
|
|
55
|
-
setQ("");
|
|
56
|
-
setSelectedIdx(0);
|
|
57
|
-
// Tell the consumer the query cleared, so an async source drops
|
|
58
|
-
// its stale results when the palette reopens.
|
|
59
|
-
onQueryChangeRef.current?.("");
|
|
60
|
-
// RAF so the modal element exists before focus.
|
|
61
|
-
requestAnimationFrame(() => inputRef.current?.focus());
|
|
62
|
-
}
|
|
63
|
-
}, [open]);
|
|
64
|
-
|
|
65
|
-
// Filter + flat-group ordering.
|
|
66
|
-
const filtered = useMemo(() => rankItems(items, q), [items, q]);
|
|
67
|
-
const grouped = useMemo(() => {
|
|
68
|
-
const byGroup: Record<string, CommandItem[]> = {};
|
|
69
|
-
for (const it of filtered) {
|
|
70
|
-
(byGroup[it.group] ??= []).push(it);
|
|
71
|
-
}
|
|
72
|
-
const seen = new Set<string>();
|
|
73
|
-
const ordered: { group: string; items: CommandItem[] }[] = [];
|
|
74
|
-
for (const g of groupOrder) {
|
|
75
|
-
if (byGroup[g]) {
|
|
76
|
-
ordered.push({ group: g, items: byGroup[g] });
|
|
77
|
-
seen.add(g);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
for (const g of Object.keys(byGroup)) {
|
|
81
|
-
if (seen.has(g)) continue;
|
|
82
|
-
ordered.push({ group: g, items: byGroup[g]! });
|
|
83
|
-
}
|
|
84
|
-
return ordered;
|
|
85
|
-
}, [filtered, groupOrder]);
|
|
86
|
-
|
|
87
|
-
// Flat array for keyboard navigation (sequential across groups).
|
|
88
|
-
const flat = useMemo(() => grouped.flatMap((g) => g.items), [grouped]);
|
|
89
|
-
|
|
90
|
-
// Effective selection: `selectedIdx` is the raw state the keyboard/mouse
|
|
91
|
-
// set, but disabled rows and a shrunk list can leave it pointing at a
|
|
92
|
-
// non-interactive or out-of-range item. Derive the index actually shown
|
|
93
|
-
// as selected — the first enabled row when the raw one isn't usable — so
|
|
94
|
-
// a disabled item (e.g. a "coming soon" teaser) is never highlighted and
|
|
95
|
-
// the default lands on the first real result.
|
|
96
|
-
const selIdx = useMemo(() => {
|
|
97
|
-
if (flat.length === 0) return 0;
|
|
98
|
-
if (selectedIdx < flat.length && !flat[selectedIdx]?.disabled) return selectedIdx;
|
|
99
|
-
return firstEnabled(flat);
|
|
100
|
-
}, [flat, selectedIdx]);
|
|
101
|
-
|
|
102
|
-
const close = () => setOpen(false);
|
|
103
|
-
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
if (!open) return;
|
|
106
|
-
const onKey = (e: KeyboardEvent) => {
|
|
107
|
-
if (e.key === "Escape") {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
close();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (e.key === "ArrowDown") {
|
|
113
|
-
e.preventDefault();
|
|
114
|
-
setSelectedIdx(stepEnabled(flat, selIdx, 1));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (e.key === "ArrowUp") {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
setSelectedIdx(stepEnabled(flat, selIdx, -1));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (e.key === "Enter") {
|
|
123
|
-
e.preventDefault();
|
|
124
|
-
const pick = flat[selIdx];
|
|
125
|
-
if (pick && !pick.disabled) pick.run({ close });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
window.addEventListener("keydown", onKey);
|
|
130
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
131
|
-
// close + flat + selIdx are stable enough; intentionally omit close
|
|
132
|
-
// from deps so we don't re-bind every render.
|
|
133
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
-
}, [open, flat, selIdx]);
|
|
135
|
-
|
|
136
|
-
// Scroll selected row into view.
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
if (!open) return;
|
|
139
|
-
const node = listRef.current?.querySelector(`[data-row-idx="${selIdx}"]`);
|
|
140
|
-
(node as HTMLElement | null)?.scrollIntoView({ block: "nearest" });
|
|
141
|
-
}, [open, selIdx]);
|
|
142
|
-
|
|
143
|
-
if (!open) return null;
|
|
144
|
-
|
|
145
|
-
return createPortal(
|
|
146
|
-
<div
|
|
147
|
-
role="dialog"
|
|
148
|
-
aria-modal="true"
|
|
149
|
-
aria-label="Command palette"
|
|
150
|
-
style={{
|
|
151
|
-
position: "fixed",
|
|
152
|
-
inset: 0,
|
|
153
|
-
background: "rgba(10, 14, 26, 0.5)",
|
|
154
|
-
backdropFilter: "blur(2px)",
|
|
155
|
-
display: "flex",
|
|
156
|
-
alignItems: "flex-start",
|
|
157
|
-
justifyContent: "center",
|
|
158
|
-
padding: "10vh 16px 16px",
|
|
159
|
-
zIndex: 110,
|
|
160
|
-
}}
|
|
161
|
-
onMouseDown={(e) => {
|
|
162
|
-
if (e.target === e.currentTarget) close();
|
|
163
|
-
}}
|
|
164
|
-
className="ck-anim-fade"
|
|
165
|
-
>
|
|
166
|
-
<div
|
|
167
|
-
className="ck-anim-popover"
|
|
168
|
-
style={
|
|
169
|
-
{
|
|
170
|
-
width: "100%",
|
|
171
|
-
maxWidth: 560,
|
|
172
|
-
background: "var(--ck-bg-surface)",
|
|
173
|
-
border: "1px solid var(--ck-border-subtle)",
|
|
174
|
-
borderRadius: "var(--ck-radius-md)",
|
|
175
|
-
boxShadow: "var(--ck-shadow-3)",
|
|
176
|
-
overflow: "hidden",
|
|
177
|
-
display: "flex",
|
|
178
|
-
flexDirection: "column",
|
|
179
|
-
maxHeight: "70vh",
|
|
180
|
-
fontFamily: "var(--ck-font-sans)",
|
|
181
|
-
color: "var(--ck-text-primary)",
|
|
182
|
-
} as CSSProperties
|
|
183
|
-
}
|
|
184
|
-
>
|
|
185
|
-
<input
|
|
186
|
-
ref={inputRef}
|
|
187
|
-
value={q}
|
|
188
|
-
onChange={(e) => {
|
|
189
|
-
setQ(e.target.value);
|
|
190
|
-
setSelectedIdx(0);
|
|
191
|
-
onQueryChange?.(e.target.value);
|
|
192
|
-
}}
|
|
193
|
-
placeholder={placeholder}
|
|
194
|
-
style={{
|
|
195
|
-
height: 44,
|
|
196
|
-
padding: "0 16px",
|
|
197
|
-
border: "none",
|
|
198
|
-
borderBottom: "1px solid var(--ck-border-subtle)",
|
|
199
|
-
background: "transparent",
|
|
200
|
-
color: "var(--ck-text-primary)",
|
|
201
|
-
font: "400 14px/1 var(--ck-font-sans)",
|
|
202
|
-
outline: "none",
|
|
203
|
-
}}
|
|
204
|
-
/>
|
|
205
|
-
<div ref={listRef} style={{ flex: 1, minHeight: 0, overflowY: "auto", padding: 4 }}>
|
|
206
|
-
{flat.length === 0 ? (
|
|
207
|
-
<div
|
|
208
|
-
style={{
|
|
209
|
-
padding: "24px 16px",
|
|
210
|
-
color: "var(--ck-text-tertiary)",
|
|
211
|
-
fontSize: 13,
|
|
212
|
-
textAlign: "center",
|
|
213
|
-
}}
|
|
214
|
-
>
|
|
215
|
-
No matches.
|
|
216
|
-
</div>
|
|
217
|
-
) : (
|
|
218
|
-
grouped.map((g) => (
|
|
219
|
-
<div key={g.group} style={{ marginBottom: 6 }}>
|
|
220
|
-
<div
|
|
221
|
-
className="ck-eyebrow"
|
|
222
|
-
style={{ padding: "8px 12px 4px", color: "var(--ck-text-tertiary)" }}
|
|
223
|
-
>
|
|
224
|
-
{g.group}
|
|
225
|
-
</div>
|
|
226
|
-
{g.items.map((it) => {
|
|
227
|
-
const idx = flat.indexOf(it);
|
|
228
|
-
const disabled = !!it.disabled;
|
|
229
|
-
const selected = idx === selIdx && !disabled;
|
|
230
|
-
return (
|
|
231
|
-
<div
|
|
232
|
-
key={it.key}
|
|
233
|
-
data-row-idx={idx}
|
|
234
|
-
aria-disabled={disabled || undefined}
|
|
235
|
-
onMouseEnter={disabled ? undefined : () => setSelectedIdx(idx)}
|
|
236
|
-
onClick={disabled ? undefined : () => it.run({ close })}
|
|
237
|
-
style={{
|
|
238
|
-
display: "flex",
|
|
239
|
-
alignItems: "center",
|
|
240
|
-
gap: 10,
|
|
241
|
-
padding: "8px 12px",
|
|
242
|
-
borderRadius: "var(--ck-radius-sm)",
|
|
243
|
-
background: selected ? "var(--ck-accent-muted)" : "transparent",
|
|
244
|
-
color: disabled
|
|
245
|
-
? "var(--ck-text-tertiary)"
|
|
246
|
-
: selected
|
|
247
|
-
? "var(--ck-accent)"
|
|
248
|
-
: "var(--ck-text-primary)",
|
|
249
|
-
opacity: disabled ? 0.6 : 1,
|
|
250
|
-
cursor: disabled ? "default" : "pointer",
|
|
251
|
-
font: "400 13px/1.3 var(--ck-font-sans)",
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
254
|
-
{it.icon && (
|
|
255
|
-
<span style={{ display: "inline-flex", color: "var(--ck-text-tertiary)" }}>
|
|
256
|
-
{it.icon}
|
|
257
|
-
</span>
|
|
258
|
-
)}
|
|
259
|
-
<span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
260
|
-
{it.label}
|
|
261
|
-
{it.sublabel && (
|
|
262
|
-
<span style={{ color: "var(--ck-text-tertiary)", marginLeft: 6, fontSize: 12 }}>
|
|
263
|
-
{it.sublabel}
|
|
264
|
-
</span>
|
|
265
|
-
)}
|
|
266
|
-
</span>
|
|
267
|
-
{it.hint && (
|
|
268
|
-
<span style={{ color: "var(--ck-text-tertiary)", fontSize: 12 }}>
|
|
269
|
-
{it.hint}
|
|
270
|
-
</span>
|
|
271
|
-
)}
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
})}
|
|
275
|
-
</div>
|
|
276
|
-
))
|
|
277
|
-
)}
|
|
278
|
-
</div>
|
|
279
|
-
<div
|
|
280
|
-
style={{
|
|
281
|
-
padding: "8px 12px",
|
|
282
|
-
borderTop: "1px solid var(--ck-border-subtle)",
|
|
283
|
-
display: "flex",
|
|
284
|
-
alignItems: "center",
|
|
285
|
-
gap: 12,
|
|
286
|
-
color: "var(--ck-text-tertiary)",
|
|
287
|
-
font: "400 11px/1 var(--ck-font-mono)",
|
|
288
|
-
background: "var(--ck-bg-surface-2)",
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
292
|
-
<Kbd>↑</Kbd>
|
|
293
|
-
<Kbd>↓</Kbd>
|
|
294
|
-
navigate
|
|
295
|
-
</span>
|
|
296
|
-
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
297
|
-
<Kbd>Enter</Kbd>
|
|
298
|
-
run
|
|
299
|
-
</span>
|
|
300
|
-
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
301
|
-
<Kbd>Esc</Kbd>
|
|
302
|
-
close
|
|
303
|
-
</span>
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
</div>,
|
|
307
|
-
document.body,
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// First index whose item isn't disabled, or 0 if every item is (or the
|
|
312
|
-
// list is empty) — the palette has to point somewhere.
|
|
313
|
-
function firstEnabled(list: CommandItem[]): number {
|
|
314
|
-
const i = list.findIndex((it) => !it.disabled);
|
|
315
|
-
return i === -1 ? 0 : i;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Step from `from` in `dir` (+1 down / -1 up), wrapping, skipping disabled
|
|
319
|
-
// rows. Returns `from` when there's no other enabled row to land on.
|
|
320
|
-
function stepEnabled(list: CommandItem[], from: number, dir: 1 | -1): number {
|
|
321
|
-
const n = list.length;
|
|
322
|
-
if (n === 0) return 0;
|
|
323
|
-
let i = from;
|
|
324
|
-
for (let step = 0; step < n; step++) {
|
|
325
|
-
i = (i + dir + n) % n;
|
|
326
|
-
if (!list[i]?.disabled) return i;
|
|
327
|
-
}
|
|
328
|
-
return from;
|
|
329
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState, type ReactNode } from "react";
|
|
2
|
-
import { CommandContext } from "./command-context";
|
|
3
|
-
import { useKeyboardHotkey } from "../hooks/useKeyboardHotkey";
|
|
4
|
-
import type { CommandItem } from "./types";
|
|
5
|
-
|
|
6
|
-
// Hosts the command-source registry + the palette's open state.
|
|
7
|
-
// Binds Cmd+K globally so any consumer page can trigger the palette
|
|
8
|
-
// without wiring its own listener.
|
|
9
|
-
|
|
10
|
-
export function CommandProvider({
|
|
11
|
-
children,
|
|
12
|
-
hotkey = "k",
|
|
13
|
-
}: {
|
|
14
|
-
children: ReactNode;
|
|
15
|
-
// Letter for the Mod+_ shortcut. Default "k".
|
|
16
|
-
hotkey?: string;
|
|
17
|
-
}) {
|
|
18
|
-
const [sources, setSources] = useState<Record<string, CommandItem[]>>({});
|
|
19
|
-
const [open, setOpen] = useState(false);
|
|
20
|
-
|
|
21
|
-
const register = useCallback((sourceKey: string, items: CommandItem[]) => {
|
|
22
|
-
setSources((prev) => {
|
|
23
|
-
const cur = prev[sourceKey];
|
|
24
|
-
if (cur && cur.length === items.length && cur.every((it, i) => it === items[i])) {
|
|
25
|
-
return prev;
|
|
26
|
-
}
|
|
27
|
-
return { ...prev, [sourceKey]: items };
|
|
28
|
-
});
|
|
29
|
-
}, []);
|
|
30
|
-
|
|
31
|
-
const unregister = useCallback((sourceKey: string) => {
|
|
32
|
-
setSources((prev) => {
|
|
33
|
-
if (!(sourceKey in prev)) return prev;
|
|
34
|
-
const next = { ...prev };
|
|
35
|
-
delete next[sourceKey];
|
|
36
|
-
return next;
|
|
37
|
-
});
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const items = useMemo(() => Object.values(sources).flat(), [sources]);
|
|
41
|
-
|
|
42
|
-
useKeyboardHotkey(
|
|
43
|
-
hotkey,
|
|
44
|
-
(e) => {
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
setOpen((v) => !v);
|
|
47
|
-
},
|
|
48
|
-
{ mod: true, skipWhenModalOpen: false },
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const value = useMemo(
|
|
52
|
-
() => ({ register, unregister, items, open, setOpen }),
|
|
53
|
-
[register, unregister, items, open],
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
return <CommandContext.Provider value={value}>{children}</CommandContext.Provider>;
|
|
57
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
2
|
-
import type { CommandItem } from "./types";
|
|
3
|
-
|
|
4
|
-
export interface CommandContextValue {
|
|
5
|
-
register: (sourceKey: string, items: CommandItem[]) => void;
|
|
6
|
-
unregister: (sourceKey: string) => void;
|
|
7
|
-
items: CommandItem[];
|
|
8
|
-
open: boolean;
|
|
9
|
-
setOpen: (next: boolean) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const CommandContext = createContext<CommandContextValue | null>(null);
|
package/src/command/index.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { CommandProvider } from "./CommandProvider";
|
|
2
|
-
export { CommandPalette } from "./CommandPalette";
|
|
3
|
-
export type { CommandPaletteProps } from "./CommandPalette";
|
|
4
|
-
export { useCommandSource, useCommandPalette } from "./useCommandSource";
|
|
5
|
-
export { rankItems } from "./rank";
|
|
6
|
-
export type { CommandItem } from "./types";
|
package/src/command/rank.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { CommandItem } from "./types";
|
|
2
|
-
|
|
3
|
-
// Tiny fuzzy ranker. Three signals:
|
|
4
|
-
// - label substring match → score 100
|
|
5
|
-
// - label prefix match → score +20
|
|
6
|
-
// - keyword substring match → score 60 (lower than label)
|
|
7
|
-
// Anything that doesn't match returns null.
|
|
8
|
-
//
|
|
9
|
-
// Not as fancy as fzf — good enough for command palettes with
|
|
10
|
-
// dozens to low hundreds of items. Swap in a fancier library if
|
|
11
|
-
// you outgrow it; the call site (`useFilteredCommands`) is small.
|
|
12
|
-
|
|
13
|
-
interface Ranked {
|
|
14
|
-
item: CommandItem;
|
|
15
|
-
score: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function scoreOne(item: CommandItem, q: string): number | null {
|
|
19
|
-
// Secret items exist only for their exact phrase — never in empty
|
|
20
|
-
// listings, never via partial/label matches.
|
|
21
|
-
if (item.secret) {
|
|
22
|
-
return q.trim().toLowerCase() === item.secret.toLowerCase() ? 200 : null;
|
|
23
|
-
}
|
|
24
|
-
if (!q) return 1; // empty query — everyone passes, sorted by registration order
|
|
25
|
-
const ql = q.toLowerCase();
|
|
26
|
-
const lab = item.label.toLowerCase();
|
|
27
|
-
let s = 0;
|
|
28
|
-
if (lab.includes(ql)) {
|
|
29
|
-
s = 100;
|
|
30
|
-
if (lab.startsWith(ql)) s += 20;
|
|
31
|
-
} else if (item.keywords?.some((k) => k.toLowerCase().includes(ql))) {
|
|
32
|
-
s = 60;
|
|
33
|
-
} else if (item.sublabel?.toLowerCase().includes(ql)) {
|
|
34
|
-
s = 40;
|
|
35
|
-
} else {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
return s;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function rankItems(items: CommandItem[], query: string): CommandItem[] {
|
|
42
|
-
const ranked: Ranked[] = [];
|
|
43
|
-
for (const item of items) {
|
|
44
|
-
const sc = scoreOne(item, query);
|
|
45
|
-
if (sc !== null) ranked.push({ item, score: sc });
|
|
46
|
-
}
|
|
47
|
-
// Stable sort by score descending.
|
|
48
|
-
ranked.sort((a, b) => b.score - a.score);
|
|
49
|
-
return ranked.map((r) => r.item);
|
|
50
|
-
}
|
package/src/command/types.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// Single executable command. Sources register sets of these; the
|
|
4
|
-
// palette flattens, filters by the user's query, groups by `group`,
|
|
5
|
-
// and runs `run()` when the user picks.
|
|
6
|
-
|
|
7
|
-
export interface CommandItem {
|
|
8
|
-
key: string;
|
|
9
|
-
// Visual category — items sharing a group render under a single
|
|
10
|
-
// labelled header in the palette. Use anything; the kit pins
|
|
11
|
-
// ordering via the palette's `groupOrder` prop.
|
|
12
|
-
group: string;
|
|
13
|
-
label: string;
|
|
14
|
-
// Dimmed secondary text — usually a path or short context.
|
|
15
|
-
sublabel?: string;
|
|
16
|
-
// Right-aligned hint (kbd shortcut, "→", etc.).
|
|
17
|
-
hint?: ReactNode;
|
|
18
|
-
icon?: ReactNode;
|
|
19
|
-
// Extra strings to score against (filenames, aliases). Lower
|
|
20
|
-
// weight than label so they don't drown the primary match.
|
|
21
|
-
keywords?: string[];
|
|
22
|
-
// Called when the user picks this command. `close()` shuts the
|
|
23
|
-
// palette — useful when the command opens a sub-flow that wants
|
|
24
|
-
// to keep the palette open.
|
|
25
|
-
run: (api: { close: () => void }) => void;
|
|
26
|
-
// Cheat-code gating: when set, the item is INVISIBLE to every
|
|
27
|
-
// query except an exact (case-insensitive, trimmed) match of this
|
|
28
|
-
// phrase — it never appears in empty-query listings or partial
|
|
29
|
-
// matches. Use for hidden debug/test menus; pair with a "Debug"
|
|
30
|
-
// group so the reveal is self-describing.
|
|
31
|
-
secret?: string;
|
|
32
|
-
// Non-interactive placeholder: the row still renders (dimmed) but is
|
|
33
|
-
// skipped by keyboard nav, never auto-selected, and `run` never fires
|
|
34
|
-
// on it. Use for "coming soon" teasers. Distinct from `secret`, which
|
|
35
|
-
// HIDES the row; `disabled` SHOWS it as unavailable.
|
|
36
|
-
disabled?: boolean | undefined;
|
|
37
|
-
}
|