@cboxdk/cbox-ui 0.9.0 → 0.10.1
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 +183 -78
- package/dist/index.d.ts +468 -537
- package/dist/index.js +3782 -3324
- package/dist/index.js.map +1 -1
- package/package.json +11 -3
- package/tokens/cbox.css +3 -35
- package/tokens/shell.css +362 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Shared UI for the Cbox app family — id, cortex, atlas, track, and whatever's next.
|
|
4
4
|
|
|
5
|
-
This package gives every Cbox app the same
|
|
5
|
+
This package gives every Cbox app the same shell (`CboxShell`: rail, header with the context selectors, ⌘K palette, Help column, app switcher, account menu, phone layout) and the same page primitives (panels, headers, tabs, status pills), so moving between apps feels like one product. It also ships the design tokens (colours, fonts, radii) and the brand assets in one place.
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The shell renders one Inertia prop, `shell`, which the server builds (laravel-console-kit). The prop carries only the current selection and URLs; menus, the app catalog, Help and search load in the browser after the page has painted, from same-origin endpoints the app serves. See [The shell](#the-shell) and the spec in `docs/specs/2026-11-10-shell-v1.md`.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -29,61 +29,40 @@ The package is published on npm as `@cboxdk/cbox-ui` under the MIT License. The
|
|
|
29
29
|
|
|
30
30
|
That's it — every `text-foreground`, `bg-card`, `border-border`, `text-mono`, etc. utility now resolves against the Cbox palette. Light + dark variants come bundled: dark applies under `.dark` or `data-theme="dark"` on the root, whichever the host writes. Add `@source '../../node_modules/@cboxdk/cbox-ui/dist';` so Tailwind generates the utilities the components use, and when the host stores its theme choice as an attribute, point Tailwind's `dark:` variant at it (`@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));`).
|
|
31
31
|
|
|
32
|
-
### 2.
|
|
33
|
-
|
|
34
|
-
In **id itself** (which IS the source of truth), populate from Inertia shared props:
|
|
32
|
+
### 2. Render the shell in the persistent layout
|
|
35
33
|
|
|
36
34
|
```tsx
|
|
37
|
-
|
|
35
|
+
// resources/js/layouts/AppLayout.tsx
|
|
36
|
+
import { router, usePage, Link } from '@inertiajs/react';
|
|
37
|
+
import { CboxShell, type Shell } from '@cboxdk/cbox-ui';
|
|
38
38
|
|
|
39
|
-
export default function
|
|
39
|
+
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|
40
|
+
const { shell } = usePage<{ shell: Shell }>().props;
|
|
40
41
|
return (
|
|
41
|
-
<
|
|
42
|
+
<CboxShell
|
|
43
|
+
shell={shell}
|
|
44
|
+
LinkComponent={Link}
|
|
45
|
+
navigate={(url, o) => (o?.newTab ? window.open(url, '_blank', 'noopener') : router.visit(url, { method: o?.method, data: o?.data }))}
|
|
46
|
+
onPreferenceChange={(key, value) => router.post('/settings/shell', { [key]: value }, { preserveState: true, preserveScroll: true })}
|
|
47
|
+
>
|
|
42
48
|
{children}
|
|
43
|
-
</
|
|
49
|
+
</CboxShell>
|
|
44
50
|
);
|
|
45
51
|
}
|
|
46
|
-
```
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
import { CboxIdProvider } from '@cboxdk/cbox-ui';
|
|
52
|
-
import { useIdSession } from '@cboxdk/cbox-id-client';
|
|
53
|
-
|
|
54
|
-
export default function AppRoot({ children }) {
|
|
55
|
-
const session = useIdSession(); // {user, organizations, currentOrg, ...}
|
|
56
|
-
return (
|
|
57
|
-
<CboxIdProvider value={session.cboxIdContext}>
|
|
58
|
-
{children}
|
|
59
|
-
</CboxIdProvider>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
53
|
+
// Every page: `Page.layout = page => <AppLayout>{page}</AppLayout>`, so the rail, Help and the
|
|
54
|
+
// chat stay mounted between visits.
|
|
62
55
|
```
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `apps` — sister apps for the launcher
|
|
70
|
-
- `urls` — `{idBaseUrl, accountUrl, orgSettingsUrl, switchOrgUrl, signOutUrl}`
|
|
71
|
-
- `notifications?` — `{unreadCount, inboxUrl}` (optional, hides the bell when missing)
|
|
72
|
-
- `onSwitchOrganization?` — callback when the user picks an org (default: form-POST to `urls.switchOrgUrl`)
|
|
73
|
-
|
|
74
|
-
### 3. Drop in the chrome
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { TopBar } from '@cboxdk/cbox-ui';
|
|
57
|
+
Set the theme before the first paint: the server writes `class="dark"` and `data-theme` on
|
|
58
|
+
`<html>` from `shell.preferences.theme`, or the head carries the inline script
|
|
59
|
+
`themeBootScript(theme)` returns (it resolves `system` from the device without saving it).
|
|
60
|
+
Copy `public/brand/` (the Cbox wordmark: `cbox-logo.png`, `cbox-logo-white.png` and their `@2x`)
|
|
61
|
+
as before; `brandAssetBase` changes the path.
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
brand={<MyAppLogo />}
|
|
81
|
-
centre={<MySearchBar />}
|
|
82
|
-
showOrgSwitcher
|
|
83
|
-
/>
|
|
84
|
-
```
|
|
63
|
+
### 3. Identity context (inbox only)
|
|
85
64
|
|
|
86
|
-
|
|
65
|
+
`<CboxIdProvider>` still feeds the inbox surface (`CboxSurfaces`). The shell does not read it.
|
|
87
66
|
|
|
88
67
|
### 4. Use page primitives
|
|
89
68
|
|
|
@@ -106,16 +85,15 @@ Match id's existing patterns — see id/resources/js/pages/* for examples.
|
|
|
106
85
|
|
|
107
86
|
## Components
|
|
108
87
|
|
|
109
|
-
###
|
|
88
|
+
### Shell
|
|
110
89
|
|
|
111
|
-
|
|
|
90
|
+
| Export | What it does |
|
|
112
91
|
|---|---|
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `CboxAppCatalog` | The
|
|
118
|
-
| `NotificationsBell` | Bell icon with unread badge, links to id's unified inbox. Renders nothing if context lacks `notifications`. |
|
|
92
|
+
| `CboxShell` | The one shell for every app, rendered from the `shell` prop. See [The shell](#the-shell). |
|
|
93
|
+
| `Shell`, `ContextLevel`, `ContextItem`, `NavArea`, `PaletteAction`, … | The prop's TypeScript contract (spec §11.1). laravel-console-kit produces it. |
|
|
94
|
+
| `useShellHelp()` | `toggleHelp`, `openArticle`, `ask` for a page's own `?` button. |
|
|
95
|
+
| `applyTheme`, `themeBootScript` | Put a theme choice on the document the way the tokens read it. |
|
|
96
|
+
| `CboxAppCatalog` | The framework-free `<cbox-app-switcher>` element in React. Kept for apps that have not moved to the shell yet. |
|
|
119
97
|
|
|
120
98
|
### Page primitives
|
|
121
99
|
|
|
@@ -175,7 +153,7 @@ no provider is present. The host owns the selected language and its catalogs:
|
|
|
175
153
|
|
|
176
154
|
```tsx
|
|
177
155
|
<CboxLocalizationProvider locale={language} translate={t}>
|
|
178
|
-
<
|
|
156
|
+
<CboxShell shell={shell}>{children}</CboxShell>
|
|
179
157
|
</CboxLocalizationProvider>
|
|
180
158
|
```
|
|
181
159
|
|
|
@@ -193,6 +171,8 @@ callback; without it, existing browser formatting is preserved.
|
|
|
193
171
|
|
|
194
172
|
### App catalog (Switch app)
|
|
195
173
|
|
|
174
|
+
The shell's own app grid reads the same endpoint (see [The shell](#the-shell)). The element below stays for apps that have not moved to the shell yet.
|
|
175
|
+
|
|
196
176
|
Every Cbox app, Blade or React, draws the same Switch-app control: the `<cbox-app-switcher>`
|
|
197
177
|
custom element from `@cboxdk/cbox-ui/app-switcher`. It has no React dependency and renders
|
|
198
178
|
in a shadow root with its own styles, so no app stylesheet (a reset, a missing preflight, a
|
|
@@ -270,30 +250,155 @@ In React, `<CboxServiceMark appKey="infra" />` and the collapsed `CboxBrand` rea
|
|
|
270
250
|
kept catalog; `CboxAppIcon` renders any single app's icon, and a host that loads the catalog
|
|
271
251
|
itself calls `rememberAppCatalog(apps)`.
|
|
272
252
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
253
|
+
## The shell
|
|
254
|
+
|
|
255
|
+
`CboxShell` is the one shell for every Cbox app (spec: `docs/specs/2026-11-10-shell-v1.md`).
|
|
256
|
+
The app passes the `shell` prop and the page; the shell does the rest.
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
┌──────┬────────────────────────────────────────────────────────────────────┬──────────┐
|
|
260
|
+
│ rail │ [C] Cbox ⇕ / [W] Webshop · ■ Production ⇕ [Search or ask… ⌘K] [Help²] [🔔] [⣿] │ Help │
|
|
261
|
+
│ ├────────────────────────────────────────────────────────────────────┤ (docked │
|
|
262
|
+
│ areas│ sandbox strip (only while the environment has test data) │ ≥1280px) │
|
|
263
|
+
│ … │ subnav │ page │ │
|
|
264
|
+
│ [SD] │ │ │ │
|
|
265
|
+
└──────┴────────┴───────────────────────────────────────────────────────────┴──────────┘
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### The `shell` prop
|
|
269
|
+
|
|
270
|
+
The types are exported (`Shell`, `ContextLevel`, `ContextItem`, `NavArea`, `NavPage`,
|
|
271
|
+
`PaletteAction`, `ShellHelp`, `ShellPreferences`, …) and are the contract laravel-console-kit
|
|
272
|
+
produces. Abridged:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
type Shell = {
|
|
276
|
+
app: { key; name; homeUrl };
|
|
277
|
+
person: { id; name; initials; avatarUrl? };
|
|
278
|
+
page?: { title; route? }; // "On this page" and the chat's page intent
|
|
279
|
+
nav: { areas: NavArea[]; activeArea; activePage? };
|
|
280
|
+
context: { levels: ContextLevel[]; switchUrl }; // 1–3 levels, organization first
|
|
281
|
+
palette: { searchUrl?; gotoUrl?; actions: PaletteAction[]; placeholder? };
|
|
282
|
+
help: { enabled; topic?; sessionUrl?; scriptUrl?; portalUrl?; updatesUrl?; searchUrl?; articlesUrl? };
|
|
283
|
+
notifications: { enabled: false } | { enabled: true; unread; url };
|
|
284
|
+
apps: { catalogUrl; productsUrl? };
|
|
285
|
+
account?: { profileUrl?; switchAccountUrl?; signOutUrl?; signOutMethod?; languages?; formats?; signedInWith? };
|
|
286
|
+
banner?: { kind: 'sandbox' | 'impersonation' | 'notice'; text; action? };
|
|
287
|
+
preferences: { theme; locale; railPinned; helpOpen; subnavCollapsed?; regionalFormat?; timeZone? };
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
type NavArea = { key; label; url; icon?; pages?: NavPage[] };
|
|
293
|
+
type NavPage = {
|
|
294
|
+
key; label; url; icon?;
|
|
295
|
+
locked?: boolean; // not in the plan: a lock and "(locked)", `url` leads to the upgrade prompt
|
|
296
|
+
count?: number; // a badge (open drafts); hidden at 0
|
|
297
|
+
group?: string; // a heading in the subnav and the phone nav ("Organization", "Project")
|
|
298
|
+
};
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Pages of one `group` show together under its heading, in the order each group first appears.
|
|
302
|
+
Locked pages still show in the palette's Pages, marked "Locked". Every nav link (and the
|
|
303
|
+
banner action) also accepts `href` in place of `url`, the name laravel-console-kit 0.2.2 sends.
|
|
304
|
+
`palette.placeholder` replaces "Search or ask…" in the palette's field (the header button keeps
|
|
305
|
+
"Search or ask…").
|
|
306
|
+
|
|
307
|
+
`account` and each of its URLs are optional: the account menu leaves out a row it has no URL
|
|
308
|
+
for, and without `account` it holds only the preferences (laravel-console-kit's
|
|
309
|
+
`->account()` builds it).
|
|
310
|
+
|
|
311
|
+
The shell's first focusable element is a "Skip to content" link, hidden until focused. It
|
|
312
|
+
moves focus to `<main id={mainId}>` (default `main-content`).
|
|
313
|
+
|
|
314
|
+
`ContextLevel.settingsUrl` is a URL template on the wire: `{id}` is replaced with the row's id
|
|
315
|
+
(`/orgs/{id}/settings`). Nav and action icons are Lucide names (`"rocket"`, `"server"`); pass
|
|
316
|
+
`icons` to `CboxShell` for others.
|
|
317
|
+
|
|
318
|
+
### What the app serves (all same-origin JSON)
|
|
319
|
+
|
|
320
|
+
| URL | Request | Answer |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `level.listUrl` | `GET ?q=&parent=&cursor=&limit=` | `{ object: "list", data: ContextItem[], has_more, next_cursor }`. Loaded when a menu opens; every level below the first gets `parent`, the level above's id (projects of an organization, environments of a project). Answers are kept for the page and dropped when the context changes. |
|
|
323
|
+
| `level.createUrl` | `POST { name, parent? }` | `{ id, detailsUrl }` (or `details_url`). The person lands on `detailsUrl`. A 422 shows its first error under the field. |
|
|
324
|
+
| `account.switchAccountUrl` | A plain link (full page load) | Usually a redirect to Cbox ID's account chooser. |
|
|
325
|
+
| `context.switchUrl` | `POST { <level key>: id, …, return_to }` (a form post through `navigate`) | A redirect: the same page in the new context, or that area's list for an entity page (§5.6). |
|
|
326
|
+
| `palette.searchUrl` | `GET ?q=` | `{ data: [{ id, title, subtitle?, url }] }`, in the current context. |
|
|
327
|
+
| `palette.gotoUrl` | `GET ?q=` | `{ data: [{ title, subtitle?, levels?: { organization, project, … }, url? }] }` across organizations. |
|
|
328
|
+
| `apps.catalogUrl` | `GET` | The catalog below, plus `in_organization`, `admission_url`, `purpose` and `hue` per app. |
|
|
329
|
+
| `help.updatesUrl` | `GET`; `POST {updatesUrl}/{id}/read` | `{ data: [{ id, kind: "ticket" \| "chat", title, excerpt?, at, unread, url? }] }` |
|
|
330
|
+
| `help.searchUrl` | `GET ?q=&topic=` | `{ data: [{ id, title, excerpt?, body?, url?, source? }] }` |
|
|
331
|
+
| `help.articlesUrl` | `GET` (template, `{topic}`) | The same shape; the first three are shown. |
|
|
332
|
+
|
|
333
|
+
Every link the shell follows from these answers (`url`, `detailsUrl`) must be `http(s)://` or
|
|
334
|
+
a `/path`; anything else is dropped. None of these may call another service synchronously (local first): organizations come from
|
|
335
|
+
the app's mirror of Cbox ID, the catalog from a cached copy. Help and docs search are proxies of
|
|
336
|
+
Cbox Assistant served by laravel-assistant-client; when Assistant is down, Help says so and
|
|
337
|
+
everything else works.
|
|
338
|
+
|
|
339
|
+
Requests send `X-XSRF-TOKEN` (from the `XSRF-TOKEN` cookie) or `X-CSRF-TOKEN` (from
|
|
340
|
+
`<meta name="csrf-token">`). Without a `navigate` prop, POSTs are real form submits with
|
|
341
|
+
`_token`.
|
|
342
|
+
|
|
343
|
+
### Preferences and first paint
|
|
344
|
+
|
|
345
|
+
The shell reports every preference change through `onPreferenceChange(key, value)`: `theme`,
|
|
346
|
+
`locale`, `regionalFormat`, `timeZone` (Cbox ID's shared preferences), and `railPinned`,
|
|
347
|
+
`helpOpen`, `subnavCollapsed` (the app's cookie, read on the server). Without the callback it
|
|
348
|
+
writes a `cbox_shell_<key>` cookie. The server sends them back in `shell.preferences`, so the
|
|
349
|
+
rail, Help and the subnav render in place on the server; `themeBootScript(theme)` (or the same
|
|
350
|
+
class and `data-theme` written by the server) sets the theme before paint.
|
|
351
|
+
|
|
352
|
+
### Help
|
|
353
|
+
|
|
354
|
+
Help is part of the shell: support updates (the button's count), docs and knowledge base
|
|
355
|
+
search, articles for the page's `help.topic`, Ask Cbox, Talk to a person and the support portal.
|
|
356
|
+
It docks beside the page from 1280px (360px, 400px from 1440px) and is a full-screen dialog
|
|
357
|
+
below. Its state (open, article, search, scroll, updates) lives in a store outside React, so it
|
|
358
|
+
survives visits and even a remount; the chat element sits outside the home/article view, so
|
|
359
|
+
opening an article never ends the conversation, and it survives as long as the shell sits in
|
|
360
|
+
the persistent layout. Help the person left open shows docked on load; below 1280px it waits
|
|
361
|
+
until they open it, so a phone or a narrow window never lands under the overlay. Support
|
|
362
|
+
updates are per organization: they load again when the organization or `updatesUrl` changes.
|
|
363
|
+
Closing Help gives focus back to what opened it.
|
|
364
|
+
|
|
365
|
+
The chat is the Assistant SDK's `<cbox-support session-url>` element (laravel-assistant-client,
|
|
366
|
+
`/ai-assistant/support.js` by default, loaded the first time Help opens). The shell sets
|
|
367
|
+
`element.intent = { route, title, context, url }` (and an `intent` JSON attribute) on every
|
|
368
|
+
page, and calls `element.ask(question, intent)` / `element.handover()` when the SDK has them,
|
|
369
|
+
else `element.open()`.
|
|
370
|
+
|
|
371
|
+
A page's own `?` button uses `useShellHelp()?.toggleHelp()`, or `<PageHeader help />`. The page's
|
|
372
|
+
help topic is `shell.help.topic`, set on the server per route (console-kit's `helpTopic`), not
|
|
373
|
+
a prop of the header: Help needs it on the first paint and on pages without a header.
|
|
374
|
+
|
|
375
|
+
### Keyboard
|
|
376
|
+
|
|
377
|
+
⌘K / Ctrl+K opens the palette anywhere (↑↓ move, Enter opens, ⌘Enter opens in a new tab, Esc
|
|
378
|
+
closes). In the context menus ↑↓ Home End move, Enter picks, → makes a row's gear the active
|
|
379
|
+
option (Enter opens its settings; in the cascade a second → crosses to the environments), ←
|
|
380
|
+
goes back, Esc closes and Tab leaves; focus returns to the trigger. ↓ past the last row
|
|
381
|
+
reaches "New …" and ↑ above the first reaches "View all", so both work without a mouse; in the
|
|
382
|
+
app grid End and ↓ from the bottom row reach "All products". ⌘. collapses the subnav. Search fields focus themselves only with a fine pointer and
|
|
383
|
+
hover, never on touch. Sheets and the Help overlay trap focus; the docked column does not.
|
|
384
|
+
|
|
385
|
+
### Phone (< 768px)
|
|
386
|
+
|
|
387
|
+
The header holds ☰ (areas, pages, Switch app, notifications, the account), one context button
|
|
388
|
+
(`Cbox / Webshop / Production`) that opens every level stacked, Search and Help (full screen).
|
|
389
|
+
Nothing is hidden without a replacement.
|
|
390
|
+
|
|
391
|
+
### Tests and snapshots
|
|
392
|
+
|
|
393
|
+
`bun run test` runs the unit tests (vitest, Testing Library, happy-dom). `bun run test:visual`
|
|
394
|
+
renders the fixture page in `tests/visual` (the real shell, the fixture `shell` prop and its
|
|
395
|
+
answers) and compares every open state at 1440 and 390, light and dark, against
|
|
396
|
+
`tests/visual/__screenshots__`, and runs axe on every open state. It runs in the pinned
|
|
397
|
+
Playwright container (`scripts/visual.sh`, needs Docker), the same one CI's `visual` job uses,
|
|
398
|
+
so the Linux baselines match pixel for pixel. After an intended change:
|
|
399
|
+
`bun run test:visual:update`, then look at every changed image.
|
|
400
|
+
|
|
401
|
+
### Tokens
|
|
297
402
|
|
|
298
403
|
Core tokens now match the warm cream/ink design-system colors. Body text uses
|
|
299
404
|
the system stack; hosts self-host Plus Jakarta Sans for headings and JetBrains
|