@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
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# @s3pweb/shell-ui
|
|
2
|
+
|
|
3
|
+
Stateless, white-label-ready application shell for React. A single `<Shell>`
|
|
4
|
+
component renders either a **vertical sidebar** or a **horizontal top-nav**
|
|
5
|
+
based on a `navMode` prop. The consumer owns router, i18n, auth, and state —
|
|
6
|
+
Shell owns the chrome.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌──────────────┬────────────────────────┐ ┌────────────────────────┐
|
|
10
|
+
│ ▢ Logo │ │ │ Logo ▢ ▢ ▢ ▢ 👤 🌙 │
|
|
11
|
+
│ │ │ ├────────────────────────┤
|
|
12
|
+
│ ▢ Home │ <Outlet /> │ or │ │
|
|
13
|
+
│ ▢ Inbox ⓿ │ │ │ <Outlet /> │
|
|
14
|
+
│ ▼ Reports │ │ │ │
|
|
15
|
+
│ ▢ Settings │ │ │ │
|
|
16
|
+
└──────────────┴────────────────────────┘ └────────────────────────┘
|
|
17
|
+
navMode='sidebar' navMode='top'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Design
|
|
21
|
+
|
|
22
|
+
- **No router, no i18n provider, no state library.** Pass `items` with
|
|
23
|
+
`active: boolean` flags and an `onItemSelect` callback. Wire to your router.
|
|
24
|
+
- **Hybrid controlled / uncontrolled.** `collapsed`, `navMode`, and per-group
|
|
25
|
+
`expanded` work zero-config; pass props + callbacks to persist them.
|
|
26
|
+
- **Theming via Tailwind v4 tokens.** All colors live in CSS variables
|
|
27
|
+
(`--color-primary`, `--color-sidebar`, …). Override them per brand without
|
|
28
|
+
forking the component.
|
|
29
|
+
- **No `@s3pweb/*` imports.** Externally consumable as-is.
|
|
30
|
+
|
|
31
|
+
Runtime deps: `react`, `radix-ui`, `lucide-react`, `class-variance-authority`,
|
|
32
|
+
`clsx`, `tailwind-merge`, `tw-animate-css`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add @s3pweb/shell-ui
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires **React 19** and **Tailwind CSS v4** in the consumer app.
|
|
41
|
+
|
|
42
|
+
## Public API
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import {
|
|
46
|
+
Shell, // <Shell> — sidebar OR top-nav, switched by navMode prop
|
|
47
|
+
Sidebar, // <Sidebar> — vertical-only, exported for advanced layouts
|
|
48
|
+
SidebarItems, // The nav-items list alone (used for mobile Sheet drawers)
|
|
49
|
+
isNavGroup, // Type guard: NavEntry → NavGroup
|
|
50
|
+
type NavItem,
|
|
51
|
+
type NavGroup,
|
|
52
|
+
type NavEntry, // NavItem | NavGroup
|
|
53
|
+
type SidebarUser, // { name, subtitle?, initials? }
|
|
54
|
+
type ShellLocale, // 'fr' | 'en'
|
|
55
|
+
type ShellProps,
|
|
56
|
+
} from '@s3pweb/shell-ui';
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Minimal example (outside the monorepo)
|
|
60
|
+
|
|
61
|
+
**`src/index.css`** — Tailwind v4, scan shell-ui sources, import the preset:
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
@import 'tailwindcss';
|
|
65
|
+
|
|
66
|
+
/* So Tailwind generates the utility classes shell-ui references. */
|
|
67
|
+
@source "../node_modules/@s3pweb/shell-ui/src/**/*.{ts,tsx}";
|
|
68
|
+
|
|
69
|
+
/* Neutral tokens + dark-variant binding + tw-animate-css.
|
|
70
|
+
Skip if your app already declares --color-primary, --color-sidebar, etc. */
|
|
71
|
+
@import '@s3pweb/shell-ui/preset.css';
|
|
72
|
+
|
|
73
|
+
/* Optional brand theme. */
|
|
74
|
+
@import '@s3pweb/shell-ui/themes/s3pweb.css';
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**`src/App.tsx`** — render the shell:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { useState } from 'react';
|
|
81
|
+
import { BarChart3, FileText, Home, Inbox, Settings } from 'lucide-react';
|
|
82
|
+
import { Shell, type NavEntry } from '@s3pweb/shell-ui';
|
|
83
|
+
|
|
84
|
+
export function App() {
|
|
85
|
+
const [activeId, setActiveId] = useState('inbox');
|
|
86
|
+
|
|
87
|
+
const items: NavEntry[] = [
|
|
88
|
+
{ id: 'home', label: 'Home', icon: <Home />, active: activeId === 'home' },
|
|
89
|
+
{
|
|
90
|
+
id: 'inbox', label: 'Inbox', icon: <Inbox />, active: activeId === 'inbox',
|
|
91
|
+
badge: <span className="rounded-full bg-primary px-1.5 text-xs text-primary-foreground">12</span>,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'reports', label: 'Reports', icon: <BarChart3 />,
|
|
95
|
+
active: activeId.startsWith('reports-'),
|
|
96
|
+
items: [
|
|
97
|
+
{ id: 'reports-monthly', label: 'Monthly', icon: <FileText />, active: activeId === 'reports-monthly' },
|
|
98
|
+
{ id: 'reports-quarterly', label: 'Quarterly', icon: <FileText />, active: activeId === 'reports-quarterly' },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{ id: 'settings', label: 'Settings', icon: <Settings />, active: activeId === 'settings' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Shell
|
|
106
|
+
items={items}
|
|
107
|
+
locale="en"
|
|
108
|
+
logo={<span className="text-lg font-semibold">ACME</span>}
|
|
109
|
+
user={{ name: 'Alex Dev', subtitle: 'alex@acme.com' }}
|
|
110
|
+
onItemSelect={(item) => setActiveId(item.id)}
|
|
111
|
+
onLogout={() => signOut()}
|
|
112
|
+
onThemeToggle={() => document.documentElement.classList.toggle('dark')}
|
|
113
|
+
>
|
|
114
|
+
<main className="p-8">
|
|
115
|
+
<h1 className="text-2xl font-semibold">Active: {activeId}</h1>
|
|
116
|
+
</main>
|
|
117
|
+
</Shell>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
No provider stack, no router required. Pass `navMode='top'` (or wire
|
|
123
|
+
`onNavModeChange`) to swap chrome layout without touching the rest of the code.
|
|
124
|
+
|
|
125
|
+
## Wiring inside the s3pweb monorepo
|
|
126
|
+
|
|
127
|
+
Apps don't render `<Shell>` directly — they go through `MainLayout` in
|
|
128
|
+
`@s3pweb/shared-layouts`, which bridges Zustand (`useUIStore`) → Shell props:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
// apps/s3pweb/src/app/main-layout.tsx
|
|
132
|
+
<SharedMainLayout
|
|
133
|
+
logo={<S3pwebLogo />}
|
|
134
|
+
navigationItems={navigationItems}
|
|
135
|
+
bgClassName="bg-white"
|
|
136
|
+
variant="corporate"
|
|
137
|
+
enableNavModeToggle
|
|
138
|
+
user={user ? { name: user.fullName, subtitle: user.email } : undefined}
|
|
139
|
+
onLogout={logout}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```css
|
|
144
|
+
/* apps/s3pweb/src/index.css */
|
|
145
|
+
@import 'tailwindcss';
|
|
146
|
+
@import 'tw-animate-css';
|
|
147
|
+
|
|
148
|
+
@source "../../../packages/shell-ui/src/**/*.{ts,tsx}";
|
|
149
|
+
/* ...other @source dirs... */
|
|
150
|
+
|
|
151
|
+
@import '@s3pweb/shared-ui/themes/base.css';
|
|
152
|
+
@import './theme/s3pweb.css';
|
|
153
|
+
@import '@s3pweb/shell-ui/themes/s3pweb.css';
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`MainLayout` reads `collapsed`, `navMode`, `theme`, `expandedNavGroups` from
|
|
157
|
+
`useUIStore`, maps the app's `NavEntry` (with React Router metadata) into
|
|
158
|
+
shell-ui's `NavEntry`, and renders `<Shell>` with a router-aware
|
|
159
|
+
`onItemSelect={(item) => navigate(item.href)}`.
|
|
160
|
+
|
|
161
|
+
## Brand themes
|
|
162
|
+
|
|
163
|
+
Two pre-built brand themes ship with the package. They redefine the neutral
|
|
164
|
+
tokens and add per-module decoration (icon chips, active-item accent line/dot).
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
@import '@s3pweb/shell-ui/themes/s3pweb.css';
|
|
168
|
+
/* or */
|
|
169
|
+
@import '@s3pweb/shell-ui/themes/aftral.css';
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Themes are scoped under `[data-shell-theme="s3pweb"]` (or `"aftral"`), so
|
|
173
|
+
multiple brands can coexist in the same document. Activate one by setting the
|
|
174
|
+
attribute on `<html>` or any ancestor:
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<html data-shell-theme="s3pweb">
|
|
178
|
+
...
|
|
179
|
+
</html>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Combine with the standard `.dark` class for dark mode:
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<html class="dark" data-shell-theme="s3pweb">
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
To roll your own theme, override the tokens in `:root` (or a scoped selector):
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
:root {
|
|
192
|
+
--color-primary: oklch(0.3 0.1 240);
|
|
193
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
194
|
+
--color-cta: oklch(0.85 0.15 90);
|
|
195
|
+
--color-sidebar: oklch(0.97 0 0);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Per-module decoration (`slug`)
|
|
200
|
+
|
|
201
|
+
Every `NavEntry` is rendered with `data-shell-ui-nav-slug="..."` on three
|
|
202
|
+
surfaces — the sidebar group, the sidebar flat leaf, and the top-bar button —
|
|
203
|
+
so brand CSS can paint a module-specific chip in all three layouts with a
|
|
204
|
+
single rule.
|
|
205
|
+
|
|
206
|
+
**`slug` defaults to `id`** — only set it explicitly when you need a
|
|
207
|
+
CSS-friendly theme key that's different from your id (e.g. your `id` is a
|
|
208
|
+
route path like `/incidents` or a UUID like `inc-1234`):
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
{ id: '/incidents', slug: 'incidents', label: 'Incidents', icon: <Bell />, items: [...] }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Canonical palette rule
|
|
215
|
+
|
|
216
|
+
To paint one slug, list all three selectors in a single rule (they share
|
|
217
|
+
declarations). The `:not([data-shell-ui-nav-children] *)` clause excludes
|
|
218
|
+
sub-items inside an expanded group so only top-level items get the chip.
|
|
219
|
+
|
|
220
|
+
```css
|
|
221
|
+
[data-shell-theme='s3pweb'] [data-shell-ui-nav-slug='foo'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon],
|
|
222
|
+
[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],
|
|
223
|
+
[data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button][data-shell-ui-nav-slug='foo'] {
|
|
224
|
+
background: var(--color-emerald-100);
|
|
225
|
+
color: var(--color-emerald-700);
|
|
226
|
+
}
|
|
227
|
+
.dark [data-shell-theme='s3pweb'] [data-shell-ui-nav-slug='foo'] [data-shell-ui-nav-group-button] [data-shell-ui-nav-icon],
|
|
228
|
+
.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],
|
|
229
|
+
.dark [data-shell-theme='s3pweb'] [data-shell-ui-top-bar-button][data-shell-ui-nav-slug='foo'] {
|
|
230
|
+
background: color-mix(in oklab, var(--color-emerald-700) 28%, transparent);
|
|
231
|
+
color: var(--color-emerald-300);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Shade tiers
|
|
236
|
+
|
|
237
|
+
Tailwind 4.2.1 ships every family as `--color-{family}-{shade}`. Pick the tier
|
|
238
|
+
that matches the family's chroma:
|
|
239
|
+
|
|
240
|
+
- **Saturated** (red, orange, amber, yellow, lime, green, emerald, teal, cyan,
|
|
241
|
+
sky, blue, indigo, violet, purple, fuchsia, pink, rose) —
|
|
242
|
+
light: `bg=*-100 / fg=*-700`; dark: `bg=color-mix(*-700 28%, transparent) / fg=*-300`.
|
|
243
|
+
- **Desaturated** (slate, gray, zinc, neutral, stone, olive) —
|
|
244
|
+
light: `bg=*-200 / fg=*-500`; dark: `bg=color-mix(*-500 30%, transparent) / fg=*-300`.
|
|
245
|
+
Their `*-700` reads almost black against a `*-100` bg, so the tier shifts.
|
|
246
|
+
- **One-step darker** (when two slugs share a family and you want them distinct) —
|
|
247
|
+
light: `bg=*-300 / fg=*-800`; dark: `bg=color-mix(*-800 36%, transparent) / fg=*-300`.
|
|
248
|
+
|
|
249
|
+
The shipped `themes/s3pweb.css` keeps the per-slug palette block empty by
|
|
250
|
+
default (every chip neutral) — see the long comment in that file for a
|
|
251
|
+
copy-paste-ready template; uncomment and add rules per module as needed.
|
|
252
|
+
|
|
253
|
+
## Slots & extras
|
|
254
|
+
|
|
255
|
+
| Prop | Where | Use case |
|
|
256
|
+
|--------------------------|-----------------------------|---------------------------------------|
|
|
257
|
+
| `logo` / `collapsedLogo` | Sidebar top / top-bar left | Brand mark |
|
|
258
|
+
| `topSlot` | Sidebar, above the nav list | Entity selector, search box |
|
|
259
|
+
| `userSection` | Sidebar footer | Custom user widget (replaces default) |
|
|
260
|
+
| `footer` / `footerExtra` | Sidebar footer | Status pill, secondary CTA |
|
|
261
|
+
| `bgClassName` | Sidebar `<aside>` / `<header>` | `bg-white`, `bg-sidebar`, etc. |
|
|
262
|
+
| `mainClassName` | The `<Outlet />` wrapper | Padding, scroll behavior |
|
|
263
|
+
| `renderCollapsedTooltip` | Collapsed sidebar tooltip | Override default tooltip content |
|
|
264
|
+
|
|
265
|
+
## Locale
|
|
266
|
+
|
|
267
|
+
Built-in button labels (Collapse / Expand / Logout / Dark mode / etc.) are
|
|
268
|
+
embedded in the package. Pass `locale="fr"` (default) or `locale="en"` — no
|
|
269
|
+
i18n provider needed. The strings are exposed via `getShellStrings(locale)`
|
|
270
|
+
for advanced use.
|
|
271
|
+
|
|
272
|
+
## Storybook
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
pnpm --filter @s3pweb/shell-ui storybook # dev server on :6006
|
|
276
|
+
pnpm --filter @s3pweb/shell-ui build-storybook # static build to ./storybook-static
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Three stories ship out of the box:
|
|
280
|
+
|
|
281
|
+
- **Shells / S3pweb** — replicates the s3pweb SaaS nav (Carte, Messagerie,
|
|
282
|
+
Éco-conduite, Maintenance, Social, Incidents, Prise de poste, Km/Carburant,
|
|
283
|
+
Nibelis).
|
|
284
|
+
- **Shells / Aftral** — replicates the AFTRAL nav (Eco-driving + Coaching, plus
|
|
285
|
+
the Administration group for non-AFTRAL users).
|
|
286
|
+
- **Playground / Configurable** — generic shell with Storybook Controls for
|
|
287
|
+
theme, locale, badges, groups, user/logo/toggles visibility. Events stream
|
|
288
|
+
to the Actions panel.
|
|
289
|
+
|
|
290
|
+
Switch between light/dark mode via the Storybook toolbar.
|
|
291
|
+
|
|
292
|
+
## Data types
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
interface NavItem {
|
|
296
|
+
id: string;
|
|
297
|
+
label: string;
|
|
298
|
+
icon?: ReactNode;
|
|
299
|
+
active?: boolean;
|
|
300
|
+
badge?: ReactNode;
|
|
301
|
+
href?: string; // Renders as <a href> for native browser semantics
|
|
302
|
+
meta?: unknown; // Free-form payload, passed back via onItemSelect
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface NavGroup {
|
|
306
|
+
id: string;
|
|
307
|
+
label: string;
|
|
308
|
+
icon?: ReactNode;
|
|
309
|
+
items: NavItem[];
|
|
310
|
+
active?: boolean;
|
|
311
|
+
expanded?: boolean;
|
|
312
|
+
slug?: string; // data-shell-ui-nav-slug for per-module theming
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type NavEntry = NavItem | NavGroup;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Publishing externally
|
|
319
|
+
|
|
320
|
+
This package is `"private": true` today. To publish to a registry:
|
|
321
|
+
|
|
322
|
+
1. Build to `dist/` (e.g. `tsup --dts --format esm`)
|
|
323
|
+
2. Set `"private": false` + bump `"version"`
|
|
324
|
+
3. Update `"exports"` to point at `./dist/*` instead of `./src/*`
|
|
325
|
+
4. Configure `publishConfig` for your registry
|
|
326
|
+
5. `pnpm publish`
|
|
327
|
+
|
|
328
|
+
The current `"exports"` entries:
|
|
329
|
+
|
|
330
|
+
```jsonc
|
|
331
|
+
{
|
|
332
|
+
".": "./src/index.ts",
|
|
333
|
+
"./preset.css": "./src/preset.css",
|
|
334
|
+
"./themes/s3pweb.css": "./src/themes/s3pweb.css",
|
|
335
|
+
"./themes/aftral.css": "./src/themes/aftral.css"
|
|
336
|
+
}
|
|
337
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ShellAction } from '../types';
|
|
3
|
+
export interface UseActionsMenuOptions {
|
|
4
|
+
/** The full set of actions the user can pin / unpin. */
|
|
5
|
+
actions: ShellAction[];
|
|
6
|
+
/** localStorage key for the pinned id set. Default `'shell-ui:pinnedActions'`. */
|
|
7
|
+
storageKey?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Action IDs pinned by default — used the first time the hook mounts
|
|
10
|
+
* (no localStorage entry yet). Default: every action ID (all pinned).
|
|
11
|
+
*/
|
|
12
|
+
defaultPinned?: string[];
|
|
13
|
+
/** Menu trigger label / panel heading. Default `'Actions'`. */
|
|
14
|
+
menuLabel?: string;
|
|
15
|
+
/** Menu trigger placement in the chrome cluster. Default `'trailing'`. */
|
|
16
|
+
menuPlacement?: 'leading' | 'trailing';
|
|
17
|
+
/** Draw a divider after the menu trigger. Default false. */
|
|
18
|
+
menuDividerAfter?: boolean;
|
|
19
|
+
/** Override the menu trigger icon. Default lucide `Puzzle`. */
|
|
20
|
+
menuIcon?: ReactNode;
|
|
21
|
+
/** Override the menu trigger's action id. Default `'__actions-menu__'`. */
|
|
22
|
+
menuId?: string;
|
|
23
|
+
/** Labels for the pin toggle button (i18n). */
|
|
24
|
+
pinLabel?: string;
|
|
25
|
+
unpinLabel?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface UseActionsMenuReturn {
|
|
28
|
+
/** Pass straight to `<Shell actions={...}>`: pinned subset + the menu trigger appended. */
|
|
29
|
+
visibleActions: ShellAction[];
|
|
30
|
+
/** Set of currently-pinned action IDs. */
|
|
31
|
+
pinnedIds: Set<string>;
|
|
32
|
+
/** Pin / unpin imperatively (alongside the panel's pin buttons). */
|
|
33
|
+
togglePin: (id: string) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare function useActionsMenu({ actions, storageKey, defaultPinned, menuLabel, menuPlacement, menuDividerAfter, menuIcon, menuId, pinLabel, unpinLabel, }: UseActionsMenuOptions): UseActionsMenuReturn;
|
|
36
|
+
export interface ActionsMenuPanelProps {
|
|
37
|
+
actions: ShellAction[];
|
|
38
|
+
pinnedIds: Set<string>;
|
|
39
|
+
onTogglePin: (id: string) => void;
|
|
40
|
+
title?: string;
|
|
41
|
+
pinLabel?: string;
|
|
42
|
+
unpinLabel?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function ActionsMenuPanel({ actions, pinnedIds, onTogglePin, title, pinLabel, unpinLabel, className }: ActionsMenuPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Button — shadcn/ui Button installed via `npx shadcn@latest add button`,
|
|
5
|
+
* customized with the s3pweb design-system values (rounded-sm, font-semibold,
|
|
6
|
+
* 38px icon size, brand-tuned ghost hover) so the shell's top-bar / footer
|
|
7
|
+
* buttons render identically to the live SaaS shell.
|
|
8
|
+
*/
|
|
9
|
+
declare const buttonVariants: (props?: ({
|
|
10
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
|
|
11
|
+
size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
12
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
13
|
+
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & {
|
|
14
|
+
asChild?: boolean;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Partner } from './partner-cluster';
|
|
2
|
+
/**
|
|
3
|
+
* EcosystemMegaPanel — the "L'écosystème …" popover panel from the partners
|
|
4
|
+
* maquette, ported to React. Designed to live inside a Radix Popover (passed
|
|
5
|
+
* via `PartnerCluster.popoverContent` or `ShellAction.clickContent`), with
|
|
6
|
+
* panel-internal links wrapped in `<PopoverClose asChild>` so a click both
|
|
7
|
+
* dismisses the popover and opens the partner site in a new tab.
|
|
8
|
+
*
|
|
9
|
+
* Built-in fr/en strings, override any of them via the explicit props. Any
|
|
10
|
+
* Partner with an `href` opens that URL in a new tab; partners without `href`
|
|
11
|
+
* still render but their link is non-functional (href='#').
|
|
12
|
+
*/
|
|
13
|
+
export interface EcosystemMegaPanelProps {
|
|
14
|
+
/** Partners to render. */
|
|
15
|
+
partners: Partner[];
|
|
16
|
+
/** Heading line. Default: 'L'écosystème' / 'The ecosystem'. */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Sub-heading paragraph. Default: localised generic. */
|
|
19
|
+
subtitle?: string;
|
|
20
|
+
/** Footer-left label. Default: '{N} services connectés à votre compte'. */
|
|
21
|
+
connectedLabel?: string;
|
|
22
|
+
/** Footer-right action label. Default: 'Tout découvrir →' / 'Discover all →'. */
|
|
23
|
+
seeAllLabel?: string;
|
|
24
|
+
/** When set, the "see all" footer item renders as an `<a target='_blank' href>`. */
|
|
25
|
+
seeAllHref?: string;
|
|
26
|
+
/** Click callback for the "see all" footer (fires alongside href navigation if both are set). */
|
|
27
|
+
onSeeAllClick?: () => void;
|
|
28
|
+
/** Click callback when a partner row is clicked. Fires alongside href navigation. */
|
|
29
|
+
onPartnerClick?: (partner: Partner) => void;
|
|
30
|
+
/** Show the 'Nous' / 'Part.' kind tag next to each name. Default true. */
|
|
31
|
+
showKindTag?: boolean;
|
|
32
|
+
/** Locale for built-in strings. Default 'fr'. */
|
|
33
|
+
locale?: 'fr' | 'en';
|
|
34
|
+
/** Extra Tailwind classes on the panel root (override width / padding). */
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function EcosystemMegaPanel({ partners, title, subtitle, connectedLabel, seeAllLabel, seeAllHref, onSeeAllClick, onPartnerClick, showKindTag, locale, className, }: EcosystemMegaPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HoverCard as HoverCardPrimitive } from 'radix-ui';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* HoverCard — shadcn-style wrapper around Radix HoverCard, mirroring the
|
|
5
|
+
* brand-themed Tooltip in `./tooltip.tsx`. Use it for any rich-content popup
|
|
6
|
+
* that should open on hover (e.g. a partners list, user-profile preview,
|
|
7
|
+
* avatar tooltip with metadata). When you need keyboard-equivalent click
|
|
8
|
+
* semantics or a focus-trapped menu, reach for DropdownMenu instead.
|
|
9
|
+
*
|
|
10
|
+
* Defaults: openDelay 200ms / closeDelay 100ms — felt right against the
|
|
11
|
+
* existing Tooltip's instant open. Override per-instance when needed.
|
|
12
|
+
*/
|
|
13
|
+
declare function HoverCard({ openDelay, closeDelay, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
declare function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare function HoverCardContent({ className, align, sideOffset, children, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutSwitcher — ports the `s3p-app-header--layoutButton` from the s3pweb
|
|
3
|
+
* Stencil app into shell-ui as a **stateless, click-emitting** button. The
|
|
4
|
+
* consumer owns the layout state (typically a `useState` persisted to
|
|
5
|
+
* `localStorage`), and is responsible for opening whatever picker modal /
|
|
6
|
+
* drawer wires up to the click.
|
|
7
|
+
*
|
|
8
|
+
* If the consumer wants the keyboard-shortcut behaviour from the original
|
|
9
|
+
* (pressing 1-9 / AZERTY `& é " ' ( - è _ ç` to jump to a layout), they wire
|
|
10
|
+
* a `window.addEventListener('keydown')` themselves — the component does NOT
|
|
11
|
+
* attach any global listener.
|
|
12
|
+
*
|
|
13
|
+
* Use the `variant` prop to render:
|
|
14
|
+
* - **compact** (default, top-bar): 38×38 ghost-button with the
|
|
15
|
+
* `LayoutGrid` icon + a small primary-coloured badge in the
|
|
16
|
+
* bottom-right corner showing the current value.
|
|
17
|
+
* - **row** (expanded sidebar): full-width footer row matching the other
|
|
18
|
+
* sidebar toggles — icon + label + badge on the right.
|
|
19
|
+
*
|
|
20
|
+
* For collapsed-sidebar mode (where shell-ui falls back to the standard
|
|
21
|
+
* icon button), use `<LayoutNumberIcon>` as the action's `icon` so the
|
|
22
|
+
* number badge is still visible at 16px.
|
|
23
|
+
*/
|
|
24
|
+
export interface LayoutSwitcherProps {
|
|
25
|
+
/** Current layout value (typically 1-9). Rendered as the badge. */
|
|
26
|
+
value: number;
|
|
27
|
+
/** Fires when the trigger is clicked. Consumer opens its picker / modal here. */
|
|
28
|
+
onClick?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Visual variant:
|
|
31
|
+
* - 'compact' (default): 38×38 ghost-button, icon + corner badge.
|
|
32
|
+
* - 'row': h-8 full-width row, icon + label + trailing badge.
|
|
33
|
+
*/
|
|
34
|
+
variant?: 'compact' | 'row';
|
|
35
|
+
/** Title (native tooltip + aria-label prefix). Default 'Gérer les dispositions'. */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** Label shown in the `row` variant. Default = `title`. */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Extra Tailwind classes on the root button. */
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function LayoutSwitcher({ value, onClick, variant, title, label, className }: LayoutSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* Icon-slot variant of LayoutSwitcher: a 16×16 `LayoutGrid` with a small
|
|
45
|
+
* primary-coloured number badge overflowing the bottom-right corner. Use
|
|
46
|
+
* as `ShellAction.icon` when the action's `customTrigger` is skipped (e.g.
|
|
47
|
+
* in the collapsed-sidebar fallback) but you still want the badge visible.
|
|
48
|
+
*/
|
|
49
|
+
export declare function LayoutNumberIcon({ value, className }: {
|
|
50
|
+
value: number;
|
|
51
|
+
className?: string;
|
|
52
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Partner-cluster widget — replicates the "Nos solutions" header pattern from
|
|
4
|
+
* the s3pweb partners maquette (claude-maquette-partners). Use it as the
|
|
5
|
+
* trigger of a `ShellAction` with `clickContent` / `hoverContent` set, e.g.:
|
|
6
|
+
*
|
|
7
|
+
* { id: 'solutions', label: 'Nos solutions',
|
|
8
|
+
* icon: <Layers />, // sidebar fallback
|
|
9
|
+
* customTrigger: <PartnerCluster partners={ECOSYSTEM} chipBorderColor='white' />,
|
|
10
|
+
* clickContent: <YourMegaPanel /> }
|
|
11
|
+
*
|
|
12
|
+
* The cluster paints a small label, the first N partner tiles overlapped
|
|
13
|
+
* (-7px stride, ringed with `chipBorderColor` to mask the overlap against the
|
|
14
|
+
* chrome bg), and a "+M" overflow chip when there are more partners than
|
|
15
|
+
* `maxVisible`. Each tile uses a 16% tint of the partner's brand colour for
|
|
16
|
+
* its bg and the full brand colour for the mark stroke.
|
|
17
|
+
*/
|
|
18
|
+
export interface Partner {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** Brand hex colour (e.g. '#2E6BE6'). */
|
|
22
|
+
color: string;
|
|
23
|
+
/**
|
|
24
|
+
* Brand mark — rendered inside the tile as-is. Pass any ReactNode:
|
|
25
|
+
* - SVG paths wrapped via `svgMark(...)` (or your own `<svg>` element)
|
|
26
|
+
* - `<img src='/favicons/x.png' alt='X' />` for raster brand icons
|
|
27
|
+
* - any custom component
|
|
28
|
+
*
|
|
29
|
+
* The tile constrains the mark to ~60% of its width/height; any inner
|
|
30
|
+
* `<svg>` or `<img>` automatically fills that slot.
|
|
31
|
+
*/
|
|
32
|
+
mark: React.ReactNode;
|
|
33
|
+
/** Optional one-line tagline shown in detail / popover layouts. */
|
|
34
|
+
tagline?: string;
|
|
35
|
+
/** Optional category — free-form (e.g. 'Financement', 'Optimisation'). */
|
|
36
|
+
category?: string;
|
|
37
|
+
/** Optional taxonomy tag — 'solution' = ours, 'partner' = external. */
|
|
38
|
+
kind?: 'solution' | 'partner';
|
|
39
|
+
/** Optional URL — handy for popover content that links to the partner site. */
|
|
40
|
+
href?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Mix a hex colour toward white by `ratio` (0 = white, 1 = full colour).
|
|
44
|
+
* Mirrors `ECO.tint()` in the maquette's ecosystem.js. Returns an rgb() string.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tintColor(hex: string, ratio?: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Convenience helper for the most common Partner.mark case: raw SVG path
|
|
49
|
+
* content lifted from a design source. Wraps the paths in a 24×24 viewBox
|
|
50
|
+
* with `stroke=currentColor` so the brand colour cascades from the tile.
|
|
51
|
+
*
|
|
52
|
+
* mark: svgMark(<><circle cx='12' cy='12' r='8'/><path d='...'/></>)
|
|
53
|
+
*/
|
|
54
|
+
export declare function svgMark(paths: React.ReactNode, opts?: {
|
|
55
|
+
strokeWidth?: number;
|
|
56
|
+
}): React.ReactNode;
|
|
57
|
+
export interface PartnerTileProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
58
|
+
partner: Partner;
|
|
59
|
+
/** Outer square edge in px. Default 30. */
|
|
60
|
+
size?: number;
|
|
61
|
+
/** Solid mode: bg=brand colour, glyph=white. Default tinted bg + brand glyph. */
|
|
62
|
+
solid?: boolean;
|
|
63
|
+
/** Border-radius in px. Defaults to ~28% of `size`. */
|
|
64
|
+
radius?: number;
|
|
65
|
+
/** Optional 2px ring colour painted around the tile (used inside PartnerCluster). */
|
|
66
|
+
ringColor?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function PartnerTile({ partner, size, solid, radius, ringColor, className, style, title, ...props }: PartnerTileProps): import("react/jsx-runtime").JSX.Element;
|
|
69
|
+
export interface PartnerClusterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
70
|
+
partners: Partner[];
|
|
71
|
+
/** Tiles shown before the +N overflow chip. Default 4. */
|
|
72
|
+
maxVisible?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Small label shown to the left of the tiles. Default: no label.
|
|
75
|
+
* Pass a string (e.g. 'Nos solutions') to display one.
|
|
76
|
+
*/
|
|
77
|
+
label?: string | null;
|
|
78
|
+
/** Tile edge in px. Default 30. */
|
|
79
|
+
tileSize?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Colour painted on the 2px ring around each tile / the +N chip to mask the
|
|
82
|
+
* -7px overlap against the chrome's background. Should match the
|
|
83
|
+
* surrounding bg — defaults to `var(--color-sidebar)`. Pass `'white'` (or
|
|
84
|
+
* a Tailwind colour variable) when the chrome uses a different bg.
|
|
85
|
+
*/
|
|
86
|
+
chipBorderColor?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Content rendered inside the click-Popover anchored to the +N overflow
|
|
89
|
+
* chip. When the cluster has no overflow (maxVisible >= partners.length)
|
|
90
|
+
* the popover won't have a trigger — keep `maxVisible` below
|
|
91
|
+
* `partners.length` if you rely on this.
|
|
92
|
+
*/
|
|
93
|
+
popoverContent?: React.ReactNode;
|
|
94
|
+
/** Popover side. Default 'bottom' (top-bar context). Use 'right' when the cluster sits in a sidebar. */
|
|
95
|
+
popoverSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
96
|
+
/** Popover align. Default 'end'. */
|
|
97
|
+
popoverAlign?: 'start' | 'center' | 'end';
|
|
98
|
+
/** Popover offset from the trigger in px. Default 8. */
|
|
99
|
+
popoverSideOffset?: number;
|
|
100
|
+
/** Locale for built-in labels (the overflow chip title). Default 'fr'. */
|
|
101
|
+
locale?: 'fr' | 'en';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The cluster is NOT a single button. Each visible tile is an independent
|
|
105
|
+
* `<a target='_blank'>` when its partner has an `href` (otherwise a
|
|
106
|
+
* non-interactive `<span>`), and the `+N` chip is a separate `<button>` that
|
|
107
|
+
* opens `popoverContent` in a Popover. Effect: clicking a tile goes straight
|
|
108
|
+
* to that partner's site, clicking `+N` opens the full panel — matching the
|
|
109
|
+
* maquette's UX.
|
|
110
|
+
*/
|
|
111
|
+
export declare function PartnerCluster({ partners, maxVisible, label, tileSize, chipBorderColor, popoverContent, popoverSide, popoverAlign, popoverSideOffset, locale, className, ...props }: PartnerClusterProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from 'radix-ui';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Popover — shadcn-style wrapper around Radix Popover, mirroring the
|
|
5
|
+
* brand-themed Tooltip + HoverCard primitives. Use it for click-triggered
|
|
6
|
+
* rich-content popups (vs. HoverCard's hover semantics).
|
|
7
|
+
*/
|
|
8
|
+
declare function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
declare function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* Use to decouple the popover's positioning anchor from its click trigger.
|
|
12
|
+
* Typical case: a row of items where ONE small button opens the popover, but
|
|
13
|
+
* the popover should anchor to the whole row so it doesn't move when the
|
|
14
|
+
* trigger's `transform` changes on hover.
|
|
15
|
+
*/
|
|
16
|
+
declare function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a child element so clicking it closes the popover (Radix forwards the
|
|
19
|
+
* click + close to the underlying element via `asChild`). Use to make
|
|
20
|
+
* panel-internal links / buttons dismiss the popover before navigating.
|
|
21
|
+
*/
|
|
22
|
+
declare function PopoverClose({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Close>): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
declare function PopoverContent({ className, align, sideOffset, children, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export { Popover, PopoverTrigger, PopoverAnchor, PopoverClose, PopoverContent };
|