@s3pweb/shell-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.
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @s3pweb/shell-ui — S3pweb brand theme.
3
+ *
4
+ * Single source of truth for the "corporate" sidebar decoration. Both the
5
+ * stateless `@s3pweb/shell-ui` Sidebar and the higher-level
6
+ * `@s3pweb/shared-layouts` Sidebar emit the same `data-shell-ui-*`
7
+ * attributes, so this stylesheet applies to either one.
8
+ *
9
+ * @import '@s3pweb/shell-ui/themes/s3pweb.css';
10
+ * <html class='dark' data-shell-theme='s3pweb'>...</html>
11
+ *
12
+ * Source palette: apps/s3pweb/src/theme/s3pweb.css (s3pweb.com brand).
13
+ *
14
+ * Per-module icon chip colours: set `slug` on NavGroup (shell-ui) or
15
+ * `basePath` on SidebarNavGroup (shared-layouts). Recognised values:
16
+ * 'ecodriving' | 'maintenance' | 'incidents' | 'pre-shift' | 'social' |
17
+ * 'mileages-fuel'. Other slugs fall back to a neutral grey chip.
18
+ */
19
+
20
+ [data-shell-theme='s3pweb'] {
21
+ /* Generic tokens (consumed by the shell-ui Sidebar's Tailwind utilities). */
22
+ --color-background: #f9fafc;
23
+ --color-foreground: #2a2a2a;
24
+ --color-card: #ffffff;
25
+ --color-card-foreground: #2a2a2a;
26
+ --color-accent: #e8ecf2;
27
+ --color-accent-foreground: #1d3349;
28
+ --color-border: rgba(29, 51, 73, 0.12);
29
+ --color-muted: #e8ecf2;
30
+ --color-muted-foreground: #5a6b7f;
31
+ --color-secondary: #e1e4ea;
32
+ --color-secondary-foreground: #1d3349;
33
+
34
+ /* Brand palette. */
35
+ --color-primary: #1d3349;
36
+ --color-primary-foreground: #ffffff;
37
+ --color-cta: #f7be28;
38
+ --color-cta-foreground: #1d3349;
39
+ --color-c1-deep: #091129;
40
+
41
+ /* Sidebar / top-bar surface (slightly cooler than --color-background). */
42
+ --color-sidebar: #e8ecf2;
43
+ --color-sidebar-foreground: #1d3349;
44
+ --shell-chip-bg: #e9ecf2;
45
+ --shell-chip-fg: #5a6b7f;
46
+
47
+ /* Status tokens — match the Map & Truck design system spec. */
48
+ --color-success: #2f8a3c;
49
+ --color-success-foreground: #ffffff;
50
+ --color-warning: #d97706;
51
+ --color-warning-foreground: #ffffff;
52
+ --color-destructive: #c92a2a;
53
+ --color-destructive-foreground: #ffffff;
54
+ }
55
+
56
+ [data-shell-theme='s3pweb'].dark,
57
+ .dark [data-shell-theme='s3pweb'] {
58
+ --color-background: #091129;
59
+ --color-foreground: #e8ecf2;
60
+ --color-card: #0e1d33;
61
+ --color-card-foreground: #e8ecf2;
62
+ --color-accent: #1d3349;
63
+ --color-accent-foreground: #f7be28;
64
+ --color-border: rgba(247, 190, 40, 0.18);
65
+ --color-muted: #1d3349;
66
+ --color-muted-foreground: #94a5bd;
67
+ --color-secondary: #1d3349;
68
+ --color-secondary-foreground: #e8ecf2;
69
+
70
+ --color-primary: #f7be28;
71
+ --color-primary-foreground: #091129;
72
+
73
+ --color-sidebar: #1d3349;
74
+ --color-sidebar-foreground: #e8ecf2;
75
+ --shell-chip-bg: rgba(247, 190, 40, 0.12);
76
+ --shell-chip-fg: #e8ecf2;
77
+
78
+ /* Brighter status tokens for dark mode (matches the DS dark palette). */
79
+ --color-success: #4ade80;
80
+ --color-success-foreground: #091129;
81
+ --color-warning: #fbbf24;
82
+ --color-warning-foreground: #091129;
83
+ --color-destructive: #ef4444;
84
+ --color-destructive-foreground: #091129;
85
+ }
86
+
87
+ /* ============================================================================
88
+ Sidebar icon — coloured square chip per module (slug).
89
+
90
+ Two cases share the same chip styling:
91
+ - `[data-shell-ui-nav-group-button]` — the group button in a grouped nav.
92
+ The slug lives on the wrapping `[data-shell-ui-nav-group]` ancestor.
93
+ - top-level `[data-shell-ui-nav-item]` — a flat leaf nav item with no
94
+ parent group. The slug lives ON the item itself. Excluded: sub-items
95
+ inside a group's expanded section (those sit under
96
+ `[data-shell-ui-nav-children]` and stay neutral).
97
+ ============================================================================ */
98
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon],
99
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-item]:not([data-shell-ui-nav-children] *) [data-shell-ui-nav-icon] {
100
+ width: 22px;
101
+ height: 22px;
102
+ padding: 4px;
103
+ box-sizing: border-box;
104
+ border-radius: 6px;
105
+ background: var(--shell-chip-bg);
106
+ color: var(--shell-chip-fg);
107
+ }
108
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon] svg,
109
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-item]:not([data-shell-ui-nav-children] *) [data-shell-ui-nav-icon] svg {
110
+ width: 100%;
111
+ height: 100%;
112
+ }
113
+
114
+ /* ============================================================================
115
+ Top-bar — baseline chip styling. Per-slug palettes below paint the actual
116
+ colours for each module; both sidebar group buttons + flat leaves + top-bar
117
+ buttons share the same rule per module.
118
+ ============================================================================ */
119
+ [data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button] {
120
+ background: var(--shell-chip-bg);
121
+ color: var(--shell-chip-fg);
122
+ }
123
+ [data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button]:hover {
124
+ filter: brightness(0.96);
125
+ }
126
+
127
+ /* ============================================================================
128
+ Per-module palettes — currently EMPTY.
129
+
130
+ Every module inherits the neutral chip styling above (`--shell-chip-bg` /
131
+ `--shell-chip-fg`) so the sidebar reads as one calm grey palette. Re-introduce
132
+ per-slug rules below when a brighter, module-specific palette is wanted.
133
+
134
+ Adding a palette for a slug requires THREE selectors (they share the same
135
+ declarations, so list them in one rule). Sample for slug='foo':
136
+
137
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-slug='foo'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon],
138
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-item][data-shell-ui-nav-slug='foo']:not([data-shell-ui-nav-children] *) [data-shell-ui-nav-icon],
139
+ [data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button][data-shell-ui-nav-slug='foo'] {
140
+ background: var(--color-emerald-100);
141
+ color: var(--color-emerald-700);
142
+ }
143
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-nav-slug='foo'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon],
144
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-nav-item][data-shell-ui-nav-slug='foo']:not([data-shell-ui-nav-children] *) [data-shell-ui-nav-icon],
145
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button][data-shell-ui-nav-slug='foo'] {
146
+ background: color-mix(in oklab, var(--color-emerald-700) 28%, transparent);
147
+ color: var(--color-emerald-300);
148
+ }
149
+
150
+ Selector roles:
151
+ - 1st = sidebar group icon chip (slug lives on the wrapping `nav-group`).
152
+ - 2nd = sidebar flat (top-level) leaf chip; the `:not(…)` excludes
153
+ sub-items rendered inside an expanded group so only the top-level
154
+ item gets painted.
155
+ - 3rd = horizontal top-bar button.
156
+
157
+ Slug source:
158
+ `nav-slug` defaults to `id` when omitted on a NavItem / NavGroup. Set it
159
+ explicitly only when `id` isn't a CSS-friendly string (e.g. it's a route
160
+ path like `/incidents` or a UUID).
161
+
162
+ Shade-tier guidance (pick ONE per slug, then reuse the pair for dark mode):
163
+ - SATURATED families (red, orange, amber, yellow, lime, green, emerald,
164
+ teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose):
165
+ light: bg=*-100 fg=*-700
166
+ dark: bg=color-mix(in oklab, *-700 28%, transparent) fg=*-300
167
+ - DESATURATED families (slate, gray, zinc, neutral, stone, olive):
168
+ light: bg=*-200 fg=*-500
169
+ dark: bg=color-mix(in oklab, *-500 30%, transparent) fg=*-300
170
+ (the *-700 of these families reads almost black; *-500 keeps contrast
171
+ with the *-200 bg without darkening the icon to mud.)
172
+ - One-step DARKER variant (e.g. when two slugs share a hue family and
173
+ you want them distinct, like social=violet vs pre-shift=purple-darker):
174
+ light: bg=*-300 fg=*-800
175
+ dark: bg=color-mix(in oklab, *-800 36%, transparent) fg=*-300
176
+
177
+ Tailwind 4.2.1 ships every family above as `--color-{family}-{shade}` so
178
+ `var(--color-emerald-700)` etc. resolves without any extra setup. Skip the
179
+ shell-ui preset's @theme block when picking — these are global Tailwind
180
+ tokens, not s3pweb-specific.
181
+ ============================================================================ */
182
+
183
+ /* ============================================================================
184
+ Sub-item hover — light yellow tint. Mirrors the original SaaS shell rule
185
+ `[data-variant='corporate'] [data-sidebar-nav-item]:not([aria-current='page']):hover`.
186
+ Scoped to sub-items (descendants of `[data-shell-ui-nav-children]`) only —
187
+ group headers AND top-level flat items keep Tailwind's hover:bg-primary/10
188
+ so flat-nav products feel consistent with group buttons.
189
+ ============================================================================ */
190
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-children] [data-shell-ui-nav-item]:not([data-active]):not([aria-current='page']):hover {
191
+ background: rgba(247, 190, 40, 0.14);
192
+ color: var(--color-primary);
193
+ }
194
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-children] [data-shell-ui-nav-item]:not([data-active]):not([aria-current='page']):hover {
195
+ background: rgba(247, 190, 40, 0.18);
196
+ color: var(--color-cta);
197
+ }
198
+
199
+ /* ============================================================================
200
+ Active leaf item — navy gradient + yellow line (left) + yellow dot (right).
201
+ ============================================================================ */
202
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][data-active],
203
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][aria-current='page'] {
204
+ position: relative;
205
+ background: var(--color-primary);
206
+ color: var(--color-primary-foreground);
207
+ box-shadow: 0 8px 20px -10px rgba(29, 51, 73, 0.55);
208
+ }
209
+ /* Yellow left bar — expanded sidebar only. Scoped to `:not([data-collapsed])`
210
+ because at `left: -15px` it would leak past the icon-only column when
211
+ collapsed (~64 px wide); the right-side dot covers active-state visibility
212
+ in the collapsed layout. */
213
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][data-active]::before,
214
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][aria-current='page']::before {
215
+ content: '';
216
+ position: absolute;
217
+ left: -15px;
218
+ top: 6px;
219
+ bottom: 6px;
220
+ width: 5px;
221
+ border-radius: 3px;
222
+ background: var(--color-cta);
223
+ box-shadow: 0 0 12px rgba(247, 190, 40, 0.7);
224
+ }
225
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][data-active]::after,
226
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][aria-current='page']::after {
227
+ content: '';
228
+ position: absolute;
229
+ right: 4px;
230
+ top: 50%;
231
+ transform: translateY(-50%);
232
+ width: 5px;
233
+ height: 5px;
234
+ border-radius: 1px;
235
+ background: var(--color-cta);
236
+ }
237
+ /* Expanded sidebar has more room — push the dot a bit further from the
238
+ row's right edge to balance against the new left bar. */
239
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][data-active]::after,
240
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][aria-current='page']::after {
241
+ right: 8px;
242
+ }
243
+ /* When the collapsed sidebar's nav has a scrollbar, the active row's right
244
+ edge sits ~12 px inside the column. Push the dot a couple px past the
245
+ row's right edge so it stays visible against the scrollbar track. */
246
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar][data-collapsed][data-has-scrollbar] [data-shell-ui-nav-item][data-active]::after,
247
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar][data-collapsed][data-has-scrollbar] [data-shell-ui-nav-item][aria-current='page']::after {
248
+ right: -2px;
249
+ }
250
+ /* Same compensation for the group's has-children chevron — without the
251
+ scrollbar the chevron sits flush at right: 0; with the scrollbar present
252
+ it needs to slide a few px past the row's edge to keep the same visual
253
+ offset relative to the sidebar's right border. */
254
+ [data-shell-theme='s3pweb'] [data-shell-ui-sidebar][data-collapsed][data-has-scrollbar] [data-shell-ui-nav-group-button] [data-shell-ui-nav-has-children] {
255
+ right: -3px;
256
+ }
257
+
258
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][data-active],
259
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][aria-current='page'] {
260
+ background: var(--color-cta);
261
+ color: var(--color-cta-foreground);
262
+ box-shadow: none;
263
+ }
264
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][data-active]::before,
265
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar]:not([data-collapsed]) [data-shell-ui-nav-item][aria-current='page']::before,
266
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][data-active]::after,
267
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-sidebar] [data-shell-ui-nav-item][aria-current='page']::after {
268
+ background: var(--color-c1-deep);
269
+ box-shadow: none;
270
+ }
271
+
272
+ /* ============================================================================
273
+ Active flyout / popover rows — apply the same yellow square dot the active
274
+ sidebar leaf uses so popovers (collapsed-sidebar fly-out, topbar group
275
+ dropdown, "Plus" overflow menus) read coherent with the expanded sidebar
276
+ selection state. The row already has `data-active` set by the React
277
+ renderers; the dot is positioned the same way as the sidebar's ::after.
278
+ ============================================================================ */
279
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-flyout-item][data-active] {
280
+ position: relative;
281
+ }
282
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-flyout-item][data-active]::after {
283
+ content: '';
284
+ position: absolute;
285
+ right: 8px;
286
+ top: 50%;
287
+ transform: translateY(-50%);
288
+ width: 5px;
289
+ height: 5px;
290
+ border-radius: 1px;
291
+ background: var(--color-cta);
292
+ }
293
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-nav-flyout-item][data-active]::after {
294
+ background: var(--color-c1-deep);
295
+ }
296
+
297
+ /* Active group header keeps the shell-ui default (bg-primary/5 + text-foreground).
298
+ The per-module palette only colours the icon chip, not the row background. */
299
+
300
+ /* ============================================================================
301
+ Children tree line — discreet navy tint.
302
+ ============================================================================ */
303
+ [data-shell-theme='s3pweb'] [data-shell-ui-nav-children] {
304
+ border-left-color: rgba(29, 51, 73, 0.15);
305
+ }
306
+ .dark [data-shell-theme='s3pweb'] [data-shell-ui-nav-children] {
307
+ border-left-color: rgba(247, 190, 40, 0.2);
308
+ }
309
+
310
+ /* ============================================================================
311
+ Scrollbars — floating-pill thumbs tinted with the s3pweb navy. The
312
+ `background-clip: padding-box` + transparent-border trick visually inflates
313
+ the gap around the thumb so the ends read as fully rounded pills rather than
314
+ touching the track edges. In dark mode the navy disappears against the navy
315
+ bg, so we lift to the foreground grey-blue (--c3 family) which still reads
316
+ as part of the navy palette while staying visible.
317
+
318
+ Firefox uses `scrollbar-width: thin` (no hover transition possible — sets a
319
+ single resting tint via `scrollbar-color`). Webkit gets the richer
320
+ pseudo-element treatment with a more pronounced hover state.
321
+ ============================================================================ */
322
+ /* Firefox-only fallback. Chrome ≥121 honours `scrollbar-color` too, but doing
323
+ so switches it into a new overlay-scrollbar mode that *suppresses* the
324
+ ::-webkit-scrollbar pseudo-elements — including their :hover state. Gating
325
+ the standard properties behind `@supports not selector(::-webkit-scrollbar)`
326
+ keeps Firefox tinted while letting Chromium / WebKit fall through to the
327
+ richer pseudo-element rules below. */
328
+ @supports not selector(::-webkit-scrollbar) {
329
+ [data-shell-theme='s3pweb'],
330
+ [data-shell-theme='s3pweb'] * {
331
+ scrollbar-width: thin;
332
+ scrollbar-color: rgba(29, 51, 73, 0.45) transparent;
333
+ }
334
+ .dark [data-shell-theme='s3pweb'],
335
+ .dark [data-shell-theme='s3pweb'] * {
336
+ scrollbar-color: rgba(232, 236, 242, 0.4) transparent;
337
+ }
338
+ }
339
+
340
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar {
341
+ width: 12px;
342
+ height: 12px;
343
+ }
344
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar-track,
345
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar-corner {
346
+ background: transparent;
347
+ }
348
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb {
349
+ background-color: rgba(29, 51, 73, 0.45);
350
+ background-clip: padding-box;
351
+ border: 3px solid transparent;
352
+ border-radius: 999px;
353
+ min-height: 36px;
354
+ min-width: 36px;
355
+ transition:
356
+ background-color 0.18s ease,
357
+ border-color 0.18s ease;
358
+ }
359
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb:hover {
360
+ background-color: rgba(29, 51, 73, 0.85);
361
+ border-width: 2px;
362
+ }
363
+ [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb:active {
364
+ background-color: rgb(29, 51, 73);
365
+ border-width: 2px;
366
+ }
367
+ .dark [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb {
368
+ background-color: rgba(232, 236, 242, 0.35);
369
+ }
370
+ .dark [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb:hover {
371
+ background-color: rgba(232, 236, 242, 0.75);
372
+ }
373
+ .dark [data-shell-theme='s3pweb'] ::-webkit-scrollbar-thumb:active {
374
+ background-color: rgb(232, 236, 242);
375
+ }
@@ -0,0 +1,123 @@
1
+ import { ReactNode } from 'react';
2
+ export interface NavItem {
3
+ id: string;
4
+ label: string;
5
+ icon?: ReactNode;
6
+ active?: boolean;
7
+ badge?: ReactNode;
8
+ /**
9
+ * When set, the item renders as an `<a href>` instead of a `<button>` so
10
+ * native browser semantics work (right-click → Open in new tab, middle-click,
11
+ * keyboard navigation as a link). The default `onClick` still calls
12
+ * `onItemSelect` with `event.preventDefault()` on plain left-click so SPA
13
+ * routers stay in control. Modifier keys (cmd/ctrl/shift/middle button) are
14
+ * allowed through to the browser.
15
+ */
16
+ href?: string;
17
+ /** Free-form payload — passed back to onItemSelect (handy for routing metadata). */
18
+ meta?: unknown;
19
+ /**
20
+ * Theme hook — rendered as `data-shell-ui-nav-slug` on the item's root.
21
+ * Defaults to `id` when omitted. Override only when you need a CSS-friendly
22
+ * theme key that's different from your `id` (e.g. when `id` is a UUID or
23
+ * a route path that doesn't make a stable CSS selector).
24
+ */
25
+ slug?: string;
26
+ }
27
+ export interface NavGroup {
28
+ id: string;
29
+ label: string;
30
+ icon?: ReactNode;
31
+ items: NavItem[];
32
+ active?: boolean;
33
+ expanded?: boolean;
34
+ /**
35
+ * Theme hook — rendered as `data-shell-ui-nav-slug` on the group's root.
36
+ * Defaults to `id` when omitted. Brand themes use it to apply per-module
37
+ * decoration (e.g. coloured icon chips in the s3pweb theme).
38
+ */
39
+ slug?: string;
40
+ }
41
+ export type NavEntry = NavItem | NavGroup;
42
+ export declare function isNavGroup(entry: NavEntry): entry is NavGroup;
43
+ /**
44
+ * Action button rendered alongside the built-in theme / nav-mode / logout
45
+ * controls (sidebar footer cluster + top-bar right cluster). Use for things
46
+ * like fullscreen, mobile-mode, density toggles, etc. — Shell paints them
47
+ * with the same look as the built-in toggles in both layouts.
48
+ *
49
+ * When `hoverContent` is provided, the action button becomes a HoverCard
50
+ * trigger and `hoverContent` is rendered in the popover on hover (e.g. a
51
+ * partners list, user-profile preview). `onClick` is optional in that case
52
+ * — hover-only actions don't need a click handler.
53
+ */
54
+ export interface ShellAction {
55
+ id: string;
56
+ label: string;
57
+ icon: ReactNode;
58
+ onClick?: () => void;
59
+ /** Toggle-button-style active state — applies a subtle highlight when true. */
60
+ active?: boolean;
61
+ /**
62
+ * When set, the trigger opens a **HoverCard** with this content on hover.
63
+ * Shell handles placement automatically: opens downward in the top-bar,
64
+ * sideways in the sidebar footer. Mutually exclusive with `clickContent`
65
+ * — pick the trigger semantics, not both.
66
+ */
67
+ hoverContent?: ReactNode;
68
+ /**
69
+ * When set, the trigger opens a **Popover** with this content on click
70
+ * (focus-trapping, stays open until dismissed). Use this when the
71
+ * popover holds interactive controls (buttons, links, forms) — hover
72
+ * dismissal would be too fragile for that case.
73
+ */
74
+ clickContent?: ReactNode;
75
+ /**
76
+ * Override the default icon-button rendering with a self-contained widget
77
+ * (e.g. a `<PartnerCluster>` that owns its own popover, a status panel,
78
+ * an avatar with menu).
79
+ *
80
+ * - **Top-bar mode**: rendered AS-IS, regardless of width.
81
+ * - **Sidebar mode (expanded)**: rendered AS-IS — falls back to
82
+ * `customTrigger` when `customSidebarTrigger` is not set. The widget
83
+ * should fit the sidebar width (~256px).
84
+ * - **Sidebar mode (collapsed, ~32px)**: fallback to the standard icon
85
+ * button + `hoverContent` / `clickContent` wrap, since the custom
86
+ * widget likely won't fit. Make sure to set a reasonable `icon` and
87
+ * pair the action with `clickContent` for the collapsed popup.
88
+ *
89
+ * When `customTrigger` renders, `hoverContent` and `clickContent` are
90
+ * NOT wrapped around it — the widget owns its own popup wiring.
91
+ */
92
+ customTrigger?: ReactNode;
93
+ /**
94
+ * Sidebar-expanded override for `customTrigger`. Use when the widget
95
+ * needs different layout / popover positioning in the sidebar than in
96
+ * the top-bar (e.g. label visible, popover side='right'). When omitted,
97
+ * sidebar-expanded falls back to `customTrigger`.
98
+ */
99
+ customSidebarTrigger?: ReactNode;
100
+ /**
101
+ * Where the action renders relative to the built-in cluster:
102
+ * - 'leading' — before the nav-mode toggle (or top of footer in sidebar)
103
+ * - 'trailing' (default) — after theme toggle, before user avatar
104
+ * (or below built-in toggles in sidebar)
105
+ * Within a placement bucket, actions render in array order.
106
+ */
107
+ placement?: 'leading' | 'trailing';
108
+ /**
109
+ * Draw a separator immediately after this action — a vertical 1px line
110
+ * in the top-bar right cluster, a horizontal 1px line spanning the full
111
+ * footer width in the sidebar. Handy for visually grouping an action
112
+ * (e.g. an ecosystem cluster) apart from the rest.
113
+ */
114
+ dividerAfter?: boolean;
115
+ /**
116
+ * When the top-bar collapses its actions cluster (narrow viewport), this
117
+ * action stays inline instead of moving into the "…" overflow popover.
118
+ * Use for widgets whose `customTrigger` is already responsive on its own
119
+ * (e.g. a partners cluster that swaps to a "+N" chip via its own internal
120
+ * media query). Default false — every action collapses.
121
+ */
122
+ keepInlineWhenCompact?: boolean;
123
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@s3pweb/shell-ui",
3
+ "version": "0.1.0",
4
+ "description": "Stateless sidebar / top-nav components for the s3pweb white-label shell. Consumer owns router, i18n, and active state.",
5
+ "license": "ISC",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./preset.css": "./dist/preset.css",
17
+ "./themes/s3pweb.css": "./dist/themes/s3pweb.css",
18
+ "./themes/aftral.css": "./dist/themes/aftral.css"
19
+ },
20
+ "sideEffects": [
21
+ "**/*.css"
22
+ ],
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "class-variance-authority": "0.7.1",
29
+ "clsx": "2.1.1",
30
+ "lucide-react": "0.487.0",
31
+ "radix-ui": "1.4.3",
32
+ "tailwind-merge": "3.5.0",
33
+ "tw-animate-css": "1.4.0"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@storybook/react-vite": "10.4.1",
41
+ "@tailwindcss/vite": "4.2.1",
42
+ "@types/react": "19.2.14",
43
+ "@types/react-dom": "19.2.3",
44
+ "@vitejs/plugin-react": "5.1.4",
45
+ "babel-plugin-react-compiler": "1.0.0",
46
+ "react": "19.2.4",
47
+ "react-dom": "19.2.4",
48
+ "storybook": "10.4.1",
49
+ "tailwindcss": "4.2.1",
50
+ "typescript": "5.9.3",
51
+ "vite": "7.3.3",
52
+ "vite-plugin-dts": "5.0.1"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "keywords": [
58
+ "react",
59
+ "shell",
60
+ "sidebar",
61
+ "topbar",
62
+ "tailwind",
63
+ "shadcn",
64
+ "white-label",
65
+ "s3pweb"
66
+ ],
67
+ "scripts": {
68
+ "build": "vite build && cp src/preset.css dist/ && cp -r src/themes dist/themes",
69
+ "typecheck": "tsc --noEmit",
70
+ "storybook": "storybook dev -p 6006",
71
+ "build-storybook": "storybook build"
72
+ }
73
+ }