@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
package/src/layout/SidePanel.tsx
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, type CSSProperties, type ReactNode } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
|
|
4
|
-
// SidePanel — right-docked overlay-less panel for admin surfaces
|
|
5
|
-
// (doc editors, activity feeds, share managers, …). Slides in from
|
|
6
|
-
// the right and pushes the main content left via
|
|
7
|
-
// `--ck-sidepanel-width`, which Shell / topbar / scroll containers
|
|
8
|
-
// can read to inset themselves. No backdrop — the reader keeps
|
|
9
|
-
// interacting with the doc while the panel is open, mirroring
|
|
10
|
-
// Notion / Linear / Slack right-panel UX.
|
|
11
|
-
//
|
|
12
|
-
// Distinct from `<RightSidebarPanel>` (this bucket's other right-
|
|
13
|
-
// side surface):
|
|
14
|
-
// - RightSidebarPanel = scoped floating card (bordered, offset
|
|
15
|
-
// from viewport edges); good for lightweight ephemeral lists.
|
|
16
|
-
// - SidePanel = full-height slide-in that reshapes layout;
|
|
17
|
-
// good for editors + long panels the user will linger in.
|
|
18
|
-
//
|
|
19
|
-
// Companion CSS variable:
|
|
20
|
-
// `--ck-sidepanel-width` — stamped on `:root` while `open` is true.
|
|
21
|
-
// Layout consumers (Shell, main scroll containers, watermarks)
|
|
22
|
-
// should sum this into their right inset:
|
|
23
|
-
// right: calc(var(--ck-rightrail-width, 0px)
|
|
24
|
-
// + var(--ck-sidepanel-width, 0px));
|
|
25
|
-
// The variable is DELIBERATELY separate from `--ck-rightrail-width`
|
|
26
|
-
// so the SidePanel can push content-region chrome without
|
|
27
|
-
// narrowing the topbar (workspace switcher, avatar, notification
|
|
28
|
-
// pills belong to the topbar and shouldn't slide around when a
|
|
29
|
-
// doc-scoped panel opens).
|
|
30
|
-
//
|
|
31
|
-
// Anatomy:
|
|
32
|
-
//
|
|
33
|
-
// ┌─ header (sticky) ─────────────┐
|
|
34
|
-
// │ Title × │
|
|
35
|
-
// ├───────────────────────────────┤
|
|
36
|
-
// │ │
|
|
37
|
-
// │ body (scrollable + flex col) │
|
|
38
|
-
// │ │
|
|
39
|
-
// ├─ footer (sticky, optional) ───┤
|
|
40
|
-
// │ Cancel Save │
|
|
41
|
-
// └───────────────────────────────┘
|
|
42
|
-
//
|
|
43
|
-
// Non-goals:
|
|
44
|
-
// - Multiple concurrent panels. Only one at a time — a second would
|
|
45
|
-
// double the marginRight the shell reads and the second panel would
|
|
46
|
-
// render off-viewport. Callers coordinate open state.
|
|
47
|
-
// - Focus trap. The main content stays interactive; trapping focus
|
|
48
|
-
// here would break the "keep working while the panel is open"
|
|
49
|
-
// promise. ESC still closes.
|
|
50
|
-
// - Resize handle. Fixed width per open; iterate if callers need it.
|
|
51
|
-
//
|
|
52
|
-
// Phone: renders the same panel but at `width: min(100vw, width)`
|
|
53
|
-
// so it simply covers the viewport. The push-left effect degrades
|
|
54
|
-
// to a full-screen overlay — acceptable since these surfaces are
|
|
55
|
-
// desktop-first anyway.
|
|
56
|
-
//
|
|
57
|
-
// Body is a flex column so children can opt into `flex: 1` +
|
|
58
|
-
// `minHeight: 0` and grow to fill the panel (e.g. a textarea that
|
|
59
|
-
// should reach the footer). `overflowY: auto` stays as a safety
|
|
60
|
-
// net for content that genuinely exceeds the panel height.
|
|
61
|
-
|
|
62
|
-
export interface SidePanelProps {
|
|
63
|
-
open: boolean;
|
|
64
|
-
onClose: () => void;
|
|
65
|
-
/** Text shown in the header. Icons / node also accepted. */
|
|
66
|
-
title: ReactNode;
|
|
67
|
-
/**
|
|
68
|
-
* Panel width in CSS px. Default 480 — the sweet spot for an
|
|
69
|
-
* editor with a couple of fields. Callers override for wider
|
|
70
|
-
* content (e.g. Activity timeline).
|
|
71
|
-
*/
|
|
72
|
-
width?: number | undefined;
|
|
73
|
-
/**
|
|
74
|
-
* Optional sticky footer — usually action buttons (Cancel / Save).
|
|
75
|
-
* Omit for read-only surfaces (Activity feed, share reader).
|
|
76
|
-
*/
|
|
77
|
-
footer?: ReactNode | undefined;
|
|
78
|
-
/**
|
|
79
|
-
* Top inset. Defaults to
|
|
80
|
-
* `calc(var(--ck-topbar-height, 64px) + env(safe-area-inset-top, 0px))`
|
|
81
|
-
* so the panel docks under the app's topbar. Consumers with a
|
|
82
|
-
* different topbar height stamp `--ck-topbar-height` on `:root`
|
|
83
|
-
* OR override this prop directly.
|
|
84
|
-
*/
|
|
85
|
-
topOffset?: string | undefined;
|
|
86
|
-
/**
|
|
87
|
-
* Bottom inset. Defaults to `var(--ck-tabbar-height, 0px)` so
|
|
88
|
-
* the panel doesn't cover the mobile tab bar. Override for
|
|
89
|
-
* consumers with different bottom chrome.
|
|
90
|
-
*/
|
|
91
|
-
bottomOffset?: string | undefined;
|
|
92
|
-
/**
|
|
93
|
-
* z-index. Default 25 — sits above main content, below the
|
|
94
|
-
* standard Modal (100) and typical fixed topbar (30).
|
|
95
|
-
*/
|
|
96
|
-
zIndex?: number | undefined;
|
|
97
|
-
children: ReactNode;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const DEFAULT_WIDTH = 480;
|
|
101
|
-
const CSS_VAR_SIDEPANEL_WIDTH = '--ck-sidepanel-width';
|
|
102
|
-
const DEFAULT_TOP_OFFSET =
|
|
103
|
-
'calc(var(--ck-topbar-height, 64px) + env(safe-area-inset-top, 0px))';
|
|
104
|
-
const DEFAULT_BOTTOM_OFFSET = 'var(--ck-tabbar-height, 0px)';
|
|
105
|
-
const DEFAULT_Z_INDEX = 25;
|
|
106
|
-
|
|
107
|
-
export function SidePanel({
|
|
108
|
-
open,
|
|
109
|
-
onClose,
|
|
110
|
-
title,
|
|
111
|
-
width,
|
|
112
|
-
footer,
|
|
113
|
-
topOffset,
|
|
114
|
-
bottomOffset,
|
|
115
|
-
zIndex,
|
|
116
|
-
children,
|
|
117
|
-
}: SidePanelProps) {
|
|
118
|
-
const panelWidth = width ?? DEFAULT_WIDTH;
|
|
119
|
-
const resolvedTop = topOffset ?? DEFAULT_TOP_OFFSET;
|
|
120
|
-
const resolvedBottom = bottomOffset ?? DEFAULT_BOTTOM_OFFSET;
|
|
121
|
-
const resolvedZ = zIndex ?? DEFAULT_Z_INDEX;
|
|
122
|
-
|
|
123
|
-
// Stamp / clear `--ck-sidepanel-width` on document root while open.
|
|
124
|
-
// Consumers that want their layout to shift left when a panel opens
|
|
125
|
-
// read this var and inset their right edge accordingly. The kit
|
|
126
|
-
// itself doesn't force any layout to read it — it's an opt-in
|
|
127
|
-
// contract between the panel and layout consumers.
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
if (!open) return;
|
|
130
|
-
const root = document.documentElement;
|
|
131
|
-
const prev = root.style.getPropertyValue(CSS_VAR_SIDEPANEL_WIDTH);
|
|
132
|
-
root.style.setProperty(CSS_VAR_SIDEPANEL_WIDTH, `${panelWidth}px`);
|
|
133
|
-
return () => {
|
|
134
|
-
// Restore whatever value was there before us — could be empty
|
|
135
|
-
// (fall through to token default) or a previous panel's width
|
|
136
|
-
// if two panels overlap mid-transition.
|
|
137
|
-
if (prev) {
|
|
138
|
-
root.style.setProperty(CSS_VAR_SIDEPANEL_WIDTH, prev);
|
|
139
|
-
} else {
|
|
140
|
-
root.style.removeProperty(CSS_VAR_SIDEPANEL_WIDTH);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}, [open, panelWidth]);
|
|
144
|
-
|
|
145
|
-
// ESC to close. Global listener so users don't have to focus into
|
|
146
|
-
// the panel first. Only mounted while open so it doesn't eat ESC
|
|
147
|
-
// on the main surface.
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
if (!open) return;
|
|
150
|
-
const onKey = (e: KeyboardEvent) => {
|
|
151
|
-
if (e.key === 'Escape') {
|
|
152
|
-
e.stopPropagation();
|
|
153
|
-
onClose();
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
window.addEventListener('keydown', onKey);
|
|
157
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
158
|
-
}, [open, onClose]);
|
|
159
|
-
|
|
160
|
-
// Focus the close button on open so keyboard users have a
|
|
161
|
-
// predictable landing target + can Tab into the body. Not a focus
|
|
162
|
-
// trap — Tab from the last body input walks back out into the
|
|
163
|
-
// main surface, which is the whole point of the pattern.
|
|
164
|
-
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!open) return;
|
|
167
|
-
// Run after transition frame so the button is actually
|
|
168
|
-
// interactive by the time focus lands.
|
|
169
|
-
const raf = requestAnimationFrame(() => {
|
|
170
|
-
closeButtonRef.current?.focus();
|
|
171
|
-
});
|
|
172
|
-
return () => cancelAnimationFrame(raf);
|
|
173
|
-
}, [open]);
|
|
174
|
-
|
|
175
|
-
const containerStyle: CSSProperties = {
|
|
176
|
-
position: 'fixed',
|
|
177
|
-
top: resolvedTop,
|
|
178
|
-
right: 0,
|
|
179
|
-
bottom: resolvedBottom,
|
|
180
|
-
width: `min(100vw, ${panelWidth}px)`,
|
|
181
|
-
background: 'var(--ck-bg-surface, #fff)',
|
|
182
|
-
borderLeft: '1px solid var(--ck-border-subtle, #eee)',
|
|
183
|
-
boxShadow: open ? '-4px 0 20px rgba(0, 0, 0, 0.06)' : 'none',
|
|
184
|
-
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
|
185
|
-
transition:
|
|
186
|
-
'transform var(--ck-dur-fast, 180ms) var(--ck-ease, cubic-bezier(0.22, 1, 0.36, 1))',
|
|
187
|
-
display: 'flex',
|
|
188
|
-
flexDirection: 'column',
|
|
189
|
-
zIndex: resolvedZ,
|
|
190
|
-
// Prevent the closed panel from stealing pointer events on the
|
|
191
|
-
// main surface (translated off-screen but still in the DOM).
|
|
192
|
-
pointerEvents: open ? 'auto' : 'none',
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Portal to <body> so the panel escapes any `transform` /
|
|
196
|
-
// `will-change: transform` / `filter` / `contain: paint` ancestor
|
|
197
|
-
// — those establish a new containing block for `position: fixed`,
|
|
198
|
-
// meaning the panel would anchor to that ancestor instead of the
|
|
199
|
-
// viewport (e.g. inside a ZoomLayer it would land mid-page rather
|
|
200
|
-
// than on the right edge). The body has no such ancestor, so
|
|
201
|
-
// `right: 0` + `top: <offset>` genuinely means "the viewport
|
|
202
|
-
// corner".
|
|
203
|
-
//
|
|
204
|
-
// `inert` (React 19+) instead of `aria-hidden` for the closed
|
|
205
|
-
// state: aria-hidden triggers a runtime warning when a focused
|
|
206
|
-
// descendant exists (the close button focuses on open + keeps
|
|
207
|
-
// focus briefly after close), and `inert` is the modern correct
|
|
208
|
-
// API — removes the subtree from the tab sequence + AT tree +
|
|
209
|
-
// pointer targeting in one attribute.
|
|
210
|
-
return createPortal(
|
|
211
|
-
<aside
|
|
212
|
-
role="complementary"
|
|
213
|
-
inert={!open}
|
|
214
|
-
aria-label={typeof title === 'string' ? title : undefined}
|
|
215
|
-
style={containerStyle}
|
|
216
|
-
>
|
|
217
|
-
<header
|
|
218
|
-
style={{
|
|
219
|
-
display: 'flex',
|
|
220
|
-
alignItems: 'center',
|
|
221
|
-
justifyContent: 'space-between',
|
|
222
|
-
padding: '12px 16px',
|
|
223
|
-
borderBottom: '1px solid var(--ck-border-subtle, #eee)',
|
|
224
|
-
gap: 12,
|
|
225
|
-
flex: '0 0 auto',
|
|
226
|
-
}}
|
|
227
|
-
>
|
|
228
|
-
<div
|
|
229
|
-
style={{
|
|
230
|
-
font: '600 14px/1.4 var(--ck-font-sans)',
|
|
231
|
-
color: 'var(--ck-text-primary, #111)',
|
|
232
|
-
overflow: 'hidden',
|
|
233
|
-
textOverflow: 'ellipsis',
|
|
234
|
-
whiteSpace: 'nowrap',
|
|
235
|
-
}}
|
|
236
|
-
>
|
|
237
|
-
{title}
|
|
238
|
-
</div>
|
|
239
|
-
<button
|
|
240
|
-
ref={closeButtonRef}
|
|
241
|
-
type="button"
|
|
242
|
-
onClick={onClose}
|
|
243
|
-
aria-label="Close panel"
|
|
244
|
-
style={{
|
|
245
|
-
display: 'inline-flex',
|
|
246
|
-
alignItems: 'center',
|
|
247
|
-
justifyContent: 'center',
|
|
248
|
-
width: 28,
|
|
249
|
-
height: 28,
|
|
250
|
-
borderRadius: 6,
|
|
251
|
-
border: 'none',
|
|
252
|
-
background: 'transparent',
|
|
253
|
-
color: 'var(--ck-text-secondary, #666)',
|
|
254
|
-
cursor: 'pointer',
|
|
255
|
-
transition: 'background 120ms ease',
|
|
256
|
-
}}
|
|
257
|
-
onMouseEnter={(e) => {
|
|
258
|
-
e.currentTarget.style.background = 'var(--ck-bg-hover, #f4f4f5)';
|
|
259
|
-
}}
|
|
260
|
-
onMouseLeave={(e) => {
|
|
261
|
-
e.currentTarget.style.background = 'transparent';
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<CloseIcon />
|
|
265
|
-
</button>
|
|
266
|
-
</header>
|
|
267
|
-
<div
|
|
268
|
-
style={{
|
|
269
|
-
flex: 1,
|
|
270
|
-
minHeight: 0,
|
|
271
|
-
overflowY: 'auto',
|
|
272
|
-
padding: 16,
|
|
273
|
-
display: 'flex',
|
|
274
|
-
flexDirection: 'column',
|
|
275
|
-
}}
|
|
276
|
-
>
|
|
277
|
-
{children}
|
|
278
|
-
</div>
|
|
279
|
-
{footer ? (
|
|
280
|
-
<footer
|
|
281
|
-
style={{
|
|
282
|
-
display: 'flex',
|
|
283
|
-
justifyContent: 'flex-end',
|
|
284
|
-
alignItems: 'center',
|
|
285
|
-
gap: 8,
|
|
286
|
-
padding: '12px 16px',
|
|
287
|
-
borderTop: '1px solid var(--ck-border-subtle, #eee)',
|
|
288
|
-
flex: '0 0 auto',
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
{footer}
|
|
292
|
-
</footer>
|
|
293
|
-
) : null}
|
|
294
|
-
</aside>,
|
|
295
|
-
document.body,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function CloseIcon(): ReactNode {
|
|
300
|
-
return (
|
|
301
|
-
<svg
|
|
302
|
-
width="16"
|
|
303
|
-
height="16"
|
|
304
|
-
viewBox="0 0 24 24"
|
|
305
|
-
fill="none"
|
|
306
|
-
stroke="currentColor"
|
|
307
|
-
strokeWidth="2"
|
|
308
|
-
strokeLinecap="round"
|
|
309
|
-
strokeLinejoin="round"
|
|
310
|
-
aria-hidden="true"
|
|
311
|
-
>
|
|
312
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
313
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
314
|
-
</svg>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
// Eyebrow + title + optional right-slot, sticky at top of a page
|
|
4
|
-
// region (NOT the topbar — this is for an in-page sticky like
|
|
5
|
-
// "Reviewing as <name>" or "Document signed · view audit log").
|
|
6
|
-
// Caller controls when it shows. Visual mass matches a topbar
|
|
7
|
-
// (64 px) so it can sit directly underneath one cleanly.
|
|
8
|
-
|
|
9
|
-
export interface StickyBannerProps {
|
|
10
|
-
eyebrow?: string;
|
|
11
|
-
title: ReactNode;
|
|
12
|
-
// Right-aligned action cluster.
|
|
13
|
-
actions?: ReactNode;
|
|
14
|
-
// Sticky top offset in px. Default 64 (below topbar).
|
|
15
|
-
topOffset?: number;
|
|
16
|
-
height?: number;
|
|
17
|
-
className?: string;
|
|
18
|
-
// Visual tone — adjusts background. Default 'info'.
|
|
19
|
-
tone?: "info" | "warning" | "success" | "critical";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const TONE_BG: Record<NonNullable<StickyBannerProps["tone"]>, string> = {
|
|
23
|
-
info: "var(--ck-info-muted)",
|
|
24
|
-
warning: "var(--ck-warning-muted)",
|
|
25
|
-
success: "var(--ck-success-muted)",
|
|
26
|
-
critical: "var(--ck-critical-muted)",
|
|
27
|
-
};
|
|
28
|
-
const TONE_BORDER: Record<NonNullable<StickyBannerProps["tone"]>, string> = {
|
|
29
|
-
info: "var(--ck-accent-border)",
|
|
30
|
-
warning: "var(--ck-warning)",
|
|
31
|
-
success: "var(--ck-success)",
|
|
32
|
-
critical: "var(--ck-critical)",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export function StickyBanner({
|
|
36
|
-
eyebrow,
|
|
37
|
-
title,
|
|
38
|
-
actions,
|
|
39
|
-
topOffset = 64,
|
|
40
|
-
height = 64,
|
|
41
|
-
className,
|
|
42
|
-
tone = "info",
|
|
43
|
-
}: StickyBannerProps) {
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
className={className}
|
|
47
|
-
style={
|
|
48
|
-
{
|
|
49
|
-
position: "sticky",
|
|
50
|
-
top: topOffset,
|
|
51
|
-
height,
|
|
52
|
-
padding: "0 24px",
|
|
53
|
-
display: "flex",
|
|
54
|
-
alignItems: "center",
|
|
55
|
-
gap: 16,
|
|
56
|
-
background: TONE_BG[tone],
|
|
57
|
-
borderBottom: `1px solid ${TONE_BORDER[tone]}`,
|
|
58
|
-
zIndex: 10,
|
|
59
|
-
fontFamily: "var(--ck-font-sans)",
|
|
60
|
-
} as CSSProperties
|
|
61
|
-
}
|
|
62
|
-
>
|
|
63
|
-
<div style={{ flex: 1, minWidth: 0 }}>
|
|
64
|
-
{eyebrow && (
|
|
65
|
-
<div className="ck-eyebrow" style={{ marginBottom: 2 }}>
|
|
66
|
-
{eyebrow}
|
|
67
|
-
</div>
|
|
68
|
-
)}
|
|
69
|
-
<div
|
|
70
|
-
style={{
|
|
71
|
-
font: "500 14px/1.3 var(--ck-font-sans)",
|
|
72
|
-
color: "var(--ck-text-primary)",
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
{title}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
{actions && (
|
|
79
|
-
<div style={{ display: "flex", gap: 8, flex: "none" }}>{actions}</div>
|
|
80
|
-
)}
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
package/src/layout/Topbar.tsx
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// Top chrome strip. Three slots — left (crumbs / brand / back arrow),
|
|
4
|
-
// center (page-specific affordance like a tab switcher), right
|
|
5
|
-
// (actions). Height is exposed as --ck-breadcrumb-height (default
|
|
6
|
-
// 64 px) so the right-side panel, command palette, and other fixed-
|
|
7
|
-
// position UI can offset themselves correctly.
|
|
8
|
-
|
|
9
|
-
export interface TopbarProps {
|
|
10
|
-
// Usually <Breadcrumb /> on desktop, page title on mobile.
|
|
11
|
-
left?: ReactNode;
|
|
12
|
-
// Optional centred slot. Hidden if not supplied.
|
|
13
|
-
center?: ReactNode;
|
|
14
|
-
// Right cluster — theme toggle, profile chip, etc.
|
|
15
|
-
right?: ReactNode;
|
|
16
|
-
// Height in px. Default 64. Stamps --ck-breadcrumb-height so
|
|
17
|
-
// sticky elements below can align.
|
|
18
|
-
height?: number;
|
|
19
|
-
className?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Topbar({
|
|
23
|
-
left,
|
|
24
|
-
center,
|
|
25
|
-
right,
|
|
26
|
-
height = 64,
|
|
27
|
-
className,
|
|
28
|
-
}: TopbarProps) {
|
|
29
|
-
return (
|
|
30
|
-
<header
|
|
31
|
-
data-ck-topbar
|
|
32
|
-
className={className}
|
|
33
|
-
style={
|
|
34
|
-
{
|
|
35
|
-
height,
|
|
36
|
-
padding: "0 24px",
|
|
37
|
-
display: "flex",
|
|
38
|
-
alignItems: "center",
|
|
39
|
-
gap: 16,
|
|
40
|
-
background: "var(--ck-bg-surface)",
|
|
41
|
-
borderBottom: "1px solid var(--ck-border-subtle)",
|
|
42
|
-
["--ck-breadcrumb-height" as string]: `${height}px`,
|
|
43
|
-
} as React.CSSProperties
|
|
44
|
-
}
|
|
45
|
-
>
|
|
46
|
-
<div style={{ flex: "1 1 auto", minWidth: 0 }}>{left}</div>
|
|
47
|
-
{center && (
|
|
48
|
-
<div
|
|
49
|
-
style={{
|
|
50
|
-
position: "absolute",
|
|
51
|
-
left: "50%",
|
|
52
|
-
transform: "translateX(-50%)",
|
|
53
|
-
display: "flex",
|
|
54
|
-
alignItems: "center",
|
|
55
|
-
gap: 8,
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
{center}
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
{right && (
|
|
62
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: "none" }}>
|
|
63
|
-
{right}
|
|
64
|
-
</div>
|
|
65
|
-
)}
|
|
66
|
-
</header>
|
|
67
|
-
);
|
|
68
|
-
}
|
package/src/layout/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export { Shell } from "./Shell";
|
|
2
|
-
export type { ShellProps } from "./Shell";
|
|
3
|
-
export { Topbar } from "./Topbar";
|
|
4
|
-
export type { TopbarProps } from "./Topbar";
|
|
5
|
-
export { Breadcrumb } from "./Breadcrumb";
|
|
6
|
-
export type { BreadcrumbProps, CrumbItem } from "./Breadcrumb";
|
|
7
|
-
export { LeftNavRail } from "./LeftNavRail";
|
|
8
|
-
export type { LeftNavRailProps } from "./LeftNavRail";
|
|
9
|
-
export { useNavRailState } from "./useNavRailState";
|
|
10
|
-
export type { NavRailState, UseNavRailStateOpts } from "./useNavRailState";
|
|
11
|
-
export { MobileTabBar } from "./MobileTabBar";
|
|
12
|
-
export type { MobileTabBarProps, MobileTab } from "./MobileTabBar";
|
|
13
|
-
export { RightSidebarPanel } from "./RightSidebarPanel";
|
|
14
|
-
export type { RightSidebarPanelProps } from "./RightSidebarPanel";
|
|
15
|
-
export { SidePanel } from "./SidePanel";
|
|
16
|
-
export type { SidePanelProps } from "./SidePanel";
|
|
17
|
-
export { StickyBanner } from "./StickyBanner";
|
|
18
|
-
export type { StickyBannerProps } from "./StickyBanner";
|
|
19
|
-
export { NavSection } from "./NavSection";
|
|
20
|
-
export type { NavSectionProps } from "./NavSection";
|
|
21
|
-
export { NavItem } from "./NavItem";
|
|
22
|
-
export type { NavItemProps } from "./NavItem";
|
|
23
|
-
export { NavSearchTrigger } from "./NavSearchTrigger";
|
|
24
|
-
export type { NavSearchTriggerProps } from "./NavSearchTrigger";
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import { useViewport } from "../hooks/useViewport";
|
|
3
|
-
|
|
4
|
-
// Manages the collapse state of <LeftNavRail>. Two layers:
|
|
5
|
-
//
|
|
6
|
-
// 1. User preference — persisted in localStorage under
|
|
7
|
-
// `storageKey`. The user toggles via a button in the rail.
|
|
8
|
-
// 2. Viewport rule — below `forceCollapseBelow` the rail is
|
|
9
|
-
// always collapsed regardless of user preference.
|
|
10
|
-
//
|
|
11
|
-
// The hook returns the EFFECTIVE collapsed state (combining the
|
|
12
|
-
// two) plus a setter that only mutates the user preference (the
|
|
13
|
-
// viewport rule continues to apply on top).
|
|
14
|
-
|
|
15
|
-
export interface UseNavRailStateOpts {
|
|
16
|
-
storageKey?: string;
|
|
17
|
-
// Viewport width below which the rail force-collapses, ignoring
|
|
18
|
-
// user preference. Default 1100 px.
|
|
19
|
-
forceCollapseBelow?: number;
|
|
20
|
-
// Initial preference if nothing is in localStorage. Default false
|
|
21
|
-
// (expanded).
|
|
22
|
-
defaultCollapsed?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface NavRailState {
|
|
26
|
-
collapsed: boolean;
|
|
27
|
-
// The viewport is currently overriding the user preference.
|
|
28
|
-
// Toggle button should be disabled when this is true.
|
|
29
|
-
forcedByViewport: boolean;
|
|
30
|
-
toggle: () => void;
|
|
31
|
-
setCollapsed: (next: boolean) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const DEFAULT_KEY = "ck-leftnav-collapsed";
|
|
35
|
-
|
|
36
|
-
export function useNavRailState({
|
|
37
|
-
storageKey = DEFAULT_KEY,
|
|
38
|
-
forceCollapseBelow = 1100,
|
|
39
|
-
defaultCollapsed = false,
|
|
40
|
-
}: UseNavRailStateOpts = {}): NavRailState {
|
|
41
|
-
const [userPref, setUserPref] = useState<boolean>(() => {
|
|
42
|
-
if (typeof window === "undefined") return defaultCollapsed;
|
|
43
|
-
const stored = window.localStorage.getItem(storageKey);
|
|
44
|
-
if (stored === "1") return true;
|
|
45
|
-
if (stored === "0") return false;
|
|
46
|
-
return defaultCollapsed;
|
|
47
|
-
});
|
|
48
|
-
const vp = useViewport({ narrow: forceCollapseBelow });
|
|
49
|
-
const forcedByViewport = vp.isNarrow;
|
|
50
|
-
const collapsed = forcedByViewport || userPref;
|
|
51
|
-
|
|
52
|
-
const setCollapsed = useCallback(
|
|
53
|
-
(next: boolean) => {
|
|
54
|
-
setUserPref(next);
|
|
55
|
-
},
|
|
56
|
-
[],
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
window.localStorage.setItem(storageKey, userPref ? "1" : "0");
|
|
61
|
-
}, [userPref, storageKey]);
|
|
62
|
-
|
|
63
|
-
const toggle = useCallback(() => {
|
|
64
|
-
if (forcedByViewport) return; // no-op — viewport rules
|
|
65
|
-
setUserPref((v) => !v);
|
|
66
|
-
}, [forcedByViewport]);
|
|
67
|
-
|
|
68
|
-
return { collapsed, forcedByViewport, toggle, setCollapsed };
|
|
69
|
-
}
|
package/src/lib/time-utils.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Time formatting helpers.
|
|
2
|
-
|
|
3
|
-
// "3m ago" / "2h ago" / "5d ago", falls back to a locale date past
|
|
4
|
-
// 30 days. Use for activity rows, comment timestamps, anywhere a
|
|
5
|
-
// short relative cue beats a full timestamp.
|
|
6
|
-
export function relativeTime(ts: number): string {
|
|
7
|
-
const diff = Date.now() - ts;
|
|
8
|
-
const sec = Math.round(diff / 1000);
|
|
9
|
-
if (sec < 60) return `${sec}s ago`;
|
|
10
|
-
const min = Math.round(sec / 60);
|
|
11
|
-
if (min < 60) return `${min}m ago`;
|
|
12
|
-
const hr = Math.round(min / 60);
|
|
13
|
-
if (hr < 24) return `${hr}h ago`;
|
|
14
|
-
const day = Math.round(hr / 24);
|
|
15
|
-
if (day < 30) return `${day}d ago`;
|
|
16
|
-
return new Date(ts).toLocaleDateString();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Null-safe locale date formatter — accepts ms timestamps, ISO
|
|
20
|
-
// strings, null, undefined, or non-finite values. Empty inputs
|
|
21
|
-
// render as an em-dash placeholder ("—"), which most list rows
|
|
22
|
-
// were doing inline with a ternary before.
|
|
23
|
-
export function formatDate(ms: number | string | null | undefined): string {
|
|
24
|
-
const n = coerceMs(ms);
|
|
25
|
-
if (n === null) return "—";
|
|
26
|
-
return new Date(n).toLocaleDateString();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Full date + time formatter with the same null-safe semantics.
|
|
30
|
-
export function formatTimestamp(
|
|
31
|
-
ms: number | string | null | undefined,
|
|
32
|
-
opts?: Intl.DateTimeFormatOptions,
|
|
33
|
-
): string {
|
|
34
|
-
const n = coerceMs(ms);
|
|
35
|
-
if (n === null) return "—";
|
|
36
|
-
return opts ? new Date(n).toLocaleString(undefined, opts) : new Date(n).toLocaleString();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function coerceMs(ms: number | string | null | undefined): number | null {
|
|
40
|
-
if (ms == null) return null;
|
|
41
|
-
const n = typeof ms === "string" ? Number(ms) : ms;
|
|
42
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
43
|
-
return n;
|
|
44
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
// Looping marquee strip — the signature neobrutalism section
|
|
4
|
-
// divider. "NEOBRUTALISM · NEOBRUTALISM · NEOBRUTALISM…" in
|
|
5
|
-
// chunky 800-weight type, scrolling horizontally. Decoration,
|
|
6
|
-
// not nav.
|
|
7
|
-
//
|
|
8
|
-
// Pure CSS animation — no rAF loops, no JS. The repeated text
|
|
9
|
-
// inside is doubled so when the first set scrolls past the edge,
|
|
10
|
-
// the second is already in position to seamlessly continue.
|
|
11
|
-
|
|
12
|
-
export interface MarqueeProps {
|
|
13
|
-
text: string;
|
|
14
|
-
// Pixels per second. Default 60 — comfortable reading speed.
|
|
15
|
-
speed?: number;
|
|
16
|
-
// Visual variant. "default" = accent background; "inverted" =
|
|
17
|
-
// pure black background with white text.
|
|
18
|
-
variant?: "default" | "inverted";
|
|
19
|
-
// px height. Default 56.
|
|
20
|
-
height?: number;
|
|
21
|
-
// Slight skew angle (deg). Default 0; try -2 for a punk feel.
|
|
22
|
-
skew?: number;
|
|
23
|
-
className?: string;
|
|
24
|
-
style?: CSSProperties;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function Marquee({
|
|
28
|
-
text,
|
|
29
|
-
speed = 60,
|
|
30
|
-
variant = "default",
|
|
31
|
-
height = 56,
|
|
32
|
-
skew = 0,
|
|
33
|
-
className,
|
|
34
|
-
style,
|
|
35
|
-
}: MarqueeProps) {
|
|
36
|
-
// Each phrase is ~80 px wide for a typical 20-char fragment;
|
|
37
|
-
// 10 copies gives ~800 px content — wide enough that the loop
|
|
38
|
-
// is always seamless on any viewport. Real measurement happens
|
|
39
|
-
// visually; for now the count + duration combo just need to be
|
|
40
|
-
// multiples that produce a continuous loop.
|
|
41
|
-
const phrases = new Array(10).fill(text).join(" • ");
|
|
42
|
-
const duration = `${(phrases.length * 8) / speed}s`;
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
className={className}
|
|
47
|
-
style={{
|
|
48
|
-
overflow: "hidden",
|
|
49
|
-
height,
|
|
50
|
-
background:
|
|
51
|
-
variant === "inverted" ? "#000000" : "var(--ck-accent, #88AAEE)",
|
|
52
|
-
color: variant === "inverted" ? "#FFFFFF" : "#000000",
|
|
53
|
-
border: "2px solid var(--neobrut-ink, #000)",
|
|
54
|
-
borderRadius: 4,
|
|
55
|
-
transform: skew ? `skewY(${skew}deg)` : undefined,
|
|
56
|
-
position: "relative",
|
|
57
|
-
...style,
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
<div
|
|
61
|
-
style={{
|
|
62
|
-
position: "absolute",
|
|
63
|
-
inset: 0,
|
|
64
|
-
display: "flex",
|
|
65
|
-
alignItems: "center",
|
|
66
|
-
gap: 40,
|
|
67
|
-
whiteSpace: "nowrap",
|
|
68
|
-
font: "800 22px/1 var(--ck-font-display, var(--ck-font-sans))",
|
|
69
|
-
letterSpacing: "0.04em",
|
|
70
|
-
animation: `ck-neobrut-marquee ${duration} linear infinite`,
|
|
71
|
-
willChange: "transform",
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
{/* Doubled for seamless loop. CSS animation translates by
|
|
75
|
-
-50% so the second copy moves into the first's position. */}
|
|
76
|
-
<span>{phrases}</span>
|
|
77
|
-
<span aria-hidden>{phrases}</span>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|