@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.
Files changed (109) hide show
  1. package/package.json +38 -0
  2. package/src/actionbar/ActionBar.tsx +436 -0
  3. package/src/actionbar/ActionBarButton.tsx +110 -0
  4. package/src/actionbar/ActionBarMenuGroup.tsx +106 -0
  5. package/src/actionbar/ActionBarProvider.tsx +76 -0
  6. package/src/actionbar/actionbar-context.ts +23 -0
  7. package/src/actionbar/index.ts +13 -0
  8. package/src/actionbar/types.ts +50 -0
  9. package/src/actionbar/useActionBarItems.ts +47 -0
  10. package/src/ambient/AmbientBackdrop.tsx +74 -0
  11. package/src/ambient/CommandInput.tsx +107 -0
  12. package/src/ambient/SuperbarStrip.tsx +36 -0
  13. package/src/ambient/index.ts +6 -0
  14. package/src/bento/BentoCell.tsx +66 -0
  15. package/src/bento/BentoGrid.tsx +42 -0
  16. package/src/bento/index.ts +2 -0
  17. package/src/command/CommandPalette.tsx +277 -0
  18. package/src/command/CommandProvider.tsx +57 -0
  19. package/src/command/command-context.ts +12 -0
  20. package/src/command/index.ts +6 -0
  21. package/src/command/rank.ts +45 -0
  22. package/src/command/types.ts +26 -0
  23. package/src/command/useCommandSource.ts +37 -0
  24. package/src/dialogs/DialogsProvider.tsx +216 -0
  25. package/src/dialogs/Modal.tsx +204 -0
  26. package/src/dialogs/Toast.tsx +85 -0
  27. package/src/dialogs/dialogs-context.ts +6 -0
  28. package/src/dialogs/index.ts +10 -0
  29. package/src/dialogs/types.ts +37 -0
  30. package/src/dialogs/useDialogs.ts +8 -0
  31. package/src/editorial/EditorialSpotlight.tsx +63 -0
  32. package/src/editorial/Folio.tsx +52 -0
  33. package/src/editorial/PlateMarker.tsx +33 -0
  34. package/src/editorial/RomanSection.tsx +65 -0
  35. package/src/editorial/RunningMarginalia.tsx +65 -0
  36. package/src/editorial/index.ts +10 -0
  37. package/src/frutiger/GlossyOrb.tsx +79 -0
  38. package/src/frutiger/SkyBackdrop.tsx +114 -0
  39. package/src/frutiger/index.ts +2 -0
  40. package/src/hooks/index.ts +5 -0
  41. package/src/hooks/useKeyboardHotkey.ts +80 -0
  42. package/src/hooks/useReducedMotion.ts +20 -0
  43. package/src/hooks/useViewport.ts +61 -0
  44. package/src/index.ts +26 -0
  45. package/src/layout/Breadcrumb.tsx +74 -0
  46. package/src/layout/LeftNavRail.tsx +126 -0
  47. package/src/layout/MobileTabBar.tsx +101 -0
  48. package/src/layout/NavItem.tsx +128 -0
  49. package/src/layout/NavSearchTrigger.tsx +88 -0
  50. package/src/layout/NavSection.tsx +40 -0
  51. package/src/layout/RightSidebarPanel.tsx +111 -0
  52. package/src/layout/Shell.tsx +91 -0
  53. package/src/layout/StickyBanner.tsx +83 -0
  54. package/src/layout/Topbar.tsx +68 -0
  55. package/src/layout/index.ts +22 -0
  56. package/src/layout/useNavRailState.ts +69 -0
  57. package/src/lib/cn.ts +7 -0
  58. package/src/lib/time-utils.ts +44 -0
  59. package/src/neobrutalism/Marquee.tsx +81 -0
  60. package/src/neobrutalism/Sticker.tsx +71 -0
  61. package/src/neobrutalism/index.ts +4 -0
  62. package/src/primitives/Avatar.tsx +53 -0
  63. package/src/primitives/Button.tsx +30 -0
  64. package/src/primitives/Card.tsx +41 -0
  65. package/src/primitives/Checkbox.tsx +78 -0
  66. package/src/primitives/CountBadge.tsx +50 -0
  67. package/src/primitives/Input.tsx +71 -0
  68. package/src/primitives/Kbd.tsx +45 -0
  69. package/src/primitives/PageHeader.tsx +77 -0
  70. package/src/primitives/Tag.tsx +56 -0
  71. package/src/primitives/Textarea.tsx +62 -0
  72. package/src/primitives/ToggleSwitch.tsx +79 -0
  73. package/src/primitives/Tooltip.tsx +171 -0
  74. package/src/primitives/index.ts +24 -0
  75. package/src/pwa/InstallPromptBanner.tsx +132 -0
  76. package/src/pwa/index.ts +4 -0
  77. package/src/pwa/manifest.template.json +20 -0
  78. package/src/pwa/registerSW.ts +55 -0
  79. package/src/riso/Halftone.tsx +85 -0
  80. package/src/riso/Misregister.tsx +63 -0
  81. package/src/riso/RisoStamp.tsx +76 -0
  82. package/src/riso/index.ts +3 -0
  83. package/src/sketch/HandUnderline.tsx +53 -0
  84. package/src/sketch/RoughArrow.tsx +91 -0
  85. package/src/sketch/RoughBox.tsx +73 -0
  86. package/src/sketch/StickyNote.tsx +56 -0
  87. package/src/sketch/index.ts +4 -0
  88. package/src/styles/base.css +80 -0
  89. package/src/styles/chrome-ambient.css +222 -0
  90. package/src/styles/chrome-bento.css +184 -0
  91. package/src/styles/chrome-editorial.css +145 -0
  92. package/src/styles/chrome-frutiger.css +364 -0
  93. package/src/styles/chrome-neobrutalism.css +315 -0
  94. package/src/styles/chrome-riso.css +328 -0
  95. package/src/styles/chrome-sketch.css +351 -0
  96. package/src/styles/chrome-swiss.css +232 -0
  97. package/src/styles/chrome-terminal.css +235 -0
  98. package/src/styles/fonts.css +22 -0
  99. package/src/styles/index.css +198 -0
  100. package/src/styles/tokens.css +976 -0
  101. package/src/terminal/AsciiBox.tsx +65 -0
  102. package/src/terminal/BrailleSpinner.tsx +46 -0
  103. package/src/terminal/index.ts +4 -0
  104. package/src/theme/ThemeProvider.tsx +93 -0
  105. package/src/theme/index.ts +5 -0
  106. package/src/theme/inline-script.ts +36 -0
  107. package/src/theme/theme-context.ts +7 -0
  108. package/src/theme/types.ts +22 -0
  109. 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
+ }