@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
package/src/actionbar/types.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// Single item in the action bar. Pages register items via
|
|
4
|
-
// useActionBarItems() — the bar itself reads from the registry
|
|
5
|
-
// and renders them grouped by category. Categories with 2+ items
|
|
6
|
-
// fold into one inline-expandable group head.
|
|
7
|
-
|
|
8
|
-
export type ActionBarItemVariant = "ghost" | "primary" | "soft";
|
|
9
|
-
|
|
10
|
-
// Slot anchor — which edge of the bar the item gravitates toward.
|
|
11
|
-
// `leading` (default) keeps existing behavior: items pack from the
|
|
12
|
-
// drag grip outward in registration order. `trailing` pins the item
|
|
13
|
-
// to the right edge with a flex spacer separating it from the
|
|
14
|
-
// leading group, regardless of registration order. Use trailing for
|
|
15
|
-
// system status indicators (sync, connection, identity) that need a
|
|
16
|
-
// stable visual home and shouldn't be shuffled by page items
|
|
17
|
-
// registering after them.
|
|
18
|
-
export type ActionBarItemSlot = "leading" | "trailing";
|
|
19
|
-
|
|
20
|
-
export interface ActionBarItem {
|
|
21
|
-
// React key for the rendered button. Stable across rerenders.
|
|
22
|
-
key: string;
|
|
23
|
-
label: string;
|
|
24
|
-
icon?: ReactNode;
|
|
25
|
-
onClick: () => void;
|
|
26
|
-
// State indicator — active items render with the accent tone.
|
|
27
|
-
active?: boolean;
|
|
28
|
-
// Tooltip / aria-label. Falls back to label.
|
|
29
|
-
title?: string;
|
|
30
|
-
// Disable without removing.
|
|
31
|
-
disabled?: boolean;
|
|
32
|
-
// Group key — items sharing a category collapse into one
|
|
33
|
-
// expandable group head when 2+ are present. Single items
|
|
34
|
-
// render inline. Categories are declared at the provider level.
|
|
35
|
-
category?: string;
|
|
36
|
-
// Visual mode. Default ghost.
|
|
37
|
-
variant?: ActionBarItemVariant;
|
|
38
|
-
// Optional kbd hint shown beside the label (just visual — the
|
|
39
|
-
// shortcut is wired by the caller via useKeyboardHotkey).
|
|
40
|
-
hint?: string;
|
|
41
|
-
// When inside an expanded group, clicking this item normally
|
|
42
|
-
// collapses the group afterward. Set true to keep it open
|
|
43
|
-
// (e.g. for items whose post-click state changes the label
|
|
44
|
-
// and should stay visible).
|
|
45
|
-
keepGroupOpenOnClick?: boolean;
|
|
46
|
-
// Slot anchor. Defaults to `leading`. Trailing items render after
|
|
47
|
-
// a flex spacer so they pin to the right edge regardless of
|
|
48
|
-
// registration order — system status indicators belong here.
|
|
49
|
-
slot?: ActionBarItemSlot;
|
|
50
|
-
// Marks this item as admin-only. Hidden until the ActionBar's
|
|
51
|
-
// admin-mode toggle is switched on. The toggle button auto-appears
|
|
52
|
-
// on the bar whenever at least one registered item has adminOnly=
|
|
53
|
-
// true; when no consumer registers such items, the toggle stays
|
|
54
|
-
// hidden and the bar looks exactly as it always did.
|
|
55
|
-
//
|
|
56
|
-
// Use for elevated-privilege actions that would confuse regular
|
|
57
|
-
// users (version history, retry render, delete forever, etc.).
|
|
58
|
-
// Consumer decides eligibility upstream — typically by only
|
|
59
|
-
// registering the item when the caller has `capabilities.manage`
|
|
60
|
-
// (or an equivalent gate) === true. The kit does not check
|
|
61
|
-
// permissions; it only handles the toggle UX.
|
|
62
|
-
adminOnly?: boolean;
|
|
63
|
-
// Marks a normally-visible item as hidden when admin mode is on.
|
|
64
|
-
// Symmetric counterpart to `adminOnly`: pages that want an
|
|
65
|
-
// exclusive "different toolset" UX (e.g. Share disappears in favour
|
|
66
|
-
// of Manage Share + Activity + History) mark their regular items
|
|
67
|
-
// hiddenInAdmin=true so they clear when the toggle flips on.
|
|
68
|
-
// Pages that want additive layering (regular items stay + admin
|
|
69
|
-
// items appear alongside) just leave this unset.
|
|
70
|
-
//
|
|
71
|
-
// Per-item because different pages within the same app want
|
|
72
|
-
// different behaviour: block_doc viewer wants exclusive; a
|
|
73
|
-
// hypothetical PDF admin surface might want additive.
|
|
74
|
-
hiddenInAdmin?: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Category definition — declared at the provider level. Drives
|
|
78
|
-
// the group head's label + icon.
|
|
79
|
-
export interface ActionBarCategory {
|
|
80
|
-
label: string;
|
|
81
|
-
icon?: ReactNode;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type ActionBarCategories = Record<string, ActionBarCategory>;
|
|
85
|
-
|
|
86
|
-
export interface ActionBarSource {
|
|
87
|
-
sourceKey: string;
|
|
88
|
-
items: ActionBarItem[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Status dot — bar-intrinsic chrome rendered at the right edge,
|
|
92
|
-
// mirroring the left-edge drag grip. Distinct from `ActionBarItem`
|
|
93
|
-
// (which is page-level content registered through the items
|
|
94
|
-
// registry): a status dot is a fixed system affordance —
|
|
95
|
-
// connection / sync / identity health. The grip on the left says
|
|
96
|
-
// "you can move this thing"; the status dot on the right says
|
|
97
|
-
// "here's how the system is doing."
|
|
98
|
-
//
|
|
99
|
-
// Visual: a small coloured dot, optionally pulsing. Hover shows the
|
|
100
|
-
// title as a native tooltip. Click fires `onClick` — the bar owns
|
|
101
|
-
// no popover behaviour; consumers render their own popover anchored
|
|
102
|
-
// to body / wherever fits their UX.
|
|
103
|
-
//
|
|
104
|
-
// Registered via `useActionBarStatusDot()`. One source of truth at
|
|
105
|
-
// any time (last call wins). Pass `null` to clear.
|
|
106
|
-
export interface ActionBarStatusDot {
|
|
107
|
-
// Colour of the dot. Any CSS colour string — typically a token
|
|
108
|
-
// like `var(--ck-status-success)`. The bar applies no defaulting,
|
|
109
|
-
// so omit the entry (pass null to the hook) to render no dot.
|
|
110
|
-
color: string;
|
|
111
|
-
// Hover tooltip + aria-label. Falls back to "Status".
|
|
112
|
-
title?: string;
|
|
113
|
-
// Click handler. When omitted the dot renders as a non-interactive
|
|
114
|
-
// span (no focus ring, no cursor change, no role=button).
|
|
115
|
-
onClick?: () => void;
|
|
116
|
-
// When true, the dot pulses (CSS animation). Use sparingly —
|
|
117
|
-
// pulsing reads as attention-grabbing.
|
|
118
|
-
pulse?: boolean;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Registered via `useActionBarPanel()` — the bar-intrinsic popover
|
|
122
|
-
// slot (design#13). The BAR owns positioning (docked above itself,
|
|
123
|
-
// following drag natively) and the chrome themes own the panel's
|
|
124
|
-
// visual identity via `.ck-actionbar-panel`; the consumer owns only
|
|
125
|
-
// the content subtree. One panel at a time (last call wins).
|
|
126
|
-
export interface ActionBarPanel {
|
|
127
|
-
// Controlled visibility. The bar calls onOpenChange(false) on
|
|
128
|
-
// outside-click and Escape; the consumer toggles open from its
|
|
129
|
-
// own affordance (typically the status dot's onClick).
|
|
130
|
-
open: boolean;
|
|
131
|
-
onOpenChange: (open: boolean) => void;
|
|
132
|
-
// Content subtree. Portalled into the themed panel container the
|
|
133
|
-
// bar renders — it stays in the CONSUMER's React tree (fresh JSX
|
|
134
|
-
// identity per render is fine; content never flows through
|
|
135
|
-
// context state, which would re-render-loop).
|
|
136
|
-
content: ReactNode;
|
|
137
|
-
// Panel width in px. Default 320; always viewport-clamped.
|
|
138
|
-
width?: number;
|
|
139
|
-
// aria-label for the panel region. Default "Panel".
|
|
140
|
-
ariaLabel?: string;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// What the context actually stores for the panel slot: the config
|
|
144
|
-
// minus `content`. Only stable primitives + the onOpenChange
|
|
145
|
-
// callback go through context state, so the registration effect
|
|
146
|
-
// re-runs on real changes only.
|
|
147
|
-
export type ActionBarPanelMeta = Omit<ActionBarPanel, "content">;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from "react";
|
|
2
|
-
import { ActionBarContext } from "./actionbar-context";
|
|
3
|
-
import type { ActionBarItem } from "./types";
|
|
4
|
-
|
|
5
|
-
// Register a set of items into the global action-bar registry.
|
|
6
|
-
// Auto-unregisters on unmount + on sourceKey change.
|
|
7
|
-
//
|
|
8
|
-
// **Pass a `useMemo`'d array.** The effect re-registers whenever
|
|
9
|
-
// the array identity changes; the provider's `register()` then
|
|
10
|
-
// dedups by item-by-item identity, so a fresh array with identical
|
|
11
|
-
// item refs is cheap (state stays === and no re-render fires) but
|
|
12
|
-
// a fresh array with NEW item objects (e.g. consumers whose
|
|
13
|
-
// onClick closures captured updated state) does re-register —
|
|
14
|
-
// downstream renders pick up the fresh handlers.
|
|
15
|
-
//
|
|
16
|
-
// Inline-array consumers (`useActionBarItems(key, [{...}])`)
|
|
17
|
-
// will allocate new items every render and loop infinitely
|
|
18
|
-
// through `register → setState → re-render → register`. The
|
|
19
|
-
// useMemo on the caller side is what keeps that loop bounded
|
|
20
|
-
// to "items whose deps actually changed".
|
|
21
|
-
//
|
|
22
|
-
// Previous versions wrapped items in a local `useMemo` keyed by
|
|
23
|
-
// `[items.length, items.map(i => i.key).join('|')]`. That gate
|
|
24
|
-
// suppressed identity updates whenever the keys + length stayed
|
|
25
|
-
// stable, which is precisely the case for a selection-mode
|
|
26
|
-
// toolbar whose buttons are fixed but whose onClicks close over
|
|
27
|
-
// changing state — they ended up registered ONCE with the first
|
|
28
|
-
// render's closures and never refreshed, shipping stale state to
|
|
29
|
-
// every subsequent click. The gate is gone; the provider's
|
|
30
|
-
// shallow item-identity dedup is the single source of truth.
|
|
31
|
-
//
|
|
32
|
-
// **Important**: the useEffect deps below intentionally do NOT
|
|
33
|
-
// include the ctx object itself — only its stable function refs
|
|
34
|
-
// (register/unregister are useCallback'd in the provider).
|
|
35
|
-
// Including ctx would cause an infinite render loop: register
|
|
36
|
-
// mutates provider state → provider's value memo recomputes →
|
|
37
|
-
// ctx ref changes → effect re-runs → cleanup unregisters →
|
|
38
|
-
// state mutates again → ctx changes → loop. Manifests as the
|
|
39
|
-
// main thread saturating and event clicks never reaching the
|
|
40
|
-
// router. Same fix applies in useCommandSource.
|
|
41
|
-
|
|
42
|
-
export function useActionBarItems(sourceKey: string, items: ActionBarItem[]) {
|
|
43
|
-
const ctx = useContext(ActionBarContext);
|
|
44
|
-
if (!ctx) {
|
|
45
|
-
throw new Error("useActionBarItems must be used within <ActionBarProvider>");
|
|
46
|
-
}
|
|
47
|
-
const { register, unregister } = ctx;
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
register(sourceKey, items);
|
|
50
|
-
return () => unregister(sourceKey);
|
|
51
|
-
}, [register, unregister, sourceKey, items]);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function useActionBarItemsContext() {
|
|
55
|
-
const ctx = useContext(ActionBarContext);
|
|
56
|
-
if (!ctx) {
|
|
57
|
-
throw new Error("Action bar context not found");
|
|
58
|
-
}
|
|
59
|
-
return ctx;
|
|
60
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { useActionBarItems } from "./useActionBarItems";
|
|
3
|
-
import type { ActionBarItem } from "./types";
|
|
4
|
-
|
|
5
|
-
// useActionBarMode — one hook that manages a small set of named
|
|
6
|
-
// "modes" (each with its own item list) + registers the active mode's
|
|
7
|
-
// items into the ActionBar registry. Consumer combines this with
|
|
8
|
-
// <ActionBarModeHandle> to give privileged users a peek-out affordance
|
|
9
|
-
// that flips between the modes.
|
|
10
|
-
//
|
|
11
|
-
// Design intent (M4.5 block_doc surface):
|
|
12
|
-
// - Default mode: standard viewer actions (comment / download / share)
|
|
13
|
-
// - Manage mode: admin-only shortcuts (preview draft / manage share /
|
|
14
|
-
// view activity), gated behind `capabilities.manage === true`
|
|
15
|
-
// - Draft mode (optional third): appears once the user enters draft
|
|
16
|
-
// preview; distinct action set (publish / discard / raw JSON /
|
|
17
|
-
// return)
|
|
18
|
-
//
|
|
19
|
-
// The mode name is the ActionBar's source key, so switching modes
|
|
20
|
-
// unregisters the previous set and registers the new set atomically
|
|
21
|
-
// (useActionBarItems handles the cleanup).
|
|
22
|
-
//
|
|
23
|
-
// State persistence: pass `storageKey` for localStorage-backed
|
|
24
|
-
// stickiness across reloads. Nil-safe when window is unavailable
|
|
25
|
-
// (SSR).
|
|
26
|
-
|
|
27
|
-
export type ActionBarModeConfig<K extends string> = {
|
|
28
|
-
[key in K]: ActionBarItem[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export interface UseActionBarModeOptions<K extends string> {
|
|
32
|
-
/**
|
|
33
|
-
* Map of mode keys → items. Each mode key becomes the source key
|
|
34
|
-
* passed to useActionBarItems, so it must be a stable string
|
|
35
|
-
* (e.g. "viewer", "manage") — mode registrations will not
|
|
36
|
-
* "cross-contaminate" the item set of another mode.
|
|
37
|
-
*/
|
|
38
|
-
modes: ActionBarModeConfig<K>;
|
|
39
|
-
/**
|
|
40
|
-
* Which mode to start in. Overridden by localStorage when
|
|
41
|
-
* `storageKey` is set AND localStorage has a valid value.
|
|
42
|
-
*/
|
|
43
|
-
defaultMode: K;
|
|
44
|
-
/**
|
|
45
|
-
* Optional localStorage key. When set, the current mode is
|
|
46
|
-
* persisted + rehydrated across reloads.
|
|
47
|
-
*/
|
|
48
|
-
storageKey?: string | undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface UseActionBarModeResult<K extends string> {
|
|
52
|
-
/** The currently active mode key. */
|
|
53
|
-
mode: K;
|
|
54
|
-
/** Set the mode explicitly (typically wired to the mode Handle). */
|
|
55
|
-
setMode: (next: K) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Toggle between the FIRST two modes in the config's iteration
|
|
58
|
-
* order. Convenience for the common two-mode case; a
|
|
59
|
-
* three-mode consumer uses setMode directly.
|
|
60
|
-
*/
|
|
61
|
-
toggle: () => void;
|
|
62
|
-
/**
|
|
63
|
-
* All configured mode keys in registration order — useful for
|
|
64
|
-
* building a cycle-through-modes button.
|
|
65
|
-
*/
|
|
66
|
-
allModes: readonly K[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function loadPersistedMode<K extends string>(
|
|
70
|
-
storageKey: string,
|
|
71
|
-
allModes: readonly K[],
|
|
72
|
-
): K | null {
|
|
73
|
-
if (typeof window === "undefined") return null;
|
|
74
|
-
try {
|
|
75
|
-
const raw = window.localStorage.getItem(storageKey);
|
|
76
|
-
if (raw && (allModes as readonly string[]).includes(raw)) {
|
|
77
|
-
return raw as K;
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
/* ignore */
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function persistMode(storageKey: string, mode: string) {
|
|
86
|
-
if (typeof window === "undefined") return;
|
|
87
|
-
try {
|
|
88
|
-
window.localStorage.setItem(storageKey, mode);
|
|
89
|
-
} catch {
|
|
90
|
-
/* ignore */
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function useActionBarMode<K extends string>(
|
|
95
|
-
opts: UseActionBarModeOptions<K>,
|
|
96
|
-
): UseActionBarModeResult<K> {
|
|
97
|
-
const { modes, defaultMode, storageKey } = opts;
|
|
98
|
-
const allModes = useMemo(() => Object.keys(modes) as K[], [modes]);
|
|
99
|
-
const [mode, setModeState] = useState<K>(() => {
|
|
100
|
-
if (storageKey) {
|
|
101
|
-
const persisted = loadPersistedMode<K>(storageKey, allModes);
|
|
102
|
-
if (persisted) return persisted;
|
|
103
|
-
}
|
|
104
|
-
return defaultMode;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// If the modes config changes (dev / hot reload) so the current
|
|
108
|
-
// mode key is no longer valid, drop back to the default.
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!(allModes as readonly string[]).includes(mode)) {
|
|
111
|
-
setModeState(defaultMode);
|
|
112
|
-
}
|
|
113
|
-
}, [mode, allModes, defaultMode]);
|
|
114
|
-
|
|
115
|
-
const setMode = useCallback(
|
|
116
|
-
(next: K) => {
|
|
117
|
-
setModeState(next);
|
|
118
|
-
if (storageKey) persistMode(storageKey, next);
|
|
119
|
-
},
|
|
120
|
-
[storageKey],
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const toggle = useCallback(() => {
|
|
124
|
-
if (allModes.length < 2) return;
|
|
125
|
-
const idx = allModes.indexOf(mode);
|
|
126
|
-
// Cycle to the NEXT mode; wrap-around handles both the "toggle
|
|
127
|
-
// between 2" and "cycle through 3+" cases.
|
|
128
|
-
const nextIdx = (idx + 1) % allModes.length;
|
|
129
|
-
const next = allModes[nextIdx];
|
|
130
|
-
if (next !== undefined) {
|
|
131
|
-
setMode(next);
|
|
132
|
-
}
|
|
133
|
-
}, [allModes, mode, setMode]);
|
|
134
|
-
|
|
135
|
-
// Register the active mode's items with the ActionBar. Uses the
|
|
136
|
-
// mode key as the source key, so switching modes atomically
|
|
137
|
-
// unregisters the previous set. Items themselves must be stable
|
|
138
|
-
// (useMemo'd) — the ActionBar deduplicates by reference, so passing
|
|
139
|
-
// `modes[mode]` re-uses the caller's memoized array.
|
|
140
|
-
const activeItems = modes[mode];
|
|
141
|
-
useActionBarItems(mode, activeItems);
|
|
142
|
-
|
|
143
|
-
return { mode, setMode, toggle, allModes };
|
|
144
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { ActionBarContext } from "./actionbar-context";
|
|
4
|
-
import type { ActionBarPanel } from "./types";
|
|
5
|
-
import type { ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
// Push a panel config into the action bar's popover slot (design#13).
|
|
8
|
-
//
|
|
9
|
-
// The BAR owns positioning: docked above itself, centered, following
|
|
10
|
-
// drag natively (it renders the panel inside its own fixed-position
|
|
11
|
-
// root — no polling, no rect maths in the consumer) and viewport-
|
|
12
|
-
// clamped at the edges. The chrome themes own the panel's visual
|
|
13
|
-
// identity via `.ck-actionbar-panel`; consumers must NOT paint their
|
|
14
|
-
// own background/border/shadow. The consumer owns only the content
|
|
15
|
-
// subtree and the `open` state.
|
|
16
|
-
//
|
|
17
|
-
// RETURNS a ReactNode the consumer MUST render (a portal into the
|
|
18
|
-
// bar's panel container, or null while closed). Content stays in the
|
|
19
|
-
// consumer's own React tree — a fresh JSX identity every render is
|
|
20
|
-
// fine because only the meta (open/onOpenChange/width/ariaLabel)
|
|
21
|
-
// flows through context state; content in state would re-render-loop
|
|
22
|
-
// (setPanel → context change → consumer re-render → new JSX →
|
|
23
|
-
// setPanel → …).
|
|
24
|
-
//
|
|
25
|
-
// The bar calls `onOpenChange(false)` on outside-click and Escape.
|
|
26
|
-
// Pass `null` to clear (also cleared automatically on unmount).
|
|
27
|
-
// Last call wins — one panel at a time, same contract as the status
|
|
28
|
-
// dot.
|
|
29
|
-
export function useActionBarPanel(config: ActionBarPanel | null): ReactNode {
|
|
30
|
-
const ctx = useContext(ActionBarContext);
|
|
31
|
-
if (!ctx) {
|
|
32
|
-
throw new Error("useActionBarPanel must be used within <ActionBarProvider>");
|
|
33
|
-
}
|
|
34
|
-
const { setPanel, panelHost } = ctx;
|
|
35
|
-
const open = config?.open;
|
|
36
|
-
const onOpenChange = config?.onOpenChange;
|
|
37
|
-
const width = config?.width;
|
|
38
|
-
const ariaLabel = config?.ariaLabel;
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (open === undefined || onOpenChange === undefined) {
|
|
41
|
-
setPanel(null);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
setPanel({
|
|
45
|
-
open,
|
|
46
|
-
onOpenChange,
|
|
47
|
-
...(width !== undefined ? { width } : {}),
|
|
48
|
-
...(ariaLabel !== undefined ? { ariaLabel } : {}),
|
|
49
|
-
});
|
|
50
|
-
return () => {
|
|
51
|
-
setPanel(null);
|
|
52
|
-
};
|
|
53
|
-
}, [setPanel, open, onOpenChange, width, ariaLabel]);
|
|
54
|
-
if (!config?.open || panelHost === null || config.content == null) return null;
|
|
55
|
-
return createPortal(config.content, panelHost);
|
|
56
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from "react";
|
|
2
|
-
import { ActionBarContext } from "./actionbar-context";
|
|
3
|
-
import type { ActionBarStatusDot } from "./types";
|
|
4
|
-
|
|
5
|
-
// Push a status-dot config into the action bar's right-edge slot.
|
|
6
|
-
// The bar renders the dot as bar-intrinsic chrome (mirroring the
|
|
7
|
-
// left-edge drag grip), separate from the items registry.
|
|
8
|
-
//
|
|
9
|
-
// Pass `null` to clear. Last call wins — there's no source-key
|
|
10
|
-
// fan-out as with items, because the status dot is a single
|
|
11
|
-
// system-level affordance (sync / connection / identity) and
|
|
12
|
-
// stacking multiple makes no UX sense.
|
|
13
|
-
//
|
|
14
|
-
// On unmount, the dot is cleared automatically.
|
|
15
|
-
//
|
|
16
|
-
// **Important**: pass a stable config object (typically via
|
|
17
|
-
// `useMemo`), or omit fields you don't change between renders.
|
|
18
|
-
// The effect re-runs when individual fields change — colour
|
|
19
|
-
// transitions or pulse toggles re-render the dot but don't thrash
|
|
20
|
-
// the bar.
|
|
21
|
-
export function useActionBarStatusDot(config: ActionBarStatusDot | null) {
|
|
22
|
-
const ctx = useContext(ActionBarContext);
|
|
23
|
-
if (!ctx) {
|
|
24
|
-
throw new Error("useActionBarStatusDot must be used within <ActionBarProvider>");
|
|
25
|
-
}
|
|
26
|
-
const { setStatusDot } = ctx;
|
|
27
|
-
// Project config to primitive deps so a fresh-but-equivalent
|
|
28
|
-
// config object doesn't trigger thrash. onClick identity changes
|
|
29
|
-
// are tolerated through a manual ref capture in the bar render.
|
|
30
|
-
const color = config?.color;
|
|
31
|
-
const title = config?.title;
|
|
32
|
-
const onClick = config?.onClick;
|
|
33
|
-
const pulse = config?.pulse;
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (color === undefined) {
|
|
36
|
-
setStatusDot(null);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
setStatusDot({
|
|
40
|
-
color,
|
|
41
|
-
...(title !== undefined ? { title } : {}),
|
|
42
|
-
...(onClick !== undefined ? { onClick } : {}),
|
|
43
|
-
...(pulse !== undefined ? { pulse } : {}),
|
|
44
|
-
});
|
|
45
|
-
return () => {
|
|
46
|
-
setStatusDot(null);
|
|
47
|
-
};
|
|
48
|
-
}, [setStatusDot, color, title, onClick, pulse]);
|
|
49
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useContext, useEffect } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { ActionBarContext } from "./actionbar-context";
|
|
4
|
-
import type { ReactNode } from "react";
|
|
5
|
-
|
|
6
|
-
// Push a transient bubble into the bar's toast slot — rendered above
|
|
7
|
-
// the bar (following it when dragged), themed via
|
|
8
|
-
// `.ck-actionbar-toast`, suppressed while the panel is open. The
|
|
9
|
-
// CONSUMER owns the lifetime: pass the node to show, `null` to hide
|
|
10
|
-
// (typically driven by a setTimeout in the consumer).
|
|
11
|
-
//
|
|
12
|
-
// RETURNS a ReactNode the consumer MUST render (a portal into the
|
|
13
|
-
// bar's toast container, or null while hidden). Same portal-host
|
|
14
|
-
// contract as useActionBarPanel: only the boolean "active" flag goes
|
|
15
|
-
// through context state, so a fresh JSX identity per render can't
|
|
16
|
-
// re-render-loop. Pairs with useActionBarPanel for the notification-
|
|
17
|
-
// center pattern (design#13).
|
|
18
|
-
export function useActionBarToast(content: ReactNode | null): ReactNode {
|
|
19
|
-
const ctx = useContext(ActionBarContext);
|
|
20
|
-
if (!ctx) {
|
|
21
|
-
throw new Error("useActionBarToast must be used within <ActionBarProvider>");
|
|
22
|
-
}
|
|
23
|
-
const { setToastActive, toastHost } = ctx;
|
|
24
|
-
const active = content != null;
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
setToastActive(active);
|
|
27
|
-
return () => {
|
|
28
|
-
setToastActive(false);
|
|
29
|
-
};
|
|
30
|
-
}, [setToastActive, active]);
|
|
31
|
-
if (!active || toastHost === null) return null;
|
|
32
|
-
return createPortal(content, toastHost);
|
|
33
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// Standalone ambient-mesh backdrop primitive. The ambient chrome
|
|
4
|
-
// applies its mesh on `body` automatically, so consumers don't
|
|
5
|
-
// usually need this. Use it when you want an ambient look INSIDE
|
|
6
|
-
// a single section (a hero banner, a preview region) without
|
|
7
|
-
// flipping the whole app's chrome.
|
|
8
|
-
|
|
9
|
-
export interface AmbientBackdropProps {
|
|
10
|
-
children?: ReactNode;
|
|
11
|
-
// Use light variant tints instead of dark. Default auto-detects
|
|
12
|
-
// via prefers-color-scheme.
|
|
13
|
-
variant?: "dark" | "light";
|
|
14
|
-
// Hide the noise overlay. Default false.
|
|
15
|
-
noise?: boolean;
|
|
16
|
-
className?: string;
|
|
17
|
-
style?: CSSProperties;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const DARK_BG = `
|
|
21
|
-
radial-gradient(ellipse at 18% 22%, #3A2A66 0%, transparent 55%),
|
|
22
|
-
radial-gradient(ellipse at 82% 70%, #2A1E50 0%, transparent 52%),
|
|
23
|
-
radial-gradient(ellipse at 50% 110%, #4A2870 0%, transparent 50%),
|
|
24
|
-
#1A1525
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
const LIGHT_BG = `
|
|
28
|
-
radial-gradient(ellipse at 20% 24%, #FCEFE5 0%, transparent 55%),
|
|
29
|
-
radial-gradient(ellipse at 78% 70%, #EFEAF8 0%, transparent 52%),
|
|
30
|
-
radial-gradient(ellipse at 50% 110%, #FFE8DC 0%, transparent 50%),
|
|
31
|
-
#F6F4F0
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
export function AmbientBackdrop({
|
|
35
|
-
children,
|
|
36
|
-
variant,
|
|
37
|
-
noise,
|
|
38
|
-
className,
|
|
39
|
-
style,
|
|
40
|
-
}: AmbientBackdropProps) {
|
|
41
|
-
// Auto-pick from prefers-color-scheme when not explicit. SSR-
|
|
42
|
-
// safe: defaults to dark, the canonical Superlist look.
|
|
43
|
-
const isDark =
|
|
44
|
-
variant === "dark" ||
|
|
45
|
-
(variant === undefined &&
|
|
46
|
-
(typeof window === "undefined" ||
|
|
47
|
-
window.matchMedia?.("(prefers-color-scheme: dark)").matches));
|
|
48
|
-
return (
|
|
49
|
-
<div
|
|
50
|
-
className={className}
|
|
51
|
-
style={{
|
|
52
|
-
position: "relative",
|
|
53
|
-
background: isDark ? DARK_BG : LIGHT_BG,
|
|
54
|
-
...style,
|
|
55
|
-
}}
|
|
56
|
-
>
|
|
57
|
-
{noise && (
|
|
58
|
-
<div
|
|
59
|
-
aria-hidden
|
|
60
|
-
style={{
|
|
61
|
-
position: "absolute",
|
|
62
|
-
inset: 0,
|
|
63
|
-
pointerEvents: "none",
|
|
64
|
-
opacity: 0.025,
|
|
65
|
-
backgroundImage:
|
|
66
|
-
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>\")",
|
|
67
|
-
mixBlendMode: "overlay",
|
|
68
|
-
}}
|
|
69
|
-
/>
|
|
70
|
-
)}
|
|
71
|
-
{children}
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { useState, type CSSProperties, type FormEvent } from "react";
|
|
2
|
-
|
|
3
|
-
// The signature ambient component — floating AI / command pill
|
|
4
|
-
// at the bottom of the main content. Heavily blurred glass
|
|
5
|
-
// backdrop, gradient avatar on the left (signals "AI / live"),
|
|
6
|
-
// and a thin Superbar gradient line above it that doubles as a
|
|
7
|
-
// section divider. The most distinctive moment in the design
|
|
8
|
-
// language.
|
|
9
|
-
//
|
|
10
|
-
// Works under any chrome — only really "reads" under
|
|
11
|
-
// ambient because the glass + Superbar accent are the defining
|
|
12
|
-
// gestures. Under other chromes the pill is just a styled
|
|
13
|
-
// input + divider.
|
|
14
|
-
|
|
15
|
-
export interface CommandInputProps {
|
|
16
|
-
placeholder?: string;
|
|
17
|
-
onSubmit?: (value: string) => void;
|
|
18
|
-
// Width cap on the pill itself. Default 520 px (Superlist-ish).
|
|
19
|
-
maxWidth?: number;
|
|
20
|
-
// Hide the Superbar divider above. Default false.
|
|
21
|
-
hideDivider?: boolean;
|
|
22
|
-
className?: string;
|
|
23
|
-
style?: CSSProperties;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function CommandInput({
|
|
27
|
-
placeholder = "Ask anything…",
|
|
28
|
-
onSubmit,
|
|
29
|
-
maxWidth = 520,
|
|
30
|
-
hideDivider = false,
|
|
31
|
-
className,
|
|
32
|
-
style,
|
|
33
|
-
}: CommandInputProps) {
|
|
34
|
-
const [value, setValue] = useState("");
|
|
35
|
-
const submit = (e: FormEvent) => {
|
|
36
|
-
e.preventDefault();
|
|
37
|
-
if (!value.trim()) return;
|
|
38
|
-
onSubmit?.(value.trim());
|
|
39
|
-
setValue("");
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className={className} style={{ width: "100%", ...style }}>
|
|
44
|
-
{!hideDivider && (
|
|
45
|
-
<div
|
|
46
|
-
aria-hidden
|
|
47
|
-
style={{
|
|
48
|
-
height: 1,
|
|
49
|
-
background: "var(--ck-superbar)",
|
|
50
|
-
opacity: 0.7,
|
|
51
|
-
margin: "0 auto 20px",
|
|
52
|
-
maxWidth,
|
|
53
|
-
}}
|
|
54
|
-
/>
|
|
55
|
-
)}
|
|
56
|
-
<form
|
|
57
|
-
onSubmit={submit}
|
|
58
|
-
style={{
|
|
59
|
-
maxWidth,
|
|
60
|
-
margin: "0 auto",
|
|
61
|
-
display: "flex",
|
|
62
|
-
alignItems: "center",
|
|
63
|
-
gap: 10,
|
|
64
|
-
padding: "8px 12px",
|
|
65
|
-
background: "var(--ck-bg-sidebar, rgba(20,20,28,0.55))",
|
|
66
|
-
border: "1px solid var(--ck-border-subtle, rgba(255,255,255,0.08))",
|
|
67
|
-
borderRadius: 999,
|
|
68
|
-
backdropFilter: "var(--ck-blur-overlay, blur(40px) saturate(180%))",
|
|
69
|
-
WebkitBackdropFilter: "var(--ck-blur-overlay, blur(40px) saturate(180%))",
|
|
70
|
-
boxShadow: "var(--ck-shadow-2)",
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
73
|
-
{/* Gradient avatar — coral → violet → blue. Marks the
|
|
74
|
-
pill as "AI / live", regardless of the consumer's
|
|
75
|
-
brand accent. */}
|
|
76
|
-
<span
|
|
77
|
-
aria-hidden
|
|
78
|
-
style={{
|
|
79
|
-
width: 24,
|
|
80
|
-
height: 24,
|
|
81
|
-
borderRadius: "50%",
|
|
82
|
-
background: "var(--ck-superbar)",
|
|
83
|
-
flexShrink: 0,
|
|
84
|
-
boxShadow: "0 0 12px color-mix(in srgb, #B66EFF 40%, transparent)",
|
|
85
|
-
}}
|
|
86
|
-
/>
|
|
87
|
-
<input
|
|
88
|
-
type="text"
|
|
89
|
-
value={value}
|
|
90
|
-
onChange={(e) => setValue(e.target.value)}
|
|
91
|
-
placeholder={placeholder}
|
|
92
|
-
style={{
|
|
93
|
-
flex: 1,
|
|
94
|
-
minWidth: 0,
|
|
95
|
-
height: 32,
|
|
96
|
-
padding: 0,
|
|
97
|
-
border: "none",
|
|
98
|
-
outline: "none",
|
|
99
|
-
background: "transparent",
|
|
100
|
-
color: "var(--ck-text-primary)",
|
|
101
|
-
font: "400 14px/1 var(--ck-font-sans)",
|
|
102
|
-
}}
|
|
103
|
-
/>
|
|
104
|
-
</form>
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
}
|