@cboxdk/cbox-ui 0.7.0 → 0.9.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 CHANGED
@@ -114,7 +114,7 @@ Match id's existing patterns — see id/resources/js/pages/* for examples.
114
114
  | `OrgSwitcher` | Dropdown listing the user's orgs. Calls `onSwitchOrganization` (or POSTs to `urls.switchOrgUrl`) on pick. |
115
115
  | `AppLauncher` | 9-dot grid showing sister Cbox apps the user can jump into. |
116
116
  | `UserMenu` | Avatar dropdown with "Manage account ↗" + "Team & billing ↗" (deep-links into id) + Sign out. Pass `LinkComponent` when those rows are pages of the host app, so the visit stays client-side. |
117
- | `CboxAppCatalog` | The Switch-app control fed live from the host's catalog endpoint (see "App catalog" below). |
117
+ | `CboxAppCatalog` | The Switch-app control fed live from the host's catalog endpoint: the shared `<cbox-app-switcher>` element (see "App catalog" below). |
118
118
  | `NotificationsBell` | Bell icon with unread badge, links to id's unified inbox. Renders nothing if context lacks `notifications`. |
119
119
 
120
120
  ### Page primitives
@@ -193,28 +193,82 @@ callback; without it, existing browser formatting is preserved.
193
193
 
194
194
  ### App catalog (Switch app)
195
195
 
196
- `AppSwitcher` and `AppLauncher` render the static `apps` list from `CboxIdProvider`. Apps
197
- that read the person's catalog live from Cbox ID use `CboxAppCatalog` instead: the host
198
- serves a same-origin endpoint (a proxy of Cbox ID's catalog under the signed-in person's
199
- grant) and the component renders its answer, refreshes it when `refresh_after` expires
200
- while the panel is open, and offers the way back to Cbox ID when the grant is missing.
196
+ Every Cbox app, Blade or React, draws the same Switch-app control: the `<cbox-app-switcher>`
197
+ custom element from `@cboxdk/cbox-ui/app-switcher`. It has no React dependency and renders
198
+ in a shadow root with its own styles, so no app stylesheet (a reset, a missing preflight, a
199
+ global `button`, `img` or `svg` rule) changes how it looks. It takes only the host's Cbox
200
+ colour tokens (`--foreground`, `--popover`, `--border`, `--secondary`, `--muted`,
201
+ `--accent-soft`, `--ring`), each with a fallback.
202
+
203
+ **Blade (or any server-rendered page).** Import the module once in the app's Vite entry
204
+ (`resources/js/app.js`):
205
+
206
+ ```js
207
+ import '@cboxdk/cbox-ui/app-switcher';
208
+ ```
209
+
210
+ and put the tags in the layout:
211
+
212
+ ```blade
213
+ <cbox-app-switcher
214
+ endpoint="{{ $appCatalogUrl ?? '' }}"
215
+ current="billing"
216
+ labels="{{ json_encode(['title' => __('Switch app'), 'loading' => __('Loading apps…'), 'empty' => __('No apps available'), 'unavailable' => __('Apps could not be loaded.')], JSON_UNESCAPED_UNICODE) }}"
217
+ ></cbox-app-switcher>
218
+
219
+ {{-- The app's own mark (rail, brand), from the same catalog --}}
220
+ <cbox-service-mark service="billing" aria-hidden="true"></cbox-service-mark>
221
+ ```
222
+
223
+ **React.** `CboxAppCatalog` renders the same element:
201
224
 
202
225
  ```tsx
203
- <CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" icons={serviceMarks} />
226
+ <CboxAppCatalog endpoint={appCatalogUrl} currentAppKey="infra" />
204
227
  ```
205
228
 
206
- The endpoint contract (`parseAppCatalog` is exported, so a host can test its own answer):
229
+ Attributes of `<cbox-app-switcher>`:
230
+
231
+ | Attribute | |
232
+ |---|---|
233
+ | `endpoint` | The host's same-origin catalog URL. Empty disables the control (pages without an organization). Another origin is refused. |
234
+ | `current` | This app's key: its tile is marked current. |
235
+ | `labels` | JSON with any of `title`, `loading`, `empty`, `unavailable`. Missing keys stay English. |
236
+ | `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. |
237
+
238
+ `<cbox-service-mark service="key">` paints that app's icon from the catalog this browser last
239
+ kept, and repaints when a newer one arrives. It is 24px; set
240
+ `cbox-service-mark { --cbox-service-mark-size: 22px; }` to change it. It takes `theme` too.
241
+
242
+ The endpoint contract (`parseAppCatalog` and `signInTarget` are exported, so a host can test
243
+ its own answer):
207
244
 
208
- - `200 { apps: [{ key, name, launch_url, icon_url?, description? }], refresh_after?: seconds }`
245
+ - `200 { apps: [{ key, name, launch_url, icon_url?, icon_dark_url?, description? }], refresh_after?: seconds }`
209
246
  — `refresh_after` is clamped to 1–60 s; apps without an `http(s)` launch URL are dropped.
210
- - `401`/`403` with `{ authorize_url }` or `{ state: 'authorization_required' }` — the panel
211
- says the apps could not be loaded and links "Reconnect to Cbox ID" to `authorize_url` with
212
- `return_to` set to the current path (same-origin URLs only).
213
- - Anything else — "Apps could not be loaded." with a retry.
214
-
215
- `endpoint={null}` renders the control disabled, so the chrome keeps its shape on pages
216
- without an organization. `icons` maps an app key to a mark the host bundles (the family's
217
- service marks); without one the catalog's `icon_url` is shown, then the name's initials.
247
+ - `401`/`403 { signin_url }` — the person has to sign in again. When they open the control
248
+ (or it is open when the answer arrives), the element sends the top window to `signin_url`:
249
+ no message, no button. A page that merely loads in the background is never moved. If the
250
+ same sign-in was tried in the last 30 seconds and the catalog still refuses, it shows "Apps
251
+ could not be loaded." instead, so it never loops. The legacy `{ authorize_url }` is followed
252
+ the same way, with `return_to` set to the current path (same-origin URLs only).
253
+ `signin_url` must be on the app's own origin; anything else is ignored.
254
+ - Anything else, or no network — "Apps could not be loaded.", quietly, and it tries again by
255
+ itself (2 s, 4 s, 8 s … up to 60 s). A grid it already shows stays.
256
+
257
+ Before navigating it fires `cbox-app-switcher:signin` (bubbling, composed, cancelable, with
258
+ `detail.url`); `preventDefault()` keeps the page where it is.
259
+
260
+ App icons come from Cbox ID's catalog only: `icon_url` for light surfaces and
261
+ `icon_dark_url` for dark ones; the element swaps them with the theme. Only `https:` icons
262
+ render; an app without one, or whose icon fails to load, shows its initials. Apps keep no
263
+ local copy of the marks. Each answer is kept in this browser (`localStorage`,
264
+ `cbox-app-catalog`), so the grid and the marks paint at once on the next page and the live
265
+ answer replaces them. The icons are served by Cbox ID, so the host's CSP needs
266
+ `img-src https://cbox.cboxid.com` (the element's styles are constructed stylesheets, which
267
+ `style-src` does not govern).
268
+
269
+ In React, `<CboxServiceMark appKey="infra" />` and the collapsed `CboxBrand` read the same
270
+ kept catalog; `CboxAppIcon` renders any single app's icon, and a host that loads the catalog
271
+ itself calls `rememberAppCatalog(apps)`.
218
272
 
219
273
  ### Workspace navigation (current design contract)
220
274
 
@@ -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 };