@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.
- package/README.md +337 -0
- package/dist/components/actions-menu.d.ts +45 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/ecosystem-mega-panel.d.ts +37 -0
- package/dist/components/hover-card.d.ts +16 -0
- package/dist/components/layout-switcher.d.ts +52 -0
- package/dist/components/partner-cluster.d.ts +111 -0
- package/dist/components/popover.d.ts +24 -0
- package/dist/components/realtime-pulse.d.ts +51 -0
- package/dist/components/tooltip.d.ts +14 -0
- package/dist/components/user-menu.d.ts +36 -0
- package/dist/i18n.d.ts +18 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1931 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/popover-offsets.d.ts +57 -0
- package/dist/lib/sidebar-layout-context.d.ts +13 -0
- package/dist/lib/use-media-query.d.ts +10 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/preset.css +140 -0
- package/dist/shell.d.ts +85 -0
- package/dist/sidebar.d.ts +114 -0
- package/dist/themes/aftral.css +46 -0
- package/dist/themes/s3pweb.css +375 -0
- package/dist/types.d.ts +123 -0
- package/package.json +73 -0
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|