@artstorefronts/arthelper-nav 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +237 -30
- package/dist/config.d.ts +21 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -24
- package/dist/config.js.map +1 -1
- package/dist/fallback.d.ts +26 -6
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +47 -9
- package/dist/fallback.js.map +1 -1
- package/dist/index.css +83 -39
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/nav-bar.d.ts.map +1 -1
- package/dist/nav-bar.js +15 -3
- package/dist/nav-bar.js.map +1 -1
- package/dist/nav-trigger.d.ts.map +1 -1
- package/dist/nav-trigger.js +2 -3
- package/dist/nav-trigger.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +34 -2
- package/dist/provider.js.map +1 -1
- package/dist/storage.d.ts +39 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +78 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +15 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,172 @@ This document is for an engineer wiring the bar into a host app that lives **out
|
|
|
7
7
|
this monorepo. If you're working inside this repo, see `docs/llms/suite-nav.md` for how
|
|
8
8
|
the pieces fit together end to end.
|
|
9
9
|
|
|
10
|
+
## Agent implementation prompt
|
|
11
|
+
|
|
12
|
+
Copy the block below and hand it to a coding agent working in the host app. It is written
|
|
13
|
+
to be self-contained apart from this README, which it tells the agent to read — the agent
|
|
14
|
+
will have it locally once the package is installed.
|
|
15
|
+
|
|
16
|
+
````markdown
|
|
17
|
+
# Task: mount the ArtHelper suite nav bar in this app
|
|
18
|
+
|
|
19
|
+
This app is part of the ArtHelper suite. Add the shared suite bar so the suite reads as one
|
|
20
|
+
product. The bar is a published package — you are wiring it in, not building it.
|
|
21
|
+
|
|
22
|
+
## First
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @artstorefronts/arthelper-nav
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then read `node_modules/@artstorefronts/arthelper-nav/README.md` in full before writing any
|
|
29
|
+
code. It is the authoritative API reference and explains the layout contract, the collapsed
|
|
30
|
+
cookie, persistence and degradation behaviour. Peer dependencies are `react`, `react-dom`
|
|
31
|
+
and `lucide-react` — install them yourself if this app doesn't already have them.
|
|
32
|
+
|
|
33
|
+
## Find this app's catalog id
|
|
34
|
+
|
|
35
|
+
`activeAppId` must be the id the shared catalog uses for this app — not this repo's name.
|
|
36
|
+
List them:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -s https://api.arthelper.ai/rest/nav-config | jq -r '.apps[].id'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Pick the one that is this app and use it verbatim. A wrong value means the bar cannot
|
|
43
|
+
indicate where the user is and the collapse control has no chip to dock. If none of the ids
|
|
44
|
+
is obviously this app, stop and ask — an id is added by an operator, not by you.
|
|
45
|
+
|
|
46
|
+
## Wire it in
|
|
47
|
+
|
|
48
|
+
1. **Import the stylesheet** by deep path in your global CSS entry:
|
|
49
|
+
`@import '@artstorefronts/arthelper-nav/dist/index.css';`
|
|
50
|
+
Under Tailwind v4 every `@import` must precede other rules. The CSS is scoped with an
|
|
51
|
+
`ahsuite-` prefix and uses no Tailwind utilities, so it needs no `@source` entry and
|
|
52
|
+
cannot be purged.
|
|
53
|
+
|
|
54
|
+
2. **Wrap the app root in `NavProvider`**, passing `configUrl` and `activeAppId`.
|
|
55
|
+
|
|
56
|
+
3. **Render `NavBar` as the first child of the app shell, in normal document flow.** It
|
|
57
|
+
occupies real layout space; the page should shift down exactly once, at first paint.
|
|
58
|
+
Never wrap it in a fixed/absolute container and never portal it.
|
|
59
|
+
|
|
60
|
+
4. **Put `NavTrigger` in this app's own header, beside its logo.** This is not optional:
|
|
61
|
+
when the bar is collapsed, `NavTrigger` is the only way to restore it. Ship without it
|
|
62
|
+
and a user who collapses the bar can never get it back. It hides itself while the bar is
|
|
63
|
+
expanded, so you just place it.
|
|
64
|
+
|
|
65
|
+
5. **If this app server-renders**, read the collapsed cookie on the server and pass it as
|
|
66
|
+
`initialCollapsed`. Import the cookie's name from the package rather than retyping it.
|
|
67
|
+
Without this, a user who collapsed the bar gets an expanded bar in the first byte and a
|
|
68
|
+
56px jump after hydration.
|
|
69
|
+
|
|
70
|
+
6. **Optionally pass `onAppClick`** to emit your own analytics. The package imports no
|
|
71
|
+
analytics library, and one callback covers both the desktop tiles and the mobile dropdown.
|
|
72
|
+
|
|
73
|
+
## `configUrl` — the mistake to avoid
|
|
74
|
+
|
|
75
|
+
It is the ArtHelper API's **REST** endpoint, ending `/rest/nav-config`:
|
|
76
|
+
|
|
77
|
+
- production: `https://api.arthelper.ai/rest/nav-config`
|
|
78
|
+
- local: your ArtHelper stack's API port, same path
|
|
79
|
+
|
|
80
|
+
Read it from this app's environment config with a sensible default; do not hardcode one
|
|
81
|
+
value. **It is not the GraphQL endpoint.** Pointing it at `/graphql` returns 404 and the bar
|
|
82
|
+
falls back _silently_ to its compiled-in catalog — no console error, no visible failure,
|
|
83
|
+
just a bar that never reflects the admin screen. If tiles never update, curl the URL before
|
|
84
|
+
debugging anything else.
|
|
85
|
+
|
|
86
|
+
The endpoint is public, unauthenticated and CORS-open. **Do not send credentials** — the
|
|
87
|
+
response carries a literal `Access-Control-Allow-Origin: *`, which browsers reject for
|
|
88
|
+
credentialed requests.
|
|
89
|
+
|
|
90
|
+
## Sticky, and the audit that goes with it
|
|
91
|
+
|
|
92
|
+
Decide whether the bar is sticky:
|
|
93
|
+
|
|
94
|
+
- **This app's document scrolls** (a normal page): use `<NavBar sticky />`, and offset your
|
|
95
|
+
own sticky header's `top` by the bar's height. **That offset must react to collapse** —
|
|
96
|
+
read `collapsed` from `useNav()` and use `collapsed ? 0 : <bar height>`. A constant offset
|
|
97
|
+
leaves a visible void at scroll-top once the bar collapses.
|
|
98
|
+
- **The shell is a fixed-height flex column** that never scrolls the document: do not pass
|
|
99
|
+
`sticky` and change no offsets.
|
|
100
|
+
|
|
101
|
+
**Then audit the whole app for surfaces that assume the header's bottom edge, or that they
|
|
102
|
+
can occupy the full viewport height.** Grep for `top-14`, `top: 3.5rem`, `h-screen`,
|
|
103
|
+
`h-svh`, `h-dvh`, `100vh`, `100svh`, `100dvh`, and `calc(100vh - N)` patterns. Each one is
|
|
104
|
+
now short by the bar's height while the bar is expanded, and will sit _behind_ your header
|
|
105
|
+
or run off the bottom of the screen.
|
|
106
|
+
|
|
107
|
+
This is the single largest source of breakage when adopting the bar. The two apps inside the
|
|
108
|
+
ArtHelper monorepo needed **nine** such fixes between them, including a `position: fixed`
|
|
109
|
+
sidebar rail, two pages subtracting a hardcoded pixel count, and an overlay panel rendered
|
|
110
|
+
through a portal that could not inherit a CSS variable at all. Derive them all from one
|
|
111
|
+
shared value rather than sprinkling `collapsed ?` checks through components, and remember
|
|
112
|
+
that a portalled surface may need the value passed to it another way.
|
|
113
|
+
|
|
114
|
+
Judge each hit: a surface inside a scroll container that never touches the viewport edge is
|
|
115
|
+
unaffected, and a modal meant to cover the chrome should keep covering it. Say which you
|
|
116
|
+
changed and which you deliberately did not.
|
|
117
|
+
|
|
118
|
+
## What is not configurable
|
|
119
|
+
|
|
120
|
+
The bar's **look** — colour, alignment, accent, the wordmark — is compiled into the package,
|
|
121
|
+
not served in the payload. Do not look for theme props or try to restyle it from the host;
|
|
122
|
+
changing it is a package release. Only the app catalog is configurable.
|
|
123
|
+
|
|
124
|
+
## Expected behaviour, so you don't chase non-bugs
|
|
125
|
+
|
|
126
|
+
- **Only some apps appear.** An app renders only once an operator has both enabled it and
|
|
127
|
+
given it a destination in ArtHelper's admin screen.
|
|
128
|
+
- **This app's own chip always renders**, even if its catalog entry is disabled or has no
|
|
129
|
+
destination. With no destination it is a non-navigating indicator chip. That is correct —
|
|
130
|
+
it tells the user where they are.
|
|
131
|
+
- **The bar survives an endpoint outage** by falling back to a catalog compiled into the
|
|
132
|
+
package, so it renders at full height rather than blank and never blocks first paint.
|
|
133
|
+
- **The catalog is persisted** after the first successful fetch, so repeat loads paint the
|
|
134
|
+
last known catalog rather than the compiled-in one.
|
|
135
|
+
|
|
136
|
+
## Verify in a real browser, not by reasoning
|
|
137
|
+
|
|
138
|
+
1. The bar renders above your header; measure its height with `getBoundingClientRect()` and
|
|
139
|
+
confirm it matches the documented height exactly.
|
|
140
|
+
2. Collapse it: height 0, your header rises to the very top with no gap, chip appears beside
|
|
141
|
+
your logo. Restore it: back to full height.
|
|
142
|
+
3. Reload while collapsed — it stays collapsed.
|
|
143
|
+
4. Click another app's tile — the address bar becomes that destination.
|
|
144
|
+
5. If sticky: scroll a few hundred pixels in both bar states and confirm your header is
|
|
145
|
+
neither covered nor floating above a void.
|
|
146
|
+
6. Below the `md` breakpoint the tiles become a labelled dropdown; every app is reachable and
|
|
147
|
+
it dismisses without leaving the page.
|
|
148
|
+
7. Block the config endpoint in devtools — the bar still renders at full height with this
|
|
149
|
+
app's own chip, not blank.
|
|
150
|
+
8. If this app server-renders, check the console for hydration warnings. There must be none.
|
|
151
|
+
|
|
152
|
+
## Do not
|
|
153
|
+
|
|
154
|
+
- Do not fork, vendor or patch the package. Report upstream problems instead.
|
|
155
|
+
- Do not hardcode any app's destination — destinations are config.
|
|
156
|
+
- Do not infer `activeAppId` from the URL; the host declares it.
|
|
157
|
+
- Do not send credentials with the config request.
|
|
158
|
+
- Do not render a second bar if this app is ever framed inside another ArtHelper surface.
|
|
159
|
+
- Avoid `yarn link` unless necessary; if you use it, make sure the bundler resolves a single
|
|
160
|
+
copy of React or you will hit the duplicate-React hooks error. `npm pack` plus installing
|
|
161
|
+
the tarball avoids that and resolves more like the real thing.
|
|
162
|
+
````
|
|
163
|
+
|
|
10
164
|
## Architecture in one line
|
|
11
165
|
|
|
12
|
-
Configure in data,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
166
|
+
Configure the catalog in data, ship everything else in code. The **app catalog** lives in
|
|
167
|
+
a single database row, served as JSON over a public HTTP endpoint. Every mounted host
|
|
168
|
+
fetches that JSON on load, so a catalog edit made in the admin editor is live for every
|
|
169
|
+
app in the suite on their next page load — no deploy, no version bump, no coordinated
|
|
170
|
+
release across repos.
|
|
171
|
+
|
|
172
|
+
The payload is `{ schema_version, apps }` and nothing else. The bar's look and its
|
|
173
|
+
wordmark are compiled in, on purpose — see [Theming and the wordmark](#theming-and-the-wordmark).
|
|
174
|
+
The last good catalog is persisted per browser so a return visit paints it rather than the
|
|
175
|
+
compiled-in fallback — see [Persistence](#persistence).
|
|
17
176
|
|
|
18
177
|
## Install
|
|
19
178
|
|
|
@@ -31,21 +190,13 @@ Peer dependencies (from `package.json` — install these yourself, they are not
|
|
|
31
190
|
}
|
|
32
191
|
```
|
|
33
192
|
|
|
34
|
-
> **
|
|
35
|
-
>
|
|
36
|
-
> `
|
|
37
|
-
>
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
> The `@artstorefronts` scope already exists (it publishes `@artstorefronts/ui` and
|
|
41
|
-
> `@artstorefronts/cli`). What is still missing is a **trusted publisher** configured on
|
|
42
|
-
> npm for this package, pointing at this repository and the workflow filename
|
|
43
|
-
> `release-nav.yml`. Authentication is OIDC, so there is no token to create or rotate.
|
|
44
|
-
> Until that is configured, `npm install @artstorefronts/arthelper-nav` has nothing to
|
|
45
|
-
> resolve.
|
|
193
|
+
> **Getting a change released.** There is no tag and no manual release step: bump the
|
|
194
|
+
> version in `packages/nav/package.json` in the PR that changes the package, and merging
|
|
195
|
+
> to `develop` publishes it. CI publishes only when that version is absent from the
|
|
196
|
+
> registry, and a PR that changes the published surface while leaving the version alone
|
|
197
|
+
> fails its own check — so a change cannot quietly land without reaching you.
|
|
46
198
|
>
|
|
47
|
-
>
|
|
48
|
-
> dependency from the satellite repo:
|
|
199
|
+
> **Testing an unreleased change.** Link the workspace, or use a path dependency:
|
|
49
200
|
>
|
|
50
201
|
> ```bash
|
|
51
202
|
> cd packages/nav && yarn link
|
|
@@ -54,7 +205,8 @@ Peer dependencies (from `package.json` — install these yourself, they are not
|
|
|
54
205
|
>
|
|
55
206
|
> `react` and `react-dom` are peer dependencies, so make sure the satellite's bundler
|
|
56
207
|
> resolves a single copy of React — a linked package pulling in its own is the usual
|
|
57
|
-
> cause of the duplicate-React hooks error.
|
|
208
|
+
> cause of the duplicate-React hooks error. `npm pack` plus installing the tarball avoids
|
|
209
|
+
> that failure mode entirely and resolves more like the real thing.
|
|
58
210
|
|
|
59
211
|
## Minimal mounting example
|
|
60
212
|
|
|
@@ -108,7 +260,7 @@ wrap `NavBar`, `NavTrigger`, and any component calling `useNav()`.
|
|
|
108
260
|
|
|
109
261
|
### `NavBar`
|
|
110
262
|
|
|
111
|
-
Renders the bar itself: waffle toggle, wordmark
|
|
263
|
+
Renders the bar itself: waffle toggle, wordmark, the desktop tile row (or, below the
|
|
112
264
|
768px breakpoint, a labelled dropdown trigger and panel with the same entries as rows).
|
|
113
265
|
|
|
114
266
|
| Prop | Type | Default | Description |
|
|
@@ -130,9 +282,8 @@ mounting `NavTrigger` ships a bar a viewer can collapse and never recover.
|
|
|
130
282
|
|
|
131
283
|
### Exported types
|
|
132
284
|
|
|
133
|
-
`NavContextValue`, `NavBarProps`, `NavTriggerProps`, `NavProviderProps`, `
|
|
134
|
-
`
|
|
135
|
-
`ResolvedNavApp` — all exported from the package root (`src/index.ts`), also useful if
|
|
285
|
+
`NavContextValue`, `NavBarProps`, `NavTriggerProps`, `NavProviderProps`, `NavApp`,
|
|
286
|
+
`NavAppClickArgs`, `NavConfigPayload`, `ResolvedNavApp` — all exported from the package root (`src/index.ts`), also useful if
|
|
136
287
|
you're typing `fallbackConfig` or `onAppClick` yourself.
|
|
137
288
|
|
|
138
289
|
`useNav()` is also exported, for a component nested under `NavProvider` that needs
|
|
@@ -213,12 +364,59 @@ The cookie is **per-origin**. `app.example.com` and `www.example.com` are differ
|
|
|
213
364
|
registrable domains and share no cookie scope, so collapsing the bar on one origin does
|
|
214
365
|
not sync to another — each origin's viewer sets their own collapsed state independently.
|
|
215
366
|
|
|
216
|
-
## Theming
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
367
|
+
## Theming and the wordmark
|
|
368
|
+
|
|
369
|
+
There is no theming, deliberately. The bar ships one look — dark "Forest", tiles centred
|
|
370
|
+
on the bar — baked into `dist/index.css`, and the wordmark (`arthelper`, linking to
|
|
371
|
+
`https://www.arthelper.com`) is baked into the component. No part of either can be set by
|
|
372
|
+
a host or by the payload.
|
|
373
|
+
|
|
374
|
+
Both used to be configurable: the payload carried a `theme` block (`mode`,
|
|
375
|
+
`accent_color`, `alignment`) and a `logo` block (`label`, `href`) that an admin edited in
|
|
376
|
+
the nav editor. That was a mistake, and the reason generalizes to every configurable
|
|
377
|
+
field. The bar paints the package's compiled-in fallback before the fetch resolves and
|
|
378
|
+
swaps in the served config afterwards, so **any** configured value that disagrees with the
|
|
379
|
+
fallback's guess produces a visible change on every page load — a dark bar turning light,
|
|
380
|
+
tiles jumping from left to centre, a wordmark rewriting itself. Colour, layout and the
|
|
381
|
+
wordmark are release decisions, reviewed once and then stable. The app catalog is the one
|
|
382
|
+
genuine operational switch, and it is all the payload carries now.
|
|
383
|
+
|
|
384
|
+
The wordmark's href is the site root, not the resolved `home` catalog entry. Deriving it
|
|
385
|
+
was considered and rejected: it would put a static piece of chrome back on config, and
|
|
386
|
+
`home` points at `/community` while the wordmark has always gone to the root.
|
|
387
|
+
|
|
388
|
+
Changing the look or the wordmark now means a package release. A host that wants the bar
|
|
389
|
+
on differently-coloured chrome should change its own surrounding chrome, not the bar.
|
|
390
|
+
|
|
391
|
+
A payload that still carries `theme` or `logo` has them dropped during normalization like
|
|
392
|
+
any other unrecognized field, and an older renderer that still expects them degrades to
|
|
393
|
+
its own compiled-in defaults rather than throwing.
|
|
394
|
+
|
|
395
|
+
## Persistence
|
|
396
|
+
|
|
397
|
+
The catalog is the one field that can still differ from the compiled-in fallback, and
|
|
398
|
+
because the tile row is centred on the bar, that difference is expensive: every tile the
|
|
399
|
+
fetch adds or removes shifts the whole row by half a tile, so a two-tile fallback resolving
|
|
400
|
+
to a seven-tile config moves the Home tile 230px.
|
|
401
|
+
|
|
402
|
+
So a successful fetch persists the normalized payload to `localStorage` under
|
|
403
|
+
`ahsuite-nav-config-v1`, and on mount the bar prefers that over the compiled-in fallback.
|
|
404
|
+
The fallback becomes the first-ever-visit case.
|
|
405
|
+
|
|
406
|
+
Things worth knowing if you are wiring this into a host:
|
|
407
|
+
|
|
408
|
+
- **The persisted blob is re-validated on read**, through the same `normalize` a network
|
|
409
|
+
body goes through. A blob written months ago by an older renderer is untrusted input.
|
|
410
|
+
- **Entries expire after 30 days**, so a long-dormant browser revalidates rather than
|
|
411
|
+
painting something ancient.
|
|
412
|
+
- **Every storage access is wrapped**, including the `localStorage` property read itself —
|
|
413
|
+
Safari's private mode and Chrome with site data blocked throw on _access_, not just on
|
|
414
|
+
write. An unreachable store simply means the fallback paints.
|
|
415
|
+
- **It cannot help a server-rendered host's first frame.** The server has no access to the
|
|
416
|
+
viewer's `localStorage`, so its HTML is always the compiled-in fallback. The read runs in
|
|
417
|
+
a client-only layout effect — before the browser paints, after hydration has committed —
|
|
418
|
+
which removes the hydration-time swap but not the SSR one. If that first frame matters to
|
|
419
|
+
you, pass the catalog you want server-rendered as `fallbackConfig`.
|
|
222
420
|
|
|
223
421
|
## The layout contract
|
|
224
422
|
|
|
@@ -255,6 +453,8 @@ The bar is designed to never render blank and never block first paint:
|
|
|
255
453
|
during normalization, never carried through and never thrown on.
|
|
256
454
|
- **A higher `schema_version` than this renderer understands** — ignored the same way;
|
|
257
455
|
the renderer keeps working on the fields it does know.
|
|
456
|
+
- **A corrupt, stale or unreachable `localStorage`** — treated as "nothing persisted": the
|
|
457
|
+
compiled-in fallback paints and the fetch revalidates. Never throws.
|
|
258
458
|
|
|
259
459
|
## The `schema_version` contract
|
|
260
460
|
|
|
@@ -264,3 +464,10 @@ fields it doesn't recognize rather than throw. That's what lets the suite's
|
|
|
264
464
|
asynchronously-deployed satellite fleet run mixed renderer versions against one shared
|
|
265
465
|
payload — an older `@artstorefronts/arthelper-nav` keeps working after a newer field ships on the
|
|
266
466
|
config row. The version number itself is bumped only for a genuinely breaking change.
|
|
467
|
+
|
|
468
|
+
**The one exception so far:** `theme` was _removed_ from the payload without bumping
|
|
469
|
+
`schema_version` off 1. Removal is breaking under this contract, and it was allowed only
|
|
470
|
+
because no stable version had ever been released against the old shape — the two
|
|
471
|
+
`0.1.0-alpha.*` publishes both tolerate the field's absence and degrade rather than
|
|
472
|
+
throw, which was verified against the actual tarballs. After the first stable publish the
|
|
473
|
+
same removal would require a version bump and a transition period.
|
package/dist/config.d.ts
CHANGED
|
@@ -7,17 +7,34 @@ import type { NavConfigPayload } from './types.js';
|
|
|
7
7
|
* `null` is reserved for a body that could not be used, because that is the signal
|
|
8
8
|
* the caller reads as "keep showing what you have". An empty `apps` array is NOT such
|
|
9
9
|
* a body: it is what the endpoint serves when an operator has disabled every
|
|
10
|
-
* destination, and treating it as a failure
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* destination, and treating it as a failure reverted every host to the compiled-in
|
|
11
|
+
* fallback and painted a hardcoded Home tile — the exact inverse of the instruction.
|
|
12
|
+
*
|
|
13
|
+
* A payload that still carries the retired `theme` or `logo` blocks drops them here like
|
|
14
|
+
* any other unrecognised field, which is what lets a stage serving the old shape run
|
|
15
|
+
* against this renderer while the endpoint change lands separately.
|
|
16
|
+
*
|
|
17
|
+
* This is also the gate every PERSISTED payload passes through (see `restored`). A blob
|
|
18
|
+
* out of localStorage is untrusted input in exactly the way a network body is — it may
|
|
19
|
+
* have been written months ago by a renderer with a different shape — so it gets the same
|
|
20
|
+
* coercion rather than a JSON.parse and a cast.
|
|
13
21
|
*/
|
|
14
22
|
declare function normalize(raw: unknown): NavConfigPayload | null;
|
|
15
23
|
declare function cached(url: string): NavConfigPayload | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The persisted last-good payload, re-validated. Null when there is nothing stored, the
|
|
26
|
+
* store is unreachable, the blob is corrupt, or the entry is past its staleness cap —
|
|
27
|
+
* every one of which means "paint the compiled-in fallback and wait for the fetch".
|
|
28
|
+
*
|
|
29
|
+
* The dependency runs config -> storage and never back, which is why `writeStored` is
|
|
30
|
+
* called from `fetchConfig` here rather than storage.ts reaching for `normalize` itself.
|
|
31
|
+
*/
|
|
32
|
+
declare function restored(): NavConfigPayload | null;
|
|
16
33
|
/**
|
|
17
34
|
* Fetches the config with a hard timeout. Resolves to null on any failure —
|
|
18
35
|
* network, timeout, non-2xx, or an unparseable body — and the caller keeps
|
|
19
36
|
* whatever it is already showing.
|
|
20
37
|
*/
|
|
21
38
|
declare function fetchConfig(url: string, timeoutMs?: number): Promise<NavConfigPayload | null>;
|
|
22
|
-
export { cached, fetchConfig, normalize };
|
|
39
|
+
export { cached, fetchConfig, normalize, restored };
|
|
23
40
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA0D3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAYxD;AAED,iBAAS,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzD;AAED;;;;;;;GAOG;AACH,iBAAS,QAAQ,IAAI,gBAAgB,GAAG,IAAI,CAE3C;AAED;;;;GAIG;AACH,iBAAe,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,GAAE,MAAmB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA2BxG;AAED,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FALLBACK_CONFIG } from './fallback.js';
|
|
2
|
+
import { readStored, writeStored } from './storage.js';
|
|
2
3
|
/** Short enough that a slow endpoint never delays the bar past first paint. */
|
|
3
4
|
const TIMEOUT_MS = 2500;
|
|
4
5
|
/** Last good payload per URL, so a remount paints the fresh config, not the fallback. */
|
|
@@ -9,15 +10,6 @@ function isRecord(value) {
|
|
|
9
10
|
function text(value, fallback) {
|
|
10
11
|
return typeof value === 'string' ? value : fallback;
|
|
11
12
|
}
|
|
12
|
-
function mode(value) {
|
|
13
|
-
return value === 'light' ? 'light' : 'dark';
|
|
14
|
-
}
|
|
15
|
-
function alignment(value) {
|
|
16
|
-
return value === 'centered' ? 'centered' : 'left';
|
|
17
|
-
}
|
|
18
|
-
function accent(value) {
|
|
19
|
-
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
20
|
-
}
|
|
21
13
|
// Unknown fields are dropped rather than carried, so a newer payload can never
|
|
22
14
|
// reach the renderer as a shape it does not understand.
|
|
23
15
|
function app(raw) {
|
|
@@ -66,37 +58,45 @@ function apps(raw) {
|
|
|
66
58
|
* `null` is reserved for a body that could not be used, because that is the signal
|
|
67
59
|
* the caller reads as "keep showing what you have". An empty `apps` array is NOT such
|
|
68
60
|
* a body: it is what the endpoint serves when an operator has disabled every
|
|
69
|
-
* destination, and treating it as a failure
|
|
70
|
-
*
|
|
71
|
-
*
|
|
61
|
+
* destination, and treating it as a failure reverted every host to the compiled-in
|
|
62
|
+
* fallback and painted a hardcoded Home tile — the exact inverse of the instruction.
|
|
63
|
+
*
|
|
64
|
+
* A payload that still carries the retired `theme` or `logo` blocks drops them here like
|
|
65
|
+
* any other unrecognised field, which is what lets a stage serving the old shape run
|
|
66
|
+
* against this renderer while the endpoint change lands separately.
|
|
67
|
+
*
|
|
68
|
+
* This is also the gate every PERSISTED payload passes through (see `restored`). A blob
|
|
69
|
+
* out of localStorage is untrusted input in exactly the way a network body is — it may
|
|
70
|
+
* have been written months ago by a renderer with a different shape — so it gets the same
|
|
71
|
+
* coercion rather than a JSON.parse and a cast.
|
|
72
72
|
*/
|
|
73
73
|
function normalize(raw) {
|
|
74
74
|
if (!isRecord(raw)) {
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
const theme = isRecord(raw.theme) ? raw.theme : {};
|
|
78
|
-
const logo = isRecord(raw.logo) ? raw.logo : {};
|
|
79
77
|
const parsed = apps(raw.apps);
|
|
80
78
|
if (!parsed) {
|
|
81
79
|
return null;
|
|
82
80
|
}
|
|
83
81
|
return {
|
|
84
82
|
schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : FALLBACK_CONFIG.schema_version,
|
|
85
|
-
theme: {
|
|
86
|
-
mode: mode(theme.mode),
|
|
87
|
-
accent_color: accent(theme.accent_color),
|
|
88
|
-
alignment: alignment(theme.alignment),
|
|
89
|
-
},
|
|
90
|
-
logo: {
|
|
91
|
-
label: text(logo.label, FALLBACK_CONFIG.logo.label),
|
|
92
|
-
href: text(logo.href, FALLBACK_CONFIG.logo.href),
|
|
93
|
-
},
|
|
94
83
|
apps: parsed,
|
|
95
84
|
};
|
|
96
85
|
}
|
|
97
86
|
function cached(url) {
|
|
98
87
|
return memo.get(url);
|
|
99
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The persisted last-good payload, re-validated. Null when there is nothing stored, the
|
|
91
|
+
* store is unreachable, the blob is corrupt, or the entry is past its staleness cap —
|
|
92
|
+
* every one of which means "paint the compiled-in fallback and wait for the fetch".
|
|
93
|
+
*
|
|
94
|
+
* The dependency runs config -> storage and never back, which is why `writeStored` is
|
|
95
|
+
* called from `fetchConfig` here rather than storage.ts reaching for `normalize` itself.
|
|
96
|
+
*/
|
|
97
|
+
function restored() {
|
|
98
|
+
return normalize(readStored());
|
|
99
|
+
}
|
|
100
100
|
/**
|
|
101
101
|
* Fetches the config with a hard timeout. Resolves to null on any failure —
|
|
102
102
|
* network, timeout, non-2xx, or an unparseable body — and the caller keeps
|
|
@@ -121,6 +121,7 @@ async function fetchConfig(url, timeoutMs = TIMEOUT_MS) {
|
|
|
121
121
|
return null;
|
|
122
122
|
}
|
|
123
123
|
memo.set(url, next);
|
|
124
|
+
writeStored(next);
|
|
124
125
|
return next;
|
|
125
126
|
}
|
|
126
127
|
catch {
|
|
@@ -130,5 +131,5 @@ async function fetchConfig(url, timeoutMs = TIMEOUT_MS) {
|
|
|
130
131
|
clearTimeout(timer);
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
export { cached, fetchConfig, normalize };
|
|
134
|
+
export { cached, fetchConfig, normalize, restored };
|
|
134
135
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/fallback.d.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { NavConfigPayload } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* The static config compiled into the package. Used whenever the fetch fails,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The static config compiled into the package. Used whenever the fetch fails, times out,
|
|
4
|
+
* or has not resolved yet, so the bar reserves its final 56px from first paint and never
|
|
5
|
+
* renders blank.
|
|
6
6
|
*
|
|
7
|
-
* It carries
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* It carries the WHOLE catalog, not just the enabled entries, and that is load-bearing
|
|
8
|
+
* rather than tidiness. A host's own `activeAppId` entry is always rendered and indicated,
|
|
9
|
+
* resolved from this list when the payload omits it — so a satellite whose id is missing
|
|
10
|
+
* here loses its chip entirely during an endpoint outage, and with it any indication of
|
|
11
|
+
* where the user is and anything for NavTrigger to dock. Every app that can mount the bar
|
|
12
|
+
* needs an entry, whether or not it is currently switched on.
|
|
13
|
+
*
|
|
14
|
+
* `enabled` here still governs what renders: the same enabled-and-has-destination filter
|
|
15
|
+
* runs against this list as against the fetched payload, so a disabled entry contributes
|
|
16
|
+
* its label, icon, order and destination without appearing as a tile.
|
|
17
|
+
*
|
|
18
|
+
* It carries no look and no wordmark. Colour, layout and the wordmark are compiled in
|
|
19
|
+
* (index.css and nav-bar.tsx), so there is nothing for a fetched payload to disagree with
|
|
20
|
+
* and nothing to flash between first paint and the fetch landing. The catalog is the one
|
|
21
|
+
* field that can still differ — which is why the last good payload is persisted; see
|
|
22
|
+
* storage.ts.
|
|
23
|
+
*
|
|
24
|
+
* These are the production domains, which is why they can live in compiled code at all.
|
|
25
|
+
* Changing this list requires a package release, so it is deliberately the stable
|
|
26
|
+
* first-party set — anything situational (a stage host, a locally-run satellite) belongs
|
|
27
|
+
* in the config row, where an admin can edit it without a release. `enabled` states drift
|
|
28
|
+
* from the row over time; that is expected and harmless, since the row wins whenever the
|
|
29
|
+
* endpoint is reachable.
|
|
10
30
|
*/
|
|
11
31
|
declare const FALLBACK_CONFIG: NavConfigPayload;
|
|
12
32
|
export { FALLBACK_CONFIG };
|
package/dist/fallback.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../src/fallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../src/fallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,QAAA,MAAM,eAAe,EAAE,gBAuBtB,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/fallback.js
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The static config compiled into the package. Used whenever the fetch fails,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The static config compiled into the package. Used whenever the fetch fails, times out,
|
|
3
|
+
* or has not resolved yet, so the bar reserves its final 56px from first paint and never
|
|
4
|
+
* renders blank.
|
|
5
5
|
*
|
|
6
|
-
* It carries
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* It carries the WHOLE catalog, not just the enabled entries, and that is load-bearing
|
|
7
|
+
* rather than tidiness. A host's own `activeAppId` entry is always rendered and indicated,
|
|
8
|
+
* resolved from this list when the payload omits it — so a satellite whose id is missing
|
|
9
|
+
* here loses its chip entirely during an endpoint outage, and with it any indication of
|
|
10
|
+
* where the user is and anything for NavTrigger to dock. Every app that can mount the bar
|
|
11
|
+
* needs an entry, whether or not it is currently switched on.
|
|
12
|
+
*
|
|
13
|
+
* `enabled` here still governs what renders: the same enabled-and-has-destination filter
|
|
14
|
+
* runs against this list as against the fetched payload, so a disabled entry contributes
|
|
15
|
+
* its label, icon, order and destination without appearing as a tile.
|
|
16
|
+
*
|
|
17
|
+
* It carries no look and no wordmark. Colour, layout and the wordmark are compiled in
|
|
18
|
+
* (index.css and nav-bar.tsx), so there is nothing for a fetched payload to disagree with
|
|
19
|
+
* and nothing to flash between first paint and the fetch landing. The catalog is the one
|
|
20
|
+
* field that can still differ — which is why the last good payload is persisted; see
|
|
21
|
+
* storage.ts.
|
|
22
|
+
*
|
|
23
|
+
* These are the production domains, which is why they can live in compiled code at all.
|
|
24
|
+
* Changing this list requires a package release, so it is deliberately the stable
|
|
25
|
+
* first-party set — anything situational (a stage host, a locally-run satellite) belongs
|
|
26
|
+
* in the config row, where an admin can edit it without a release. `enabled` states drift
|
|
27
|
+
* from the row over time; that is expected and harmless, since the row wins whenever the
|
|
28
|
+
* endpoint is reachable.
|
|
9
29
|
*/
|
|
10
30
|
const FALLBACK_CONFIG = {
|
|
11
31
|
schema_version: 1,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
apps: [
|
|
33
|
+
{ id: 'home', label: 'Home', icon: 'house', href: 'https://www.arthelper.com/community', enabled: true },
|
|
34
|
+
{ id: 'the-daily', label: 'The Daily', icon: 'sunrise', href: 'https://thedaily.arthelper.com', enabled: false },
|
|
35
|
+
{ id: 'art-review', label: 'Art Review', icon: 'star', href: 'https://artreview.arthelper.com', enabled: true },
|
|
36
|
+
{
|
|
37
|
+
id: 'creative-sparks',
|
|
38
|
+
label: 'Creative Sparks',
|
|
39
|
+
icon: 'sparkles',
|
|
40
|
+
href: 'https://sparks.arthelper.com',
|
|
41
|
+
enabled: false,
|
|
42
|
+
},
|
|
43
|
+
{ id: 'mockups', label: 'Mockups', icon: 'frame', href: 'https://mockups.arthelper.com', enabled: false },
|
|
44
|
+
{ id: 'art-books', label: 'Art Books', icon: 'book-open', href: 'https://artbooks.arthelper.com', enabled: false },
|
|
45
|
+
{
|
|
46
|
+
id: 'social-posting',
|
|
47
|
+
label: 'Social Posting',
|
|
48
|
+
icon: 'megaphone',
|
|
49
|
+
href: 'https://copilot.arthelper.com',
|
|
50
|
+
enabled: false,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
15
53
|
};
|
|
16
54
|
export { FALLBACK_CONFIG };
|
|
17
55
|
//# sourceMappingURL=fallback.js.map
|
package/dist/fallback.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../src/fallback.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../src/fallback.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,eAAe,GAAqB;IACxC,cAAc,EAAE,CAAC;IACjB,IAAI,EAAE;QACJ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,qCAAqC,EAAE,OAAO,EAAE,IAAI,EAAE;QACxG,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,gCAAgC,EAAE,OAAO,EAAE,KAAK,EAAE;QAChH,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iCAAiC,EAAE,OAAO,EAAE,IAAI,EAAE;QAC/G;YACE,EAAE,EAAE,iBAAiB;YACrB,KAAK,EAAE,iBAAiB;YACxB,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,KAAK;SACf;QACD,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,+BAA+B,EAAE,OAAO,EAAE,KAAK,EAAE;QACzG,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,gCAAgC,EAAE,OAAO,EAAE,KAAK,EAAE;QAClH;YACE,EAAE,EAAE,gBAAgB;YACpB,KAAK,EAAE,gBAAgB;YACvB,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,+BAA+B;YACrC,OAAO,EAAE,KAAK;SACf;KACF;CACF,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/index.css
CHANGED
|
@@ -15,9 +15,20 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/* ---------------------------------------------------------------------------
|
|
18
|
-
* Tokens. `.ahsuite-panel` inherits from
|
|
19
|
-
* in the host's own header and so carries
|
|
20
|
-
*
|
|
18
|
+
* Tokens — ONE shipped look, dark "Forest". `.ahsuite-panel` inherits from
|
|
19
|
+
* `.ahsuite-bar`; `.ahsuite-docked` lives in the host's own header and so carries
|
|
20
|
+
* its own copy.
|
|
21
|
+
*
|
|
22
|
+
* These values were briefly served from the config endpoint, as a mode /
|
|
23
|
+
* accent_color / alignment block on the payload, and are baked into the release on
|
|
24
|
+
* purpose now. The bar paints a compiled-in fallback before the fetch resolves, so
|
|
25
|
+
* every served value that disagreed with the fallback's guess flashed on EVERY page
|
|
26
|
+
* load: a dark bar turning light, tiles jumping from left to centre. Colour and
|
|
27
|
+
* layout are release decisions, not operational switches. Only the app catalog is
|
|
28
|
+
* still configurable.
|
|
29
|
+
*
|
|
30
|
+
* There is no light palette and no `data-theme` hook any more. A second look would
|
|
31
|
+
* come back as a second release, not a second runtime branch.
|
|
21
32
|
* ------------------------------------------------------------------------- */
|
|
22
33
|
.ahsuite-bar,
|
|
23
34
|
.ahsuite-docked {
|
|
@@ -32,8 +43,6 @@
|
|
|
32
43
|
--ahsuite-chip-icon: #c4d2c8;
|
|
33
44
|
--ahsuite-chip-hover-bg: #ffffff2e;
|
|
34
45
|
--ahsuite-chip-hover-icon: #9fd9b4;
|
|
35
|
-
--ahsuite-chip-active-bg: transparent;
|
|
36
|
-
--ahsuite-chip-active-image: linear-gradient(180deg, #3dbe66, #2aa34f);
|
|
37
46
|
--ahsuite-chip-active-icon: #fff;
|
|
38
47
|
--ahsuite-label: #c4d2c8;
|
|
39
48
|
--ahsuite-label-active: #fff;
|
|
@@ -41,30 +50,7 @@
|
|
|
41
50
|
--ahsuite-surface-border: #ffffff1f;
|
|
42
51
|
--ahsuite-control-border: #ffffff2e;
|
|
43
52
|
--ahsuite-row-hover-bg: #ffffff1f;
|
|
44
|
-
--ahsuite-
|
|
45
|
-
--ahsuite-tiles-justify: flex-start;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.ahsuite-bar[data-theme='light'],
|
|
49
|
-
.ahsuite-docked[data-theme='light'] {
|
|
50
|
-
--ahsuite-bar-bg: #fff;
|
|
51
|
-
--ahsuite-bar-border: #e6e1d5;
|
|
52
|
-
--ahsuite-waffle-icon: #4a5951;
|
|
53
|
-
--ahsuite-waffle-hover-bg: #f1ede2;
|
|
54
|
-
--ahsuite-waffle-hover-icon: #2e9e58;
|
|
55
|
-
--ahsuite-wordmark: #12211a;
|
|
56
|
-
--ahsuite-divider: #e6e1d5;
|
|
57
|
-
--ahsuite-chip-bg: #f1ede2;
|
|
58
|
-
--ahsuite-chip-icon: #4a5951;
|
|
59
|
-
--ahsuite-chip-hover-bg: #e4f4e8;
|
|
60
|
-
--ahsuite-chip-hover-icon: #2e9e58;
|
|
61
|
-
--ahsuite-label: #4a5951;
|
|
62
|
-
--ahsuite-label-active: #12211a;
|
|
63
|
-
--ahsuite-surface-bg: #fff;
|
|
64
|
-
--ahsuite-surface-border: #e6e1d5;
|
|
65
|
-
--ahsuite-control-border: #e6e1d5;
|
|
66
|
-
--ahsuite-row-hover-bg: #f1ede2;
|
|
67
|
-
--ahsuite-accent-text: #2e9e58;
|
|
53
|
+
--ahsuite-eyebrow: #9fd9b4;
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
/* ---------------------------------------------------------------------------
|
|
@@ -143,6 +129,40 @@
|
|
|
143
129
|
visibility 0s linear 180ms;
|
|
144
130
|
}
|
|
145
131
|
|
|
132
|
+
/* ---------------------------------------------------------------------------
|
|
133
|
+
* True centring, in three flex children: brand | tiles | balance.
|
|
134
|
+
*
|
|
135
|
+
* The tile row used to be a direct child that grew into the row's leftover space and
|
|
136
|
+
* centred inside it. Because that leftover space starts AFTER the waffle, wordmark and
|
|
137
|
+
* divider, the tiles landed a constant 60.8px right of the bar's real centre — exactly
|
|
138
|
+
* half the width of that left block, at every viewport.
|
|
139
|
+
*
|
|
140
|
+
* So the left block is one flex child and an empty `.ahsuite-balance` mirrors it on the
|
|
141
|
+
* right. Both take `flex: 1 1 0%`, so they split the free space equally whatever the
|
|
142
|
+
* brand actually measures, and the content-sized tile row between them lands on the
|
|
143
|
+
* bar's centre line. The bar's own 10px gap appears once on each side of the row, so it
|
|
144
|
+
* cancels too — the maths is symmetric, not tuned.
|
|
145
|
+
*
|
|
146
|
+
* Neither balancing child may shrink the row's content out of the bar: their 0% basis
|
|
147
|
+
* gives them a scaled shrink factor of 0, so every pixel of negative free space comes
|
|
148
|
+
* off `.ahsuite-tiles` (which clips, see below) and the brand keeps its min-content
|
|
149
|
+
* width. Squeeze harder and the equal shares fall under the brand's min-content, the
|
|
150
|
+
* brand is clamped there, and the balance takes the remainder: the row slides toward
|
|
151
|
+
* the left rather than sitting under the wordmark. Clipped or left-ish, never overlapped.
|
|
152
|
+
* ------------------------------------------------------------------------- */
|
|
153
|
+
.ahsuite-brand {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 10px;
|
|
157
|
+
flex: 1 1 0%;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ahsuite-balance {
|
|
161
|
+
display: block;
|
|
162
|
+
flex: 1 1 0%;
|
|
163
|
+
min-width: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
146
166
|
.ahsuite-waffle {
|
|
147
167
|
display: inline-flex;
|
|
148
168
|
align-items: center;
|
|
@@ -193,15 +213,27 @@
|
|
|
193
213
|
}
|
|
194
214
|
|
|
195
215
|
/* ---------------------------------------------------------------------------
|
|
196
|
-
* Desktop tile row.
|
|
216
|
+
* Desktop tile row. Sized to its own content and centred by the two balancing
|
|
217
|
+
* siblings above, never by a justify-content it distributes itself.
|
|
197
218
|
* ------------------------------------------------------------------------- */
|
|
198
219
|
.ahsuite-tiles {
|
|
199
220
|
display: flex;
|
|
200
221
|
gap: 4px;
|
|
201
222
|
align-items: center;
|
|
202
|
-
|
|
223
|
+
/* Content-sized: `flex-grow: 0` is what makes the row the thing being centred rather
|
|
224
|
+
* than a slot that centres its own contents. Growing it would work out to the same
|
|
225
|
+
* centre line while the tiles fit, and clip symmetrically — half a tile at each end —
|
|
226
|
+
* the moment they do not. `flex-shrink: 1` with `min-width: 0` lets the row give up
|
|
227
|
+
* width under pressure so `overflow: hidden` clips the trailing tiles instead of
|
|
228
|
+
* pushing the row over the wordmark. */
|
|
229
|
+
flex: 0 1 auto;
|
|
230
|
+
justify-content: flex-start;
|
|
231
|
+
min-width: 0;
|
|
203
232
|
overflow: hidden;
|
|
204
|
-
|
|
233
|
+
/* 100%, not a fixed cap. This is the property the collapse animates (to 0), so it has
|
|
234
|
+
* to stay — but a fixed cap (it was 1200px) would stop the row reaching its content
|
|
235
|
+
* width on a wide viewport and drag it off centre. */
|
|
236
|
+
max-width: 100%;
|
|
205
237
|
opacity: 1;
|
|
206
238
|
transform: translateY(0);
|
|
207
239
|
transition:
|
|
@@ -281,13 +313,14 @@ span.ahsuite-row {
|
|
|
281
313
|
white-space: nowrap;
|
|
282
314
|
}
|
|
283
315
|
|
|
284
|
-
/*
|
|
285
|
-
*
|
|
316
|
+
/* The active chip's two-stop gradient is fixed. It used to be overridable by a single
|
|
317
|
+
* flat accent_color from the payload, which meant one config value could not describe
|
|
318
|
+
* the shipped look at all — it could only replace it, and did so a frame late. */
|
|
286
319
|
.ahsuite-chip--active,
|
|
287
320
|
.ahsuite-tile--active .ahsuite-chip--active,
|
|
288
321
|
.ahsuite-tile--active:hover .ahsuite-chip--active {
|
|
289
|
-
background-color:
|
|
290
|
-
background-image:
|
|
322
|
+
background-color: transparent;
|
|
323
|
+
background-image: linear-gradient(180deg, #3dbe66, #2aa34f);
|
|
291
324
|
color: var(--ahsuite-chip-active-icon);
|
|
292
325
|
}
|
|
293
326
|
|
|
@@ -402,7 +435,7 @@ span.ahsuite-row {
|
|
|
402
435
|
font-size: 11px;
|
|
403
436
|
font-weight: 600;
|
|
404
437
|
letter-spacing: 0.06em;
|
|
405
|
-
color: var(--ahsuite-
|
|
438
|
+
color: var(--ahsuite-eyebrow);
|
|
406
439
|
}
|
|
407
440
|
|
|
408
441
|
.ahsuite-close {
|
|
@@ -459,8 +492,8 @@ span.ahsuite-row {
|
|
|
459
492
|
}
|
|
460
493
|
|
|
461
494
|
.ahsuite-row:hover .ahsuite-chip--active {
|
|
462
|
-
background-color:
|
|
463
|
-
background-image:
|
|
495
|
+
background-color: transparent;
|
|
496
|
+
background-image: linear-gradient(180deg, #3dbe66, #2aa34f);
|
|
464
497
|
color: var(--ahsuite-chip-active-icon);
|
|
465
498
|
}
|
|
466
499
|
|
|
@@ -485,6 +518,17 @@ span.ahsuite-row {
|
|
|
485
518
|
display: inline-flex;
|
|
486
519
|
}
|
|
487
520
|
|
|
521
|
+
/* Nothing to centre once the tile row is gone, so the balancing pair stands down and
|
|
522
|
+
* the trigger sits beside the wordmark instead of floating in the middle of the bar.
|
|
523
|
+
* `.ahsuite-balance` keeps growing purely to hold the right-hand slack. */
|
|
524
|
+
.ahsuite-brand {
|
|
525
|
+
flex: 0 0 auto;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.ahsuite-balance {
|
|
529
|
+
flex: 1 1 auto;
|
|
530
|
+
}
|
|
531
|
+
|
|
488
532
|
.ahsuite-panel {
|
|
489
533
|
display: block;
|
|
490
534
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export type { NavContextValue } from './context.js';
|
|
|
9
9
|
export type { NavBarProps } from './nav-bar.js';
|
|
10
10
|
export type { NavTriggerProps } from './nav-trigger.js';
|
|
11
11
|
export type { NavProviderProps } from './provider.js';
|
|
12
|
-
export type {
|
|
12
|
+
export type { NavApp, NavAppClickArgs, NavConfigPayload, ResolvedNavApp } from './types.js';
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/nav-bar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-bar.d.ts","sourceRoot":"","sources":["../src/nav-bar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nav-bar.d.ts","sourceRoot":"","sources":["../src/nav-bar.tsx"],"names":[],"mappings":"AAsBA,UAAU,WAAW;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsED,iBAAS,MAAM,CAAC,EAAE,MAAc,EAAE,SAAS,EAAE,EAAE,WAAW,2CAkIzD;AAED,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/nav-bar.js
CHANGED
|
@@ -4,7 +4,19 @@ import { ChevronDown, X } from 'lucide-react';
|
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { useNav } from './context.js';
|
|
6
6
|
import { NavIcon, WaffleIcon } from './icons.js';
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The wordmark, compiled in. It used to come from the payload's `logo` block, which meant
|
|
9
|
+
* a static piece of chrome could disagree with the compiled-in fallback and shift on load
|
|
10
|
+
* like everything else fetched. Nobody renames the ArtHelper wordmark from an admin
|
|
11
|
+
* screen, so it belongs in the release.
|
|
12
|
+
*
|
|
13
|
+
* The href is the site root, NOT the resolved `home` catalog entry. Deriving it was
|
|
14
|
+
* considered and rejected twice over: it would put this back on config, and Home points at
|
|
15
|
+
* /community while the wordmark has always gone to the root — they are different
|
|
16
|
+
* destinations that happen to share a domain, not one destination expressed twice.
|
|
17
|
+
*/
|
|
18
|
+
const WORDMARK_LABEL = 'arthelper';
|
|
19
|
+
const WORDMARK_HREF = 'https://www.arthelper.com';
|
|
8
20
|
function chipClass(entry) {
|
|
9
21
|
return entry.active ? 'ahsuite-chip ahsuite-chip--active' : 'ahsuite-chip';
|
|
10
22
|
}
|
|
@@ -32,7 +44,7 @@ function Row({ entry, onActivate }) {
|
|
|
32
44
|
return (_jsxs("a", { "aria-current": entry.active ? 'page' : undefined, className: className, href: entry.app.href, onClick: () => onActivate(entry), children: [_jsx(Chip, { entry: entry }), _jsx("span", { className: "ahsuite-row-label", children: entry.app.label })] }));
|
|
33
45
|
}
|
|
34
46
|
function NavBar({ sticky = false, className }) {
|
|
35
|
-
const {
|
|
47
|
+
const { apps, activeApp, collapsed, toggle, activate } = useNav();
|
|
36
48
|
const [open, setOpen] = useState(false);
|
|
37
49
|
const panelRef = useRef(null);
|
|
38
50
|
const triggerRef = useRef(null);
|
|
@@ -85,7 +97,7 @@ function NavBar({ sticky = false, className }) {
|
|
|
85
97
|
setOpen(false);
|
|
86
98
|
}, [activate]);
|
|
87
99
|
const classes = ['ahsuite-bar', sticky ? 'ahsuite-bar--sticky' : '', className ?? ''].filter(Boolean).join(' ');
|
|
88
|
-
return (_jsxs("div", { "aria-label": "ArtHelper suite", className: classes, "data-collapsed": collapsed ? 'true' : 'false', "
|
|
100
|
+
return (_jsxs("div", { "aria-label": "ArtHelper suite", className: classes, "data-collapsed": collapsed ? 'true' : 'false', role: "navigation", children: [_jsxs("div", { className: "ahsuite-brand", children: [_jsx("button", { "aria-expanded": !collapsed, "aria-label": collapsed ? 'Show the suite bar' : 'Hide the suite bar', className: "ahsuite-waffle", onClick: toggle, type: "button", children: _jsx(WaffleIcon, {}) }), _jsx("a", { className: "ahsuite-wordmark", href: WORDMARK_HREF, children: WORDMARK_LABEL }), _jsx("span", { "aria-hidden": "true", className: "ahsuite-divider" })] }), _jsx("div", { className: "ahsuite-tiles", children: apps.map((entry) => (_jsx(Tile, { entry: entry, onActivate: onActivate }, entry.app.id))) }), _jsxs("button", { "aria-controls": "ahsuite-panel", "aria-expanded": open, "aria-haspopup": "true", className: "ahsuite-trigger", onClick: () => setOpen((current) => !current), ref: triggerRef, type: "button", children: [activeApp ? _jsx(Chip, { entry: activeApp }) : null, _jsx("span", { className: "ahsuite-trigger-label", children: activeApp?.app.label ?? 'Apps' }), _jsx(ChevronDown, { "aria-hidden": "true", className: "ahsuite-chevron", height: 18, strokeWidth: 2, width: 18 })] }), _jsx("span", { "aria-hidden": "true", className: "ahsuite-balance" }), _jsxs("div", { "aria-label": "Apps", className: "ahsuite-panel", hidden: !open, id: "ahsuite-panel", ref: panelRef, role: "dialog", children: [_jsxs("div", { className: "ahsuite-panel-head", children: [_jsx("span", { className: "ahsuite-panel-title", children: "Apps" }), _jsxs("button", { "aria-label": "Close", className: "ahsuite-close", onClick: () => setOpen(false), type: "button", children: [_jsx(X, { "aria-hidden": "true", className: "ahsuite-icon", height: 14, strokeWidth: 2, width: 14 }), _jsx("span", { children: "Close" })] })] }), apps.map((entry) => (_jsx(Row, { entry: entry, onActivate: onActivate }, entry.app.id)))] })] }));
|
|
89
101
|
}
|
|
90
102
|
export { NavBar };
|
|
91
103
|
//# sourceMappingURL=nav-bar.js.map
|
package/dist/nav-bar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-bar.js","sourceRoot":"","sources":["../src/nav-bar.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"nav-bar.js","sourceRoot":"","sources":["../src/nav-bar.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGjD;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAAG,WAAW,CAAC;AACnC,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAYlD,SAAS,SAAS,CAAC,KAAqB;IACtC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,cAAc,CAAC;AAC7E,CAAC;AAED,SAAS,IAAI,CAAC,EAAE,KAAK,EAA6B;IAChD,OAAO,CACL,eAAM,SAAS,EAAE,SAAS,CAAC,KAAK,CAAC,YAC/B,KAAC,OAAO,IAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,GAAI,GAC5B,CACR,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,IAAI,CAAC,EAAE,KAAK,EAAE,UAAU,EAA0E;IACzG,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,mCAAmC,CAAC,CAAC,CAAC,cAAc,CAAC;IAEtF,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,CACL,gCAAoB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,aACzE,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,GAAI,EACtB,eAAM,SAAS,EAAC,eAAe,YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAQ,IACnD,CACR,CAAC;IACJ,CAAC;IAED,OAAO,CACL,6BACgB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC/C,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EACpB,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,aAEhC,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,GAAI,EACtB,eAAM,SAAS,EAAC,eAAe,YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAQ,IACtD,CACL,CAAC;AACJ,CAAC;AAED,0FAA0F;AAC1F,SAAS,GAAG,CAAC,EAAE,KAAK,EAAE,UAAU,EAA0E;IACxG,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,aAAa,CAAC;IAEnF,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,CACL,gCAAoB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,aACzE,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,GAAI,EACtB,eAAM,SAAS,EAAC,mBAAmB,YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAQ,IACvD,CACR,CAAC;IACJ,CAAC;IAED,OAAO,CACL,6BACgB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAC/C,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EACpB,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,aAEhC,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,GAAI,EACtB,eAAM,SAAS,EAAC,mBAAmB,YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAQ,IAC1D,CACL,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,SAAS,EAAe;IACxD,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;IAClE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,MAAM,CAA2B,IAAI,CAAC,CAAC;IAE1D,+EAA+E;IAC/E,4CAA4C;IAC5C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,SAAS,GAAG,CAAC,KAAoB,EAAE,EAAE;YACzC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAC3B,OAAO;YACT,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,CAAC;YACf,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC,CAAC;QACF,MAAM,aAAa,GAAG,CAAC,KAAiB,EAAE,EAAE;YAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;YAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;YACT,CAAC;YACD,IAAI,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/E,OAAO;YACT,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QACF,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,QAAQ,CAAC,gBAAgB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QACtD,OAAO,GAAG,EAAE;YACV,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACnD,QAAQ,CAAC,mBAAmB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QAC3D,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,kFAAkF;IAClF,mFAAmF;IACnF,kFAAkF;IAClF,iFAAiF;IACjF,oFAAoF;IACpF,kFAAkF;IAClF,oFAAoF;IACpF,qFAAqF;IACrF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,KAAqB,EAAE,EAAE;QACxB,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC,EACD,CAAC,QAAQ,CAAC,CACX,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEhH,OAAO,CACL,6BACa,iBAAiB,EAC5B,SAAS,EAAE,OAAO,oBACF,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAC5C,IAAI,EAAC,YAAY,aAMjB,eAAK,SAAS,EAAC,eAAe,aAC5B,kCACiB,CAAC,SAAS,gBACb,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,oBAAoB,EACnE,SAAS,EAAC,gBAAgB,EAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAC,QAAQ,YAEb,KAAC,UAAU,KAAG,GACP,EAET,YAAG,SAAS,EAAC,kBAAkB,EAAC,IAAI,EAAE,aAAa,YAChD,cAAc,GACb,EAEJ,8BAAkB,MAAM,EAAC,SAAS,EAAC,iBAAiB,GAAG,IACnD,EAEN,cAAK,SAAS,EAAC,eAAe,YAC3B,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACnB,KAAC,IAAI,IAAC,KAAK,EAAE,KAAK,EAAqB,UAAU,EAAE,UAAU,IAApC,KAAK,CAAC,GAAG,CAAC,EAAE,CAA4B,CAClE,CAAC,GACE,EAEN,mCACgB,eAAe,mBACd,IAAI,mBACL,MAAM,EACpB,SAAS,EAAC,iBAAiB,EAC3B,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,EAC7C,GAAG,EAAE,UAAU,EACf,IAAI,EAAC,QAAQ,aAEZ,SAAS,CAAC,CAAC,CAAC,KAAC,IAAI,IAAC,KAAK,EAAE,SAAS,GAAI,CAAC,CAAC,CAAC,IAAI,EAC9C,eAAM,SAAS,EAAC,uBAAuB,YAAE,SAAS,EAAE,GAAG,CAAC,KAAK,IAAI,MAAM,GAAQ,EAC/E,KAAC,WAAW,mBAAa,MAAM,EAAC,SAAS,EAAC,iBAAiB,EAAC,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,GAAI,IAC9F,EAKT,8BAAkB,MAAM,EAAC,SAAS,EAAC,iBAAiB,GAAG,EAEvD,6BAAgB,MAAM,EAAC,SAAS,EAAC,eAAe,EAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAC,eAAe,EAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAC,QAAQ,aAC7G,eAAK,SAAS,EAAC,oBAAoB,aACjC,eAAM,SAAS,EAAC,qBAAqB,qBAAY,EACjD,gCAAmB,OAAO,EAAC,SAAS,EAAC,eAAe,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,EAAC,QAAQ,aAC/F,KAAC,CAAC,mBAAa,MAAM,EAAC,SAAS,EAAC,cAAc,EAAC,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,GAAI,EACxF,mCAAkB,IACX,IACL,EACL,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACnB,KAAC,GAAG,IAAC,KAAK,EAAE,KAAK,EAAqB,UAAU,EAAE,UAAU,IAApC,KAAK,CAAC,GAAG,CAAC,EAAE,CAA4B,CACjE,CAAC,IACE,IACF,CACP,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-trigger.d.ts","sourceRoot":"","sources":["../src/nav-trigger.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nav-trigger.d.ts","sourceRoot":"","sources":["../src/nav-trigger.tsx"],"names":[],"mappings":"AAKA,UAAU,eAAe;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,EAAE,eAAe,2CAqBjD;AAED,OAAO,EAAE,UAAU,EAAE,CAAC;AACtB,YAAY,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/nav-trigger.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useNav } from './context.js';
|
|
4
4
|
import { NavIcon, WaffleIcon } from './icons.js';
|
|
5
|
-
import { themeVars } from './theme.js';
|
|
6
5
|
/**
|
|
7
6
|
* The waffle plus active-app chip a host places by hand in its own header, beside
|
|
8
7
|
* its logo. This is the RESTORE control: while the bar is collapsed it is the only
|
|
@@ -13,9 +12,9 @@ import { themeVars } from './theme.js';
|
|
|
13
12
|
* than swapped — the bar's own waffle is the hide control.
|
|
14
13
|
*/
|
|
15
14
|
function NavTrigger({ className }) {
|
|
16
|
-
const {
|
|
15
|
+
const { activeApp, collapsed, toggle } = useNav();
|
|
17
16
|
const classes = ['ahsuite-docked', className ?? ''].filter(Boolean).join(' ');
|
|
18
|
-
return (_jsxs("button", { "aria-expanded": false, "aria-label": "Show the suite bar", className: classes, "data-collapsed": collapsed ? 'true' : 'false',
|
|
17
|
+
return (_jsxs("button", { "aria-expanded": false, "aria-label": "Show the suite bar", className: classes, "data-collapsed": collapsed ? 'true' : 'false', onClick: toggle, title: "Show the suite bar", type: "button", children: [_jsx("span", { className: "ahsuite-chip ahsuite-chip--active", children: _jsx(NavIcon, { name: activeApp?.app.icon ?? '' }) }), _jsx(WaffleIcon, { className: "ahsuite-waffle-mini" })] }));
|
|
19
18
|
}
|
|
20
19
|
export { NavTrigger };
|
|
21
20
|
//# sourceMappingURL=nav-trigger.js.map
|
package/dist/nav-trigger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nav-trigger.js","sourceRoot":"","sources":["../src/nav-trigger.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"nav-trigger.js","sourceRoot":"","sources":["../src/nav-trigger.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMjD;;;;;;;;GAQG;AACH,SAAS,UAAU,CAAC,EAAE,SAAS,EAAmB;IAChD,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IAElD,MAAM,OAAO,GAAG,CAAC,gBAAgB,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE9E,OAAO,CACL,mCACiB,KAAK,gBACT,oBAAoB,EAC/B,SAAS,EAAE,OAAO,oBACF,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAC5C,OAAO,EAAE,MAAM,EACf,KAAK,EAAC,oBAAoB,EAC1B,IAAI,EAAC,QAAQ,aAEb,eAAM,SAAS,EAAC,mCAAmC,YACjD,KAAC,OAAO,IAAC,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,GAAI,GACvC,EACP,KAAC,UAAU,IAAC,SAAS,EAAC,qBAAqB,GAAG,IACvC,CACV,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA8D,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMnG,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AAsBpF,UAAU,gBAAgB;IACxB,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,WAAW,EAAE,MAAM,CAAC;IACpB,+FAA+F;IAC/F,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,iGAAiG;IACjG,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,wGAAwG;IACxG,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iBAAS,WAAW,CAAC,EACnB,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,QAAQ,GACT,EAAE,gBAAgB,2CA+DlB;AAED,OAAO,EAAE,WAAW,EAAE,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
|
package/dist/provider.js
CHANGED
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { cached, fetchConfig } from './config.js';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { cached, fetchConfig, restored } from './config.js';
|
|
5
5
|
import { NavContext } from './context.js';
|
|
6
6
|
import { readCollapsed, writeCollapsed } from './cookie.js';
|
|
7
7
|
import { FALLBACK_CONFIG } from './fallback.js';
|
|
8
8
|
import { resolveApps } from './resolve.js';
|
|
9
|
+
/**
|
|
10
|
+
* useLayoutEffect on the client, useEffect on the server — the standard isomorphic-layout
|
|
11
|
+
* pattern, and it is load-bearing in BOTH directions here.
|
|
12
|
+
*
|
|
13
|
+
* It cannot be the useState initialiser. town-square server-renders this bar, the server
|
|
14
|
+
* cannot read the viewer's localStorage, and a client first render that disagrees with the
|
|
15
|
+
* server HTML is a hydration mismatch — a tested-clean property of that mount today.
|
|
16
|
+
*
|
|
17
|
+
* It cannot be a plain useEffect either. Passive effects are flushed on a scheduler
|
|
18
|
+
* callback that can land AFTER the browser has painted, which puts the wrong catalog on
|
|
19
|
+
* screen for a frame — precisely the flash this whole change exists to remove. Layout
|
|
20
|
+
* effects run synchronously after commit and before paint, so the state swap and its
|
|
21
|
+
* re-render are both done before anything is shown.
|
|
22
|
+
*
|
|
23
|
+
* Picking the hook by `typeof window` keeps React from warning that useLayoutEffect does
|
|
24
|
+
* nothing on the server. Hydration builds the client fiber tree fresh, so the two paths
|
|
25
|
+
* never have to agree on which effect flavour occupied the slot.
|
|
26
|
+
*/
|
|
27
|
+
const useBeforePaint = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
9
28
|
function NavProvider({ configUrl, activeAppId, onAppClick, fallbackConfig, initialCollapsed, children, }) {
|
|
10
29
|
const fallback = fallbackConfig ?? FALLBACK_CONFIG;
|
|
11
30
|
// Stale-while-revalidate: paint the last good payload (or the embedded fallback)
|
|
12
31
|
// synchronously, then swap in the fetched one. First paint never waits on the network.
|
|
13
32
|
const [config, setConfig] = useState(() => cached(configUrl) ?? fallback);
|
|
14
33
|
const [collapsed, setCollapsedState] = useState(() => initialCollapsed ?? readCollapsed() ?? false);
|
|
34
|
+
// Before the first paint, upgrade the compiled-in fallback to the last payload this
|
|
35
|
+
// browser actually saw. `cached` wins when it is populated: an in-memory hit means a
|
|
36
|
+
// fetch already landed in THIS page session, which is strictly fresher than the store.
|
|
37
|
+
useBeforePaint(() => {
|
|
38
|
+
if (cached(configUrl)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const stored = restored();
|
|
42
|
+
if (!stored) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setConfig(stored);
|
|
46
|
+
}, [configUrl]);
|
|
15
47
|
useEffect(() => {
|
|
16
48
|
let live = true;
|
|
17
49
|
void fetchConfig(configUrl).then((next) => {
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last good payload, persisted per browser.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the bar paints the compiled-in FALLBACK_CONFIG before the fetch
|
|
5
|
+
* resolves, and the catalog is the one field that can still legitimately differ from it —
|
|
6
|
+
* `enabled` states drift from the config row by design (see fallback.ts). Because the tile
|
|
7
|
+
* row is centred on the bar, that drift is expensive: every tile the fetch adds or removes
|
|
8
|
+
* shifts the whole row by half a tile, so a 2-tile fallback resolving to a 7-tile config
|
|
9
|
+
* moves the Home tile 230px. Persisting the last good payload makes the second and every
|
|
10
|
+
* subsequent visit paint the catalog the viewer actually saw last time.
|
|
11
|
+
*
|
|
12
|
+
* It cannot help the FIRST visit, and it cannot help the first paint of a server-rendered
|
|
13
|
+
* host: the server has no access to the browser's localStorage, so its HTML is always the
|
|
14
|
+
* compiled-in fallback. What this removes is the hydration-time swap on every return
|
|
15
|
+
* visit. See provider.tsx for how the read is sequenced against paint.
|
|
16
|
+
*
|
|
17
|
+
* EVERY access is wrapped, including the `localStorage` property read itself. Safari's
|
|
18
|
+
* private mode and Chrome with site data blocked throw a SecurityError on ACCESS, not just
|
|
19
|
+
* on setItem, so `typeof localStorage !== 'undefined'` is not a sufficient guard.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Versioned on the RENDERER's expected shape, not on the payload's `schema_version`. A
|
|
23
|
+
* renderer that changes what it stores bumps this key rather than trying to migrate, so
|
|
24
|
+
* an old blob is orphaned and ignored instead of misread. localStorage is origin-scoped
|
|
25
|
+
* and a host has exactly one configUrl, so the key needs no URL in it.
|
|
26
|
+
*/
|
|
27
|
+
declare const STORAGE_KEY = "ahsuite-nav-config-v1";
|
|
28
|
+
/** Beyond this a dormant browser revalidates rather than painting something ancient. */
|
|
29
|
+
declare const MAX_AGE_MS: number;
|
|
30
|
+
/**
|
|
31
|
+
* The stored payload, or null. Returns the RAW payload deliberately — the caller runs it
|
|
32
|
+
* through `normalize` exactly as it would a network body, because a blob written by an
|
|
33
|
+
* older renderer is untrusted input of precisely the same kind.
|
|
34
|
+
*/
|
|
35
|
+
declare function readStored(): unknown;
|
|
36
|
+
/** Best effort. A full quota or a blocked store is not a reason to fail the fetch. */
|
|
37
|
+
declare function writeStored(payload: unknown): void;
|
|
38
|
+
export { MAX_AGE_MS, STORAGE_KEY, readStored, writeStored };
|
|
39
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,QAAA,MAAM,WAAW,0BAA0B,CAAC;AAE5C,wFAAwF;AACxF,QAAA,MAAM,UAAU,QAA2B,CAAC;AAU5C;;;;GAIG;AACH,iBAAS,UAAU,IAAI,OAAO,CAuB7B;AAED,sFAAsF;AACtF,iBAAS,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAM3C;AAED,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last good payload, persisted per browser.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the bar paints the compiled-in FALLBACK_CONFIG before the fetch
|
|
5
|
+
* resolves, and the catalog is the one field that can still legitimately differ from it —
|
|
6
|
+
* `enabled` states drift from the config row by design (see fallback.ts). Because the tile
|
|
7
|
+
* row is centred on the bar, that drift is expensive: every tile the fetch adds or removes
|
|
8
|
+
* shifts the whole row by half a tile, so a 2-tile fallback resolving to a 7-tile config
|
|
9
|
+
* moves the Home tile 230px. Persisting the last good payload makes the second and every
|
|
10
|
+
* subsequent visit paint the catalog the viewer actually saw last time.
|
|
11
|
+
*
|
|
12
|
+
* It cannot help the FIRST visit, and it cannot help the first paint of a server-rendered
|
|
13
|
+
* host: the server has no access to the browser's localStorage, so its HTML is always the
|
|
14
|
+
* compiled-in fallback. What this removes is the hydration-time swap on every return
|
|
15
|
+
* visit. See provider.tsx for how the read is sequenced against paint.
|
|
16
|
+
*
|
|
17
|
+
* EVERY access is wrapped, including the `localStorage` property read itself. Safari's
|
|
18
|
+
* private mode and Chrome with site data blocked throw a SecurityError on ACCESS, not just
|
|
19
|
+
* on setItem, so `typeof localStorage !== 'undefined'` is not a sufficient guard.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Versioned on the RENDERER's expected shape, not on the payload's `schema_version`. A
|
|
23
|
+
* renderer that changes what it stores bumps this key rather than trying to migrate, so
|
|
24
|
+
* an old blob is orphaned and ignored instead of misread. localStorage is origin-scoped
|
|
25
|
+
* and a host has exactly one configUrl, so the key needs no URL in it.
|
|
26
|
+
*/
|
|
27
|
+
const STORAGE_KEY = 'ahsuite-nav-config-v1';
|
|
28
|
+
/** Beyond this a dormant browser revalidates rather than painting something ancient. */
|
|
29
|
+
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
30
|
+
function store() {
|
|
31
|
+
try {
|
|
32
|
+
return globalThis.localStorage ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The stored payload, or null. Returns the RAW payload deliberately — the caller runs it
|
|
40
|
+
* through `normalize` exactly as it would a network body, because a blob written by an
|
|
41
|
+
* older renderer is untrusted input of precisely the same kind.
|
|
42
|
+
*/
|
|
43
|
+
function readStored() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = store()?.getItem(STORAGE_KEY);
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const envelope = JSON.parse(raw);
|
|
50
|
+
if (typeof envelope !== 'object' || envelope === null) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const { saved_at: savedAt, payload } = envelope;
|
|
54
|
+
if (typeof savedAt !== 'number') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Absolute difference, so a clock moved BACKWARDS discards the entry rather than
|
|
58
|
+
// treating a future timestamp as permanently fresh.
|
|
59
|
+
if (Math.abs(Date.now() - savedAt) > MAX_AGE_MS) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return payload ?? null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Best effort. A full quota or a blocked store is not a reason to fail the fetch. */
|
|
69
|
+
function writeStored(payload) {
|
|
70
|
+
try {
|
|
71
|
+
store()?.setItem(STORAGE_KEY, JSON.stringify({ saved_at: Date.now(), payload }));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export { MAX_AGE_MS, STORAGE_KEY, readStored, writeStored };
|
|
78
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,GAAG,uBAAuB,CAAC;AAE5C,wFAAwF;AACxF,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5C,SAAS,KAAK;IACZ,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,YAAY,IAAI,IAAI,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,KAAK,EAAE,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,QAAqD,CAAC;QAC7F,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,iFAAiF;QACjF,oDAAoD;QACpD,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,UAAU,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,OAAO,IAAI,IAAI,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,WAAW,CAAC,OAAgB;IACnC,IAAI,CAAC;QACH,KAAK,EAAE,EAAE,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;AACH,CAAC;AAED,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,18 +5,21 @@
|
|
|
5
5
|
* never removed or repurposed. Renderers ignore fields they do not recognise and
|
|
6
6
|
* degrade rather than throw on a higher schema_version, so an asynchronously
|
|
7
7
|
* deployed fleet can run mixed renderer versions against one payload.
|
|
8
|
+
*
|
|
9
|
+
* The payload used to carry a `theme` block (mode, accent_color, alignment) and a `logo`
|
|
10
|
+
* block (label, href). Both are gone, for one reason: the bar paints the compiled-in
|
|
11
|
+
* fallback before the fetch resolves, so EVERY configurable field is a field that can
|
|
12
|
+
* disagree with the fallback's guess and flash on load. The look and the wordmark are
|
|
13
|
+
* release decisions — nobody renames the ArtHelper wordmark from an admin screen — so
|
|
14
|
+
* they are baked in (see index.css and nav-bar.tsx). The app catalog is the one genuine
|
|
15
|
+
* operational switch, and it is all that is left here.
|
|
16
|
+
*
|
|
17
|
+
* Dropping a field IS breaking under the additive contract, so schema_version stays at 1
|
|
18
|
+
* only because no stable consumer was ever released against the old shape — the only
|
|
19
|
+
* publishes are 0.1.0-alpha.0 and 0.1.0-alpha.1, and both tolerate the fields' absence.
|
|
20
|
+
* AFTER THE FIRST STABLE PUBLISH the same removal would have to bump schema_version and
|
|
21
|
+
* keep the old branch alive until the fleet has rolled.
|
|
8
22
|
*/
|
|
9
|
-
type NavThemeMode = 'dark' | 'light';
|
|
10
|
-
type NavAlignment = 'left' | 'centered';
|
|
11
|
-
interface NavTheme {
|
|
12
|
-
mode: NavThemeMode;
|
|
13
|
-
accent_color: string | null;
|
|
14
|
-
alignment: NavAlignment;
|
|
15
|
-
}
|
|
16
|
-
interface NavLogo {
|
|
17
|
-
label: string;
|
|
18
|
-
href: string;
|
|
19
|
-
}
|
|
20
23
|
interface NavApp {
|
|
21
24
|
id: string;
|
|
22
25
|
label: string;
|
|
@@ -26,8 +29,6 @@ interface NavApp {
|
|
|
26
29
|
}
|
|
27
30
|
interface NavConfigPayload {
|
|
28
31
|
schema_version: number;
|
|
29
|
-
theme: NavTheme;
|
|
30
|
-
logo: NavLogo;
|
|
31
32
|
apps: NavApp[];
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
@@ -46,5 +47,5 @@ interface NavAppClickArgs {
|
|
|
46
47
|
activeAppId: string;
|
|
47
48
|
collapsed: boolean;
|
|
48
49
|
}
|
|
49
|
-
export type {
|
|
50
|
+
export type { NavApp, NavAppClickArgs, NavConfigPayload, ResolvedNavApp };
|
|
50
51
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,UAAU,MAAM;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;GAIG;AACH,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,0FAA0F;AAC1F,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/types.js
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
* never removed or repurposed. Renderers ignore fields they do not recognise and
|
|
6
6
|
* degrade rather than throw on a higher schema_version, so an asynchronously
|
|
7
7
|
* deployed fleet can run mixed renderer versions against one payload.
|
|
8
|
+
*
|
|
9
|
+
* The payload used to carry a `theme` block (mode, accent_color, alignment) and a `logo`
|
|
10
|
+
* block (label, href). Both are gone, for one reason: the bar paints the compiled-in
|
|
11
|
+
* fallback before the fetch resolves, so EVERY configurable field is a field that can
|
|
12
|
+
* disagree with the fallback's guess and flash on load. The look and the wordmark are
|
|
13
|
+
* release decisions — nobody renames the ArtHelper wordmark from an admin screen — so
|
|
14
|
+
* they are baked in (see index.css and nav-bar.tsx). The app catalog is the one genuine
|
|
15
|
+
* operational switch, and it is all that is left here.
|
|
16
|
+
*
|
|
17
|
+
* Dropping a field IS breaking under the additive contract, so schema_version stays at 1
|
|
18
|
+
* only because no stable consumer was ever released against the old shape — the only
|
|
19
|
+
* publishes are 0.1.0-alpha.0 and 0.1.0-alpha.1, and both tolerate the fields' absence.
|
|
20
|
+
* AFTER THE FIRST STABLE PUBLISH the same removal would have to bump schema_version and
|
|
21
|
+
* keep the old branch alive until the fleet has rolled.
|
|
8
22
|
*/
|
|
9
23
|
export {};
|
|
10
24
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
|
package/package.json
CHANGED