@cosxai/ui 0.1.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/package.json +38 -0
- package/src/actionbar/ActionBar.tsx +436 -0
- package/src/actionbar/ActionBarButton.tsx +110 -0
- package/src/actionbar/ActionBarMenuGroup.tsx +106 -0
- package/src/actionbar/ActionBarProvider.tsx +76 -0
- package/src/actionbar/actionbar-context.ts +23 -0
- package/src/actionbar/index.ts +13 -0
- package/src/actionbar/types.ts +50 -0
- package/src/actionbar/useActionBarItems.ts +47 -0
- package/src/ambient/AmbientBackdrop.tsx +74 -0
- package/src/ambient/CommandInput.tsx +107 -0
- package/src/ambient/SuperbarStrip.tsx +36 -0
- package/src/ambient/index.ts +6 -0
- package/src/bento/BentoCell.tsx +66 -0
- package/src/bento/BentoGrid.tsx +42 -0
- package/src/bento/index.ts +2 -0
- package/src/command/CommandPalette.tsx +277 -0
- package/src/command/CommandProvider.tsx +57 -0
- package/src/command/command-context.ts +12 -0
- package/src/command/index.ts +6 -0
- package/src/command/rank.ts +45 -0
- package/src/command/types.ts +26 -0
- package/src/command/useCommandSource.ts +37 -0
- package/src/dialogs/DialogsProvider.tsx +216 -0
- package/src/dialogs/Modal.tsx +204 -0
- package/src/dialogs/Toast.tsx +85 -0
- package/src/dialogs/dialogs-context.ts +6 -0
- package/src/dialogs/index.ts +10 -0
- package/src/dialogs/types.ts +37 -0
- package/src/dialogs/useDialogs.ts +8 -0
- package/src/editorial/EditorialSpotlight.tsx +63 -0
- package/src/editorial/Folio.tsx +52 -0
- package/src/editorial/PlateMarker.tsx +33 -0
- package/src/editorial/RomanSection.tsx +65 -0
- package/src/editorial/RunningMarginalia.tsx +65 -0
- package/src/editorial/index.ts +10 -0
- package/src/frutiger/GlossyOrb.tsx +79 -0
- package/src/frutiger/SkyBackdrop.tsx +114 -0
- package/src/frutiger/index.ts +2 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useKeyboardHotkey.ts +80 -0
- package/src/hooks/useReducedMotion.ts +20 -0
- package/src/hooks/useViewport.ts +61 -0
- package/src/index.ts +26 -0
- package/src/layout/Breadcrumb.tsx +74 -0
- package/src/layout/LeftNavRail.tsx +126 -0
- package/src/layout/MobileTabBar.tsx +101 -0
- package/src/layout/NavItem.tsx +128 -0
- package/src/layout/NavSearchTrigger.tsx +88 -0
- package/src/layout/NavSection.tsx +40 -0
- package/src/layout/RightSidebarPanel.tsx +111 -0
- package/src/layout/Shell.tsx +91 -0
- package/src/layout/StickyBanner.tsx +83 -0
- package/src/layout/Topbar.tsx +68 -0
- package/src/layout/index.ts +22 -0
- package/src/layout/useNavRailState.ts +69 -0
- package/src/lib/cn.ts +7 -0
- package/src/lib/time-utils.ts +44 -0
- package/src/neobrutalism/Marquee.tsx +81 -0
- package/src/neobrutalism/Sticker.tsx +71 -0
- package/src/neobrutalism/index.ts +4 -0
- package/src/primitives/Avatar.tsx +53 -0
- package/src/primitives/Button.tsx +30 -0
- package/src/primitives/Card.tsx +41 -0
- package/src/primitives/Checkbox.tsx +78 -0
- package/src/primitives/CountBadge.tsx +50 -0
- package/src/primitives/Input.tsx +71 -0
- package/src/primitives/Kbd.tsx +45 -0
- package/src/primitives/PageHeader.tsx +77 -0
- package/src/primitives/Tag.tsx +56 -0
- package/src/primitives/Textarea.tsx +62 -0
- package/src/primitives/ToggleSwitch.tsx +79 -0
- package/src/primitives/Tooltip.tsx +171 -0
- package/src/primitives/index.ts +24 -0
- package/src/pwa/InstallPromptBanner.tsx +132 -0
- package/src/pwa/index.ts +4 -0
- package/src/pwa/manifest.template.json +20 -0
- package/src/pwa/registerSW.ts +55 -0
- package/src/riso/Halftone.tsx +85 -0
- package/src/riso/Misregister.tsx +63 -0
- package/src/riso/RisoStamp.tsx +76 -0
- package/src/riso/index.ts +3 -0
- package/src/sketch/HandUnderline.tsx +53 -0
- package/src/sketch/RoughArrow.tsx +91 -0
- package/src/sketch/RoughBox.tsx +73 -0
- package/src/sketch/StickyNote.tsx +56 -0
- package/src/sketch/index.ts +4 -0
- package/src/styles/base.css +80 -0
- package/src/styles/chrome-ambient.css +222 -0
- package/src/styles/chrome-bento.css +184 -0
- package/src/styles/chrome-editorial.css +145 -0
- package/src/styles/chrome-frutiger.css +364 -0
- package/src/styles/chrome-neobrutalism.css +315 -0
- package/src/styles/chrome-riso.css +328 -0
- package/src/styles/chrome-sketch.css +351 -0
- package/src/styles/chrome-swiss.css +232 -0
- package/src/styles/chrome-terminal.css +235 -0
- package/src/styles/fonts.css +22 -0
- package/src/styles/index.css +198 -0
- package/src/styles/tokens.css +976 -0
- package/src/terminal/AsciiBox.tsx +65 -0
- package/src/terminal/BrailleSpinner.tsx +46 -0
- package/src/terminal/index.ts +4 -0
- package/src/theme/ThemeProvider.tsx +93 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/inline-script.ts +36 -0
- package/src/theme/theme-context.ts +7 -0
- package/src/theme/types.ts +22 -0
- package/src/theme/useTheme.ts +8 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, type ReactNode, type CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
// Fixed-bottom tab bar for phones. Sits above the home-indicator
|
|
4
|
+
// safe area on iOS, exposes --ck-tabbar-height so the GlobalActionBar
|
|
5
|
+
// and any sticky bottom UI can lift to clear it.
|
|
6
|
+
//
|
|
7
|
+
// Tab list is fully configurable — caller supplies items with an
|
|
8
|
+
// active match function so the bar can highlight the right one for
|
|
9
|
+
// the current pathname. No router coupling.
|
|
10
|
+
|
|
11
|
+
export interface MobileTab {
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon: ReactNode;
|
|
15
|
+
// Called when the tab is tapped. Caller handles navigation.
|
|
16
|
+
onSelect: () => void;
|
|
17
|
+
// Whether this tab represents the current route. Caller derives
|
|
18
|
+
// from their router (e.g. `pathname.startsWith("/shares")`).
|
|
19
|
+
active: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MobileTabBarProps {
|
|
23
|
+
tabs: MobileTab[];
|
|
24
|
+
// Height of the bar (content area; safe-area inset added on top).
|
|
25
|
+
// Default 56 px.
|
|
26
|
+
height?: number;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function MobileTabBar({
|
|
31
|
+
tabs,
|
|
32
|
+
height = 56,
|
|
33
|
+
className,
|
|
34
|
+
}: MobileTabBarProps) {
|
|
35
|
+
// Stamp --ck-tabbar-height. Includes safe-area inset so consumers
|
|
36
|
+
// (e.g. floating action bar) can lift cleanly above the home
|
|
37
|
+
// indicator. iOS env() works in calc() inside :root vars.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
document.documentElement.style.setProperty(
|
|
40
|
+
"--ck-tabbar-height",
|
|
41
|
+
`calc(${height}px + env(safe-area-inset-bottom, 0px))`,
|
|
42
|
+
);
|
|
43
|
+
return () => {
|
|
44
|
+
document.documentElement.style.setProperty("--ck-tabbar-height", "0px");
|
|
45
|
+
};
|
|
46
|
+
}, [height]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<nav
|
|
50
|
+
data-ck-tabbar
|
|
51
|
+
aria-label="Primary"
|
|
52
|
+
className={className}
|
|
53
|
+
style={
|
|
54
|
+
{
|
|
55
|
+
position: "fixed",
|
|
56
|
+
left: 0,
|
|
57
|
+
right: 0,
|
|
58
|
+
bottom: 0,
|
|
59
|
+
height: "var(--ck-tabbar-height)",
|
|
60
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)",
|
|
61
|
+
background: "var(--ck-bg-surface)",
|
|
62
|
+
borderTop: "1px solid var(--ck-border-subtle)",
|
|
63
|
+
display: "flex",
|
|
64
|
+
justifyContent: "space-around",
|
|
65
|
+
alignItems: "stretch",
|
|
66
|
+
zIndex: 50,
|
|
67
|
+
fontFamily: "var(--ck-font-sans)",
|
|
68
|
+
} as CSSProperties
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
{tabs.map((t) => (
|
|
72
|
+
<button
|
|
73
|
+
key={t.key}
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={t.onSelect}
|
|
76
|
+
aria-current={t.active ? "page" : undefined}
|
|
77
|
+
style={{
|
|
78
|
+
flex: 1,
|
|
79
|
+
display: "flex",
|
|
80
|
+
flexDirection: "column",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
gap: 2,
|
|
84
|
+
padding: "6px 8px",
|
|
85
|
+
border: "none",
|
|
86
|
+
background: "transparent",
|
|
87
|
+
color: t.active ? "var(--ck-accent)" : "var(--ck-text-tertiary)",
|
|
88
|
+
cursor: "pointer",
|
|
89
|
+
font: "500 11px/1 var(--ck-font-sans)",
|
|
90
|
+
transition: "color var(--ck-dur-fast) var(--ck-ease)",
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<span style={{ fontSize: 18, lineHeight: 1, display: "inline-flex" }}>
|
|
94
|
+
{t.icon}
|
|
95
|
+
</span>
|
|
96
|
+
<span>{t.label}</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</nav>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
+
import { CountBadge } from "../primitives/CountBadge";
|
|
3
|
+
|
|
4
|
+
// Single nav row inside <LeftNavRail>. Router-agnostic — pass your
|
|
5
|
+
// own router's link primitive via the `as` prop, or fall back to a
|
|
6
|
+
// plain anchor. Collapsed mode renders icon-only with the label as
|
|
7
|
+
// a tooltip.
|
|
8
|
+
//
|
|
9
|
+
// Use with NavLink-style render-prop routers by wrapping NavItem
|
|
10
|
+
// with your router's link and forwarding `active`:
|
|
11
|
+
//
|
|
12
|
+
// <NavLink to="/x">
|
|
13
|
+
// {({ isActive }) =>
|
|
14
|
+
// <NavItem active={isActive} icon={<X/>} label="X" />
|
|
15
|
+
// }
|
|
16
|
+
// </NavLink>
|
|
17
|
+
|
|
18
|
+
export interface NavItemProps {
|
|
19
|
+
// Visible label (also used as tooltip when collapsed).
|
|
20
|
+
label: string;
|
|
21
|
+
icon: ReactNode;
|
|
22
|
+
active?: boolean;
|
|
23
|
+
// Optional notification count badge — renders nothing for 0.
|
|
24
|
+
badge?: number;
|
|
25
|
+
collapsed?: boolean;
|
|
26
|
+
onClick?: () => void;
|
|
27
|
+
// Render as an anchor with this href. When omitted, renders as
|
|
28
|
+
// a button (use onClick).
|
|
29
|
+
href?: string;
|
|
30
|
+
// Optional custom render — escape hatch for router-link wrappers
|
|
31
|
+
// that need to spread their own props on the element. Receives
|
|
32
|
+
// the rendered children + the inline style.
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function NavItem({
|
|
38
|
+
label,
|
|
39
|
+
icon,
|
|
40
|
+
active,
|
|
41
|
+
badge,
|
|
42
|
+
collapsed,
|
|
43
|
+
onClick,
|
|
44
|
+
href,
|
|
45
|
+
className,
|
|
46
|
+
style,
|
|
47
|
+
}: NavItemProps) {
|
|
48
|
+
const showBadge = badge !== undefined && badge > 0;
|
|
49
|
+
const baseStyle: CSSProperties = {
|
|
50
|
+
position: "relative",
|
|
51
|
+
display: "flex",
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
borderRadius: "var(--ck-radius-md)",
|
|
54
|
+
transition: "background var(--ck-dur-fast) var(--ck-ease), color var(--ck-dur-fast) var(--ck-ease)",
|
|
55
|
+
background: active ? "var(--ck-accent-muted)" : "transparent",
|
|
56
|
+
color: active ? "var(--ck-accent)" : "var(--ck-text-secondary)",
|
|
57
|
+
font: "400 13px/1 var(--ck-font-sans)",
|
|
58
|
+
fontWeight: active ? 500 : 400,
|
|
59
|
+
cursor: "pointer",
|
|
60
|
+
textDecoration: "none",
|
|
61
|
+
border: "none",
|
|
62
|
+
...(collapsed
|
|
63
|
+
? {
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
width: 40,
|
|
66
|
+
height: 40,
|
|
67
|
+
}
|
|
68
|
+
: {
|
|
69
|
+
justifyContent: "space-between",
|
|
70
|
+
padding: "8px 12px",
|
|
71
|
+
}),
|
|
72
|
+
...style,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const content = collapsed ? (
|
|
76
|
+
<>
|
|
77
|
+
<span style={{ display: "inline-flex" }}>{icon}</span>
|
|
78
|
+
{showBadge && (
|
|
79
|
+
<span style={{ position: "absolute", top: 2, right: 2 }}>
|
|
80
|
+
<CountBadge count={badge} size="sm" tone="critical" />
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 10 }}>
|
|
87
|
+
{icon}
|
|
88
|
+
<span>{label}</span>
|
|
89
|
+
</span>
|
|
90
|
+
{showBadge && <CountBadge count={badge} size="sm" tone="critical" />}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// data-ck-navitem + data-active are escape hatches for chrome
|
|
95
|
+
// CSS to override the inline visual state (e.g. neobrutalism
|
|
96
|
+
// wants a fully different chunky look on the active item).
|
|
97
|
+
const dataAttrs = {
|
|
98
|
+
"data-ck-navitem": "",
|
|
99
|
+
"data-active": active ? "true" : undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (href !== undefined) {
|
|
103
|
+
return (
|
|
104
|
+
<a
|
|
105
|
+
href={href}
|
|
106
|
+
title={collapsed ? label : undefined}
|
|
107
|
+
className={className}
|
|
108
|
+
style={baseStyle}
|
|
109
|
+
{...dataAttrs}
|
|
110
|
+
>
|
|
111
|
+
{content}
|
|
112
|
+
</a>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={onClick}
|
|
120
|
+
title={collapsed ? label : undefined}
|
|
121
|
+
className={className}
|
|
122
|
+
style={baseStyle}
|
|
123
|
+
{...dataAttrs}
|
|
124
|
+
>
|
|
125
|
+
{content}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Kbd } from "../primitives/Kbd";
|
|
2
|
+
|
|
3
|
+
// Search-style command-palette opener for the left rail. Wide
|
|
4
|
+
// click target, search icon on the left, kbd hint on the right.
|
|
5
|
+
// Hidden in collapsed mode — the palette is still reachable via
|
|
6
|
+
// ⌘K, no need for a dedicated trigger when there's no label space.
|
|
7
|
+
|
|
8
|
+
export interface NavSearchTriggerProps {
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
hotkey?: string;
|
|
12
|
+
collapsed?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NavSearchTrigger({
|
|
16
|
+
onClick,
|
|
17
|
+
placeholder = "Search",
|
|
18
|
+
hotkey = "K",
|
|
19
|
+
collapsed,
|
|
20
|
+
}: NavSearchTriggerProps) {
|
|
21
|
+
if (collapsed) return null;
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={onClick}
|
|
26
|
+
aria-label="Open command palette"
|
|
27
|
+
title="Open command palette"
|
|
28
|
+
data-ck-search-trigger
|
|
29
|
+
style={{
|
|
30
|
+
position: "relative",
|
|
31
|
+
width: "100%",
|
|
32
|
+
height: 34,
|
|
33
|
+
padding: "0 8px 0 32px",
|
|
34
|
+
background: "var(--ck-bg-muted)",
|
|
35
|
+
border: "1px solid transparent",
|
|
36
|
+
borderRadius: "var(--ck-radius-sm)",
|
|
37
|
+
cursor: "pointer",
|
|
38
|
+
display: "flex",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
justifyContent: "space-between",
|
|
41
|
+
textAlign: "left",
|
|
42
|
+
transition: "background var(--ck-dur-fast) var(--ck-ease), border-color var(--ck-dur-fast) var(--ck-ease)",
|
|
43
|
+
}}
|
|
44
|
+
onMouseEnter={(e) => {
|
|
45
|
+
e.currentTarget.style.borderColor = "var(--ck-border-strong)";
|
|
46
|
+
}}
|
|
47
|
+
onMouseLeave={(e) => {
|
|
48
|
+
e.currentTarget.style.borderColor = "transparent";
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<svg
|
|
52
|
+
width="14"
|
|
53
|
+
height="14"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
strokeWidth="2"
|
|
58
|
+
strokeLinecap="round"
|
|
59
|
+
strokeLinejoin="round"
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
left: 10,
|
|
63
|
+
top: "50%",
|
|
64
|
+
transform: "translateY(-50%)",
|
|
65
|
+
color: "var(--ck-text-tertiary)",
|
|
66
|
+
pointerEvents: "none",
|
|
67
|
+
}}
|
|
68
|
+
aria-hidden
|
|
69
|
+
>
|
|
70
|
+
<circle cx="11" cy="11" r="7" />
|
|
71
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
72
|
+
</svg>
|
|
73
|
+
<span
|
|
74
|
+
style={{
|
|
75
|
+
flex: 1,
|
|
76
|
+
font: "400 12px/1 var(--ck-font-sans)",
|
|
77
|
+
color: "var(--ck-text-tertiary)",
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{placeholder}
|
|
81
|
+
</span>
|
|
82
|
+
<span style={{ display: "inline-flex", gap: 2 }}>
|
|
83
|
+
<Kbd>Mod</Kbd>
|
|
84
|
+
<Kbd>{hotkey}</Kbd>
|
|
85
|
+
</span>
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
// Section header inside <LeftNavRail>. Mono uppercase eyebrow above
|
|
4
|
+
// a group of <NavItem>s. Hides its own label in collapsed mode —
|
|
5
|
+
// icons are self-explanatory at 56 px and the cluster delimiters
|
|
6
|
+
// become visual noise.
|
|
7
|
+
|
|
8
|
+
export interface NavSectionProps {
|
|
9
|
+
label: string;
|
|
10
|
+
collapsed?: boolean;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NavSection({ label, collapsed, children }: NavSectionProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ marginBottom: 16 }}>
|
|
17
|
+
{!collapsed && (
|
|
18
|
+
<div
|
|
19
|
+
className="ck-tag"
|
|
20
|
+
style={{
|
|
21
|
+
padding: "0 12px",
|
|
22
|
+
marginBottom: 6,
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
{label}
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
display: "flex",
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
gap: collapsed ? 4 : 1,
|
|
33
|
+
alignItems: collapsed ? "center" : "stretch",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
// Floating right-side panel. Used for ephemeral side surfaces —
|
|
4
|
+
// comments, signing management, review-link list, etc. Unlike the
|
|
5
|
+
// always-on AgentRail-style right rail, this is summoned by user
|
|
6
|
+
// action and dismissed via the close button or Esc.
|
|
7
|
+
//
|
|
8
|
+
// Sticks below the topbar (reads --ck-breadcrumb-height), max-height
|
|
9
|
+
// leaves room for any bottom-fixed UI (--ck-tabbar-height).
|
|
10
|
+
// Caller owns the open/close state.
|
|
11
|
+
|
|
12
|
+
export interface RightSidebarPanelProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
// Required — small uppercase eyebrow above the title slot.
|
|
16
|
+
eyebrow: string;
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
// Right-side header slot (e.g. filter dropdown). Sits beside the
|
|
19
|
+
// close button.
|
|
20
|
+
headerExtras?: ReactNode;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
// px. Default 320.
|
|
23
|
+
width?: number;
|
|
24
|
+
// px offset from viewport edges. Default 12.
|
|
25
|
+
edgeOffset?: number;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function RightSidebarPanel({
|
|
30
|
+
open,
|
|
31
|
+
onClose,
|
|
32
|
+
eyebrow,
|
|
33
|
+
title,
|
|
34
|
+
headerExtras,
|
|
35
|
+
children,
|
|
36
|
+
width = 320,
|
|
37
|
+
edgeOffset = 12,
|
|
38
|
+
className,
|
|
39
|
+
}: RightSidebarPanelProps) {
|
|
40
|
+
if (!open) return null;
|
|
41
|
+
const topOffset = `calc(var(--ck-breadcrumb-height, 64px) + ${edgeOffset}px)`;
|
|
42
|
+
const bottomOffset = `calc(var(--ck-tabbar-height, 0px) + ${edgeOffset}px)`;
|
|
43
|
+
return (
|
|
44
|
+
<aside
|
|
45
|
+
role="complementary"
|
|
46
|
+
className={className}
|
|
47
|
+
style={
|
|
48
|
+
{
|
|
49
|
+
position: "fixed",
|
|
50
|
+
top: topOffset,
|
|
51
|
+
right: edgeOffset,
|
|
52
|
+
width,
|
|
53
|
+
maxHeight: `calc(100vh - ${topOffset} - ${bottomOffset})`,
|
|
54
|
+
background: "var(--ck-bg-surface)",
|
|
55
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
56
|
+
borderRadius: "var(--ck-radius-md)",
|
|
57
|
+
boxShadow: "var(--ck-shadow-2)",
|
|
58
|
+
display: "flex",
|
|
59
|
+
flexDirection: "column",
|
|
60
|
+
zIndex: 60,
|
|
61
|
+
fontFamily: "var(--ck-font-sans)",
|
|
62
|
+
} as CSSProperties
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
padding: "14px 16px",
|
|
68
|
+
borderBottom: "1px solid var(--ck-border-subtle)",
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "flex-start",
|
|
71
|
+
gap: 8,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
75
|
+
<div className="ck-eyebrow" style={{ marginBottom: title ? 4 : 0 }}>
|
|
76
|
+
{eyebrow}
|
|
77
|
+
</div>
|
|
78
|
+
{title && (
|
|
79
|
+
<div
|
|
80
|
+
style={{
|
|
81
|
+
font: "500 14px/1.3 var(--ck-font-sans)",
|
|
82
|
+
color: "var(--ck-text-primary)",
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{title}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
{headerExtras}
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={onClose}
|
|
93
|
+
aria-label="Close"
|
|
94
|
+
style={{
|
|
95
|
+
border: "none",
|
|
96
|
+
background: "transparent",
|
|
97
|
+
color: "var(--ck-text-tertiary)",
|
|
98
|
+
cursor: "pointer",
|
|
99
|
+
fontSize: 18,
|
|
100
|
+
lineHeight: 1,
|
|
101
|
+
padding: 2,
|
|
102
|
+
marginLeft: 4,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
×
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>{children}</div>
|
|
109
|
+
</aside>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
// Layout shell. The kit is unopinionated about WHICH navigation
|
|
4
|
+
// primitives sit in each slot — pass <LeftNavRail>, <Topbar>,
|
|
5
|
+
// <MobileTabBar> etc. (or your own equivalents) and Shell composes
|
|
6
|
+
// them into the standard layout:
|
|
7
|
+
//
|
|
8
|
+
// ┌──────────────────────────────────────────┐
|
|
9
|
+
// │ topbar (full width, fixed) │
|
|
10
|
+
// ├────────┬─────────────────────────┬───────┤
|
|
11
|
+
// │ │ │ │
|
|
12
|
+
// │ left │ main (children) │ right │
|
|
13
|
+
// │ nav │ │ rail │
|
|
14
|
+
// │ (fix) │ insets via CSS vars │ (fix) │
|
|
15
|
+
// │ │ │ │
|
|
16
|
+
// └────────┴─────────────────────────┴───────┘
|
|
17
|
+
//
|
|
18
|
+
// All three rails are position:fixed and stamp their widths/heights
|
|
19
|
+
// as CSS vars (--ck-leftnav-width, --ck-rightrail-width,
|
|
20
|
+
// --ck-tabbar-height). Shell reads those vars to inset the main
|
|
21
|
+
// region. No prop drilling.
|
|
22
|
+
|
|
23
|
+
export interface ShellProps {
|
|
24
|
+
// Drop <LeftNavRail> here. Shell renders it as-is; the rail itself
|
|
25
|
+
// handles fixed positioning.
|
|
26
|
+
leftRail?: ReactNode;
|
|
27
|
+
// <Topbar>. Sticky at the top of the main column.
|
|
28
|
+
topbar?: ReactNode;
|
|
29
|
+
// Page body.
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
// Right rail (e.g. agent panel, comments panel) — rendered as-is.
|
|
32
|
+
rightRail?: ReactNode;
|
|
33
|
+
// Mobile bottom tab bar — rendered as-is.
|
|
34
|
+
tabBar?: ReactNode;
|
|
35
|
+
// Inline style overrides for the main region (useful for setting
|
|
36
|
+
// background per route).
|
|
37
|
+
mainStyle?: CSSProperties;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Shell({
|
|
42
|
+
leftRail,
|
|
43
|
+
topbar,
|
|
44
|
+
children,
|
|
45
|
+
rightRail,
|
|
46
|
+
tabBar,
|
|
47
|
+
mainStyle,
|
|
48
|
+
className,
|
|
49
|
+
}: ShellProps) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-ck-shell
|
|
53
|
+
className={className}
|
|
54
|
+
style={{
|
|
55
|
+
minHeight: "100vh",
|
|
56
|
+
background: "var(--ck-bg-canvas)",
|
|
57
|
+
color: "var(--ck-text-primary)",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{leftRail}
|
|
61
|
+
{rightRail}
|
|
62
|
+
{tabBar}
|
|
63
|
+
<div
|
|
64
|
+
style={
|
|
65
|
+
{
|
|
66
|
+
marginLeft: "var(--ck-leftnav-width, 0px)",
|
|
67
|
+
marginRight: "var(--ck-rightrail-width, 0px)",
|
|
68
|
+
paddingBottom: "var(--ck-tabbar-height, 0px)",
|
|
69
|
+
minHeight: "100vh",
|
|
70
|
+
display: "flex",
|
|
71
|
+
flexDirection: "column",
|
|
72
|
+
transition: "margin var(--ck-dur-fast) var(--ck-ease)",
|
|
73
|
+
} as CSSProperties
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
{topbar && (
|
|
77
|
+
<div
|
|
78
|
+
style={{
|
|
79
|
+
position: "sticky",
|
|
80
|
+
top: 0,
|
|
81
|
+
zIndex: 20,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{topbar}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<main style={{ flex: 1, minHeight: 0, ...mainStyle }}>{children}</main>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
}
|