@cboxdk/cbox-ui 0.8.0 → 0.10.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 +229 -100
- package/dist/app-switcher.d.ts +19 -0
- package/dist/app-switcher.js +7 -0
- package/dist/app-switcher.js.map +1 -0
- package/dist/catalog-BroZWvws.d.ts +100 -0
- package/dist/chunk-AJCYF7SS.js +808 -0
- package/dist/chunk-AJCYF7SS.js.map +1 -0
- package/dist/index.d.ts +461 -607
- package/dist/index.js +3729 -3576
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
- package/tokens/cbox.css +3 -35
- package/tokens/shell.css +347 -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
|
-
|
|
48
|
-
In **cortex / atlas / track / future apps**, populate from the id-client SDK on session start:
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
import { CboxIdProvider } from '@cboxdk/cbox-ui';
|
|
52
|
-
import { useIdSession } from '@cboxdk/cbox-id-client';
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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.
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
- `currentOrganization` — current org context (or null on user-only surfaces)
|
|
68
|
-
- `organizations` — every org the user belongs to (drives the switcher)
|
|
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`)
|
|
63
|
+
### 3. Identity context (inbox only)
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
import { TopBar } from '@cboxdk/cbox-ui';
|
|
78
|
-
|
|
79
|
-
<TopBar
|
|
80
|
-
brand={<MyAppLogo />}
|
|
81
|
-
centre={<MySearchBar />}
|
|
82
|
-
showOrgSwitcher
|
|
83
|
-
/>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
The default right-cluster is `<AppLauncher>` → `<NotificationsBell>` → `<UserMenu>`. Override with `rightCluster={...}` if a particular app needs a different mix.
|
|
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,60 +171,211 @@ callback; without it, existing browser formatting is preserved.
|
|
|
193
171
|
|
|
194
172
|
### App catalog (Switch app)
|
|
195
173
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
|
|
176
|
+
Every Cbox app, Blade or React, draws the same Switch-app control: the `<cbox-app-switcher>`
|
|
177
|
+
custom element from `@cboxdk/cbox-ui/app-switcher`. It has no React dependency and renders
|
|
178
|
+
in a shadow root with its own styles, so no app stylesheet (a reset, a missing preflight, a
|
|
179
|
+
global `button`, `img` or `svg` rule) changes how it looks. It takes only the host's Cbox
|
|
180
|
+
colour tokens (`--foreground`, `--popover`, `--border`, `--secondary`, `--muted`,
|
|
181
|
+
`--accent-soft`, `--ring`), each with a fallback.
|
|
182
|
+
|
|
183
|
+
**Blade (or any server-rendered page).** Import the module once in the app's Vite entry
|
|
184
|
+
(`resources/js/app.js`):
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
import '@cboxdk/cbox-ui/app-switcher';
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
and put the tags in the layout:
|
|
191
|
+
|
|
192
|
+
```blade
|
|
193
|
+
<cbox-app-switcher
|
|
194
|
+
endpoint="{{ $appCatalogUrl ?? '' }}"
|
|
195
|
+
current="billing"
|
|
196
|
+
labels="{{ json_encode(['title' => __('Switch app'), 'loading' => __('Loading apps…'), 'empty' => __('No apps available'), 'unavailable' => __('Apps could not be loaded.')], JSON_UNESCAPED_UNICODE) }}"
|
|
197
|
+
></cbox-app-switcher>
|
|
198
|
+
|
|
199
|
+
{{-- The app's own mark (rail, brand), from the same catalog --}}
|
|
200
|
+
<cbox-service-mark service="billing" aria-hidden="true"></cbox-service-mark>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**React.** `CboxAppCatalog` renders the same element:
|
|
201
204
|
|
|
202
205
|
```tsx
|
|
203
206
|
<CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" />
|
|
204
207
|
```
|
|
205
208
|
|
|
206
|
-
|
|
209
|
+
Attributes of `<cbox-app-switcher>`:
|
|
210
|
+
|
|
211
|
+
| Attribute | |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `endpoint` | The host's same-origin catalog URL. Empty disables the control (pages without an organization). Another origin is refused. |
|
|
214
|
+
| `current` | This app's key: its tile is marked current. |
|
|
215
|
+
| `labels` | JSON with any of `title`, `loading`, `empty`, `unavailable`. Missing keys stay English. |
|
|
216
|
+
| `theme` | `auto` (default): `.dark`/`[data-theme="dark"]` or `.light`/`[data-theme="light"]` on `<html>`, else `prefers-color-scheme`. `class`: only the root marker counts, no marker is light (a host whose tokens only turn dark under `.dark`). `light`/`dark` pin it. |
|
|
217
|
+
|
|
218
|
+
`<cbox-service-mark service="key">` paints that app's icon from the catalog this browser last
|
|
219
|
+
kept, and repaints when a newer one arrives. It is 24px; set
|
|
220
|
+
`cbox-service-mark { --cbox-service-mark-size: 22px; }` to change it. It takes `theme` too.
|
|
221
|
+
|
|
222
|
+
The endpoint contract (`parseAppCatalog` and `signInTarget` are exported, so a host can test
|
|
223
|
+
its own answer):
|
|
207
224
|
|
|
208
225
|
- `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
|
|
209
226
|
— `refresh_after` is clamped to 1–60 s; apps without an `http(s)` launch URL are dropped.
|
|
210
|
-
- `401`/`403
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
227
|
+
- `401`/`403 { signin_url }` — the person has to sign in again. When they open the control
|
|
228
|
+
(or it is open when the answer arrives), the element sends the top window to `signin_url`:
|
|
229
|
+
no message, no button. A page that merely loads in the background is never moved. If the
|
|
230
|
+
same sign-in was tried in the last 30 seconds and the catalog still refuses, it shows "Apps
|
|
231
|
+
could not be loaded." instead, so it never loops. The legacy `{ authorize_url }` is followed
|
|
232
|
+
the same way, with `return_to` set to the current path (same-origin URLs only).
|
|
233
|
+
`signin_url` must be on the app's own origin; anything else is ignored.
|
|
234
|
+
- Anything else, or no network — "Apps could not be loaded.", quietly, and it tries again by
|
|
235
|
+
itself (2 s, 4 s, 8 s … up to 60 s). A grid it already shows stays.
|
|
236
|
+
|
|
237
|
+
Before navigating it fires `cbox-app-switcher:signin` (bubbling, composed, cancelable, with
|
|
238
|
+
`detail.url`); `preventDefault()` keeps the page where it is.
|
|
239
|
+
|
|
240
|
+
App icons come from Cbox ID's catalog only: `icon_url` for light surfaces and
|
|
241
|
+
`icon_dark_url` for dark ones; the element swaps them with the theme. Only `https:` icons
|
|
242
|
+
render; an app without one, or whose icon fails to load, shows its initials. Apps keep no
|
|
243
|
+
local copy of the marks. Each answer is kept in this browser (`localStorage`,
|
|
244
|
+
`cbox-app-catalog`), so the grid and the marks paint at once on the next page and the live
|
|
245
|
+
answer replaces them. The icons are served by Cbox ID, so the host's CSP needs
|
|
246
|
+
`img-src https://cbox.cboxid.com` (the element's styles are constructed stylesheets, which
|
|
247
|
+
`style-src` does not govern).
|
|
248
|
+
|
|
249
|
+
In React, `<CboxServiceMark appKey="infra" />` and the collapsed `CboxBrand` read the same
|
|
250
|
+
kept catalog; `CboxAppIcon` renders any single app's icon, and a host that loads the catalog
|
|
251
|
+
itself calls `rememberAppCatalog(apps)`.
|
|
252
|
+
|
|
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[] };
|
|
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
|
+
`ContextLevel.settingsUrl` is a URL template on the wire: `{id}` is replaced with the row's id
|
|
292
|
+
(`/orgs/{id}/settings`). Nav and action icons are Lucide names (`"rocket"`, `"server"`); pass
|
|
293
|
+
`icons` to `CboxShell` for others.
|
|
294
|
+
|
|
295
|
+
### What the app serves (all same-origin JSON)
|
|
296
|
+
|
|
297
|
+
| URL | Request | Answer |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `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. |
|
|
300
|
+
| `level.createUrl` | `POST { name, parent? }` | `{ id, detailsUrl }` (or `details_url`). The person lands on `detailsUrl`. A 422 shows its first error under the field. |
|
|
301
|
+
| `account.switchAccountUrl` | A plain link (full page load) | Usually a redirect to Cbox ID's account chooser. |
|
|
302
|
+
| `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). |
|
|
303
|
+
| `palette.searchUrl` | `GET ?q=` | `{ data: [{ id, title, subtitle?, url }] }`, in the current context. |
|
|
304
|
+
| `palette.gotoUrl` | `GET ?q=` | `{ data: [{ title, subtitle?, levels?: { organization, project, … }, url? }] }` across organizations. |
|
|
305
|
+
| `apps.catalogUrl` | `GET` | The catalog below, plus `in_organization`, `admission_url`, `purpose` and `hue` per app. |
|
|
306
|
+
| `help.updatesUrl` | `GET`; `POST {updatesUrl}/{id}/read` | `{ data: [{ id, kind: "ticket" \| "chat", title, excerpt?, at, unread, url? }] }` |
|
|
307
|
+
| `help.searchUrl` | `GET ?q=&topic=` | `{ data: [{ id, title, excerpt?, body?, url?, source? }] }` |
|
|
308
|
+
| `help.articlesUrl` | `GET` (template, `{topic}`) | The same shape; the first three are shown. |
|
|
309
|
+
|
|
310
|
+
Every link the shell follows from these answers (`url`, `detailsUrl`) must be `http(s)://` or
|
|
311
|
+
a `/path`; anything else is dropped. None of these may call another service synchronously (local first): organizations come from
|
|
312
|
+
the app's mirror of Cbox ID, the catalog from a cached copy. Help and docs search are proxies of
|
|
313
|
+
Cbox Assistant served by laravel-assistant-client; when Assistant is down, Help says so and
|
|
314
|
+
everything else works.
|
|
315
|
+
|
|
316
|
+
Requests send `X-XSRF-TOKEN` (from the `XSRF-TOKEN` cookie) or `X-CSRF-TOKEN` (from
|
|
317
|
+
`<meta name="csrf-token">`). Without a `navigate` prop, POSTs are real form submits with
|
|
318
|
+
`_token`.
|
|
319
|
+
|
|
320
|
+
### Preferences and first paint
|
|
321
|
+
|
|
322
|
+
The shell reports every preference change through `onPreferenceChange(key, value)`: `theme`,
|
|
323
|
+
`locale`, `regionalFormat`, `timeZone` (Cbox ID's shared preferences), and `railPinned`,
|
|
324
|
+
`helpOpen`, `subnavCollapsed` (the app's cookie, read on the server). Without the callback it
|
|
325
|
+
writes a `cbox_shell_<key>` cookie. The server sends them back in `shell.preferences`, so the
|
|
326
|
+
rail, Help and the subnav render in place on the server; `themeBootScript(theme)` (or the same
|
|
327
|
+
class and `data-theme` written by the server) sets the theme before paint.
|
|
328
|
+
|
|
329
|
+
### Help
|
|
330
|
+
|
|
331
|
+
Help is part of the shell: support updates (the button's count), docs and knowledge base
|
|
332
|
+
search, articles for the page's `help.topic`, Ask Cbox, Talk to a person and the support portal.
|
|
333
|
+
It docks beside the page from 1280px (360px, 400px from 1440px) and is a full-screen dialog
|
|
334
|
+
below. Its state (open, article, search, scroll, updates) lives in a store outside React, so it
|
|
335
|
+
survives visits and even a remount; the chat element sits outside the home/article view, so
|
|
336
|
+
opening an article never ends the conversation, and it survives as long as the shell sits in
|
|
337
|
+
the persistent layout. Help the person left open shows docked on load; below 1280px it waits
|
|
338
|
+
until they open it, so a phone or a narrow window never lands under the overlay. Support
|
|
339
|
+
updates are per organization: they load again when the organization or `updatesUrl` changes.
|
|
340
|
+
Closing Help gives focus back to what opened it.
|
|
341
|
+
|
|
342
|
+
The chat is the Assistant SDK's `<cbox-support session-url>` element (laravel-assistant-client,
|
|
343
|
+
`/ai-assistant/support.js` by default, loaded the first time Help opens). The shell sets
|
|
344
|
+
`element.intent = { route, title, context, url }` (and an `intent` JSON attribute) on every
|
|
345
|
+
page, and calls `element.ask(question, intent)` / `element.handover()` when the SDK has them,
|
|
346
|
+
else `element.open()`.
|
|
347
|
+
|
|
348
|
+
A page's own `?` button uses `useShellHelp()?.toggleHelp()`, or `<PageHeader help />`. The page's
|
|
349
|
+
help topic is `shell.help.topic`, set on the server per route (console-kit's `helpTopic`), not
|
|
350
|
+
a prop of the header: Help needs it on the first paint and on pages without a header.
|
|
351
|
+
|
|
352
|
+
### Keyboard
|
|
353
|
+
|
|
354
|
+
⌘K / Ctrl+K opens the palette anywhere (↑↓ move, Enter opens, ⌘Enter opens in a new tab, Esc
|
|
355
|
+
closes). In the context menus ↑↓ Home End move, Enter picks, → makes a row's gear the active
|
|
356
|
+
option (Enter opens its settings; in the cascade a second → crosses to the environments), ←
|
|
357
|
+
goes back, Esc closes and Tab leaves; focus returns to the trigger. ↓ past the last row
|
|
358
|
+
reaches "New …" and ↑ above the first reaches "View all", so both work without a mouse; in the
|
|
359
|
+
app grid End and ↓ from the bottom row reach "All products". ⌘. collapses the subnav. Search fields focus themselves only with a fine pointer and
|
|
360
|
+
hover, never on touch. Sheets and the Help overlay trap focus; the docked column does not.
|
|
361
|
+
|
|
362
|
+
### Phone (< 768px)
|
|
363
|
+
|
|
364
|
+
The header holds ☰ (areas, pages, Switch app, notifications, the account), one context button
|
|
365
|
+
(`Cbox / Webshop / Production`) that opens every level stacked, Search and Help (full screen).
|
|
366
|
+
Nothing is hidden without a replacement.
|
|
367
|
+
|
|
368
|
+
### Tests and snapshots
|
|
369
|
+
|
|
370
|
+
`bun run test` runs the unit tests (vitest, Testing Library, happy-dom). `bun run test:visual`
|
|
371
|
+
renders the fixture page in `tests/visual` (the real shell, the fixture `shell` prop and its
|
|
372
|
+
answers) and compares every open state at 1440 and 390, light and dark, against
|
|
373
|
+
`tests/visual/__screenshots__`, and runs axe on every open state. It runs in the pinned
|
|
374
|
+
Playwright container (`scripts/visual.sh`, needs Docker), the same one CI's `visual` job uses,
|
|
375
|
+
so the Linux baselines match pixel for pixel. After an intended change:
|
|
376
|
+
`bun run test:visual:update`, then look at every changed image.
|
|
377
|
+
|
|
378
|
+
### Tokens
|
|
250
379
|
|
|
251
380
|
Core tokens now match the warm cream/ink design-system colors. Body text uses
|
|
252
381
|
the system stack; hosts self-host Plus Jakarta Sans for headings and JetBrains
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { b as CboxAppCatalogState, c as CboxCatalogApp, a as CboxKnownApp, C as CboxThemeMode, d as authorizeHref, i as isDark, p as parseAppCatalog, r as readKnownAppCatalog, e as rememberAppCatalog, s as secureIconUrl, f as signInTarget, g as subscribeKnownAppCatalog } from './catalog-BroZWvws.js';
|
|
2
|
+
|
|
3
|
+
/** The words the control shows. Pass a translation as the `labels` attribute (JSON). */
|
|
4
|
+
type CboxAppSwitcherLabels = {
|
|
5
|
+
title: string;
|
|
6
|
+
loading: string;
|
|
7
|
+
empty: string;
|
|
8
|
+
unavailable: string;
|
|
9
|
+
};
|
|
10
|
+
declare const DEFAULT_LABELS: CboxAppSwitcherLabels;
|
|
11
|
+
/**
|
|
12
|
+
* Fired on `<cbox-app-switcher>` (bubbling, composed, cancelable) just before it sends the
|
|
13
|
+
* person to sign in again; `detail.url` is where. `preventDefault()` keeps the page where it is.
|
|
14
|
+
*/
|
|
15
|
+
declare const SIGNIN_EVENT = "cbox-app-switcher:signin";
|
|
16
|
+
/** Define `<cbox-app-switcher>` and `<cbox-service-mark>` once. A no-op outside a browser. */
|
|
17
|
+
declare function defineCboxAppSwitcher(): void;
|
|
18
|
+
|
|
19
|
+
export { type CboxAppSwitcherLabels, DEFAULT_LABELS, SIGNIN_EVENT, defineCboxAppSwitcher };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineCboxAppSwitcher } from './chunk-AJCYF7SS.js';
|
|
2
|
+
export { DEFAULT_LABELS, SIGNIN_EVENT, authorizeHref, defineCboxAppSwitcher, isDark, parseAppCatalog, readKnownAppCatalog, rememberAppCatalog, secureIconUrl, signInTarget, subscribeKnownAppCatalog } from './chunk-AJCYF7SS.js';
|
|
3
|
+
|
|
4
|
+
// src/app-switcher/index.ts
|
|
5
|
+
defineCboxAppSwitcher();
|
|
6
|
+
//# sourceMappingURL=app-switcher.js.map
|
|
7
|
+
//# sourceMappingURL=app-switcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app-switcher/index.ts"],"names":[],"mappings":";;;;AAcA,qBAAA,EAAsB","file":"app-switcher.js","sourcesContent":["/**\n * `@cboxdk/cbox-ui/app-switcher` — the one Switch-app control for every Cbox app, framework\n * free. Importing this module defines `<cbox-app-switcher>` and `<cbox-service-mark>`:\n *\n * import '@cboxdk/cbox-ui/app-switcher';\n *\n * <cbox-app-switcher endpoint=\"/acme/apps/catalog\" current=\"billing\"\n * labels='{\"title\":\"Skift app\"}'></cbox-app-switcher>\n * <cbox-service-mark service=\"billing\" aria-hidden=\"true\"></cbox-service-mark>\n *\n * Both render in a shadow root with their own styles, so no app stylesheet changes them.\n */\nimport { defineCboxAppSwitcher } from './elements';\n\ndefineCboxAppSwitcher();\n\nexport { defineCboxAppSwitcher, DEFAULT_LABELS, SIGNIN_EVENT, type CboxAppSwitcherLabels } from './elements';\nexport {\n authorizeHref,\n parseAppCatalog,\n readKnownAppCatalog,\n rememberAppCatalog,\n secureIconUrl,\n signInTarget,\n subscribeKnownAppCatalog,\n type CboxAppCatalogState,\n type CboxCatalogApp,\n type CboxKnownApp,\n} from './catalog';\nexport { isDark, type CboxThemeMode } from './theme';\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which theme the host page shows, read the way every Cbox app marks it:
|
|
3
|
+
*
|
|
4
|
+
* - `.dark` or `[data-theme="dark"]` on the root element: dark;
|
|
5
|
+
* - `.light` or `[data-theme="light"]` on the root element: light;
|
|
6
|
+
* - neither: the system's `prefers-color-scheme`, unless the element says `theme="class"`
|
|
7
|
+
* (a host whose tokens only turn dark under `.dark`, so no marker means light).
|
|
8
|
+
*
|
|
9
|
+
* `theme="light"` or `theme="dark"` on the element pins it.
|
|
10
|
+
*/
|
|
11
|
+
type CboxThemeMode = 'auto' | 'class' | 'light' | 'dark';
|
|
12
|
+
declare function isDark(mode?: CboxThemeMode): boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The app catalog contract every Cbox app serves, and the copy of it this browser keeps.
|
|
16
|
+
* Framework-free: the `<cbox-app-switcher>` element and the React components both read it.
|
|
17
|
+
*
|
|
18
|
+
* Every app's icon comes from Cbox ID's catalog: one light and one dark upload per app
|
|
19
|
+
* (`icon_url`, `icon_dark_url`). No app keeps a local copy of the marks.
|
|
20
|
+
*/
|
|
21
|
+
/** One app as the host's catalog endpoint lists it. */
|
|
22
|
+
type CboxCatalogApp = {
|
|
23
|
+
key: string;
|
|
24
|
+
name: string;
|
|
25
|
+
launch_url: string;
|
|
26
|
+
/** Cbox ID's uploaded icon for light surfaces. */
|
|
27
|
+
icon_url?: string | null;
|
|
28
|
+
/** Cbox ID's uploaded icon for dark surfaces. */
|
|
29
|
+
icon_dark_url?: string | null;
|
|
30
|
+
description?: string | null;
|
|
31
|
+
};
|
|
32
|
+
type CboxAppCatalogState = {
|
|
33
|
+
status: 'idle' | 'loading';
|
|
34
|
+
} | {
|
|
35
|
+
status: 'ready';
|
|
36
|
+
apps: CboxCatalogApp[];
|
|
37
|
+
refreshAfter: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The person has to sign in again. `signinUrl` is the new contract (the host's `401
|
|
41
|
+
* { signin_url }`), `authorizeUrl` the legacy one (`{ authorize_url }`).
|
|
42
|
+
*/
|
|
43
|
+
| {
|
|
44
|
+
status: 'signin_required';
|
|
45
|
+
signinUrl: string | null;
|
|
46
|
+
authorizeUrl: string | null;
|
|
47
|
+
} | {
|
|
48
|
+
status: 'unavailable';
|
|
49
|
+
};
|
|
50
|
+
/** One app as this browser keeps it between pages. */
|
|
51
|
+
type CboxKnownApp = {
|
|
52
|
+
key: string;
|
|
53
|
+
name: string;
|
|
54
|
+
icon_url: string | null;
|
|
55
|
+
icon_dark_url: string | null;
|
|
56
|
+
launch_url?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Interpret the catalog endpoint's answer:
|
|
60
|
+
*
|
|
61
|
+
* - `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
|
|
62
|
+
* - `401`/`403 { signin_url }`: the person signs in again there. The legacy `{ authorize_url }`
|
|
63
|
+
* and `{ state: 'authorization_required' }` mean the same.
|
|
64
|
+
* - anything else: unavailable right now.
|
|
65
|
+
*
|
|
66
|
+
* Only `http(s)` launch URLs are kept: the tiles are links the person will click.
|
|
67
|
+
*/
|
|
68
|
+
declare function parseAppCatalog(ok: boolean, body: unknown): CboxAppCatalogState;
|
|
69
|
+
type Here = {
|
|
70
|
+
origin: string;
|
|
71
|
+
pathname: string;
|
|
72
|
+
search: string;
|
|
73
|
+
};
|
|
74
|
+
/** The legacy authorize link with `return_to` set to where the person is, on this origin only. */
|
|
75
|
+
declare function authorizeHref(authorizeUrl: string | null, location: Here): string | null;
|
|
76
|
+
/**
|
|
77
|
+
* Where to send the person when the catalog says they must sign in again: the host's
|
|
78
|
+
* `signin_url` when it is on this origin (the app's own sign-in; never another site), else the legacy authorize
|
|
79
|
+
* link with `return_to`. `null` when neither is usable.
|
|
80
|
+
*/
|
|
81
|
+
declare function signInTarget(state: {
|
|
82
|
+
signinUrl: string | null;
|
|
83
|
+
authorizeUrl: string | null;
|
|
84
|
+
}, location: Here): string | null;
|
|
85
|
+
/** Only `https:` icon URLs are rendered: the icons are hosted by Cbox ID. */
|
|
86
|
+
declare function secureIconUrl(url: unknown): string | null;
|
|
87
|
+
/** The last catalog this browser kept (`localStorage` key `cbox-app-catalog`); empty without one. */
|
|
88
|
+
declare function readKnownAppCatalog(): CboxKnownApp[];
|
|
89
|
+
/** Keep the catalog the person just loaded, and tell every mark on this page to repaint. */
|
|
90
|
+
declare function rememberAppCatalog(apps: ReadonlyArray<{
|
|
91
|
+
key: string;
|
|
92
|
+
name: string;
|
|
93
|
+
launch_url?: string | null;
|
|
94
|
+
icon_url?: string | null;
|
|
95
|
+
icon_dark_url?: string | null;
|
|
96
|
+
}>): void;
|
|
97
|
+
/** Call `change` whenever the kept catalog changes, on this page or in another tab. */
|
|
98
|
+
declare function subscribeKnownAppCatalog(change: () => void): () => void;
|
|
99
|
+
|
|
100
|
+
export { type CboxThemeMode as C, type CboxKnownApp as a, type CboxAppCatalogState as b, type CboxCatalogApp as c, authorizeHref as d, rememberAppCatalog as e, signInTarget as f, subscribeKnownAppCatalog as g, isDark as i, parseAppCatalog as p, readKnownAppCatalog as r, secureIconUrl as s };
|