@jsenv/navi 0.29.165 → 0.29.167

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.
@@ -69,6 +69,17 @@ consistency across the app, not from any single call site.
69
69
  collection (searching it is that collection's own `GET_MANY`). Companion
70
70
  files: `docs/actions.md`, `docs/resource_with_params.md`,
71
71
  `docs/resource_dependencies.md`.
72
+ - `docs/data_states.md` — what a screen draws while its data is loading,
73
+ missing, stale or failed: the three questions `data`, `loading` and `error`
74
+ answer separately, the four combinations of the first two and what each one
75
+ means, and the invariant that `loading: true` never suspends — so a component
76
+ drawing its own states is never taken away, with the dialog, the scroll
77
+ position and the retry button it was holding. Also why a skeleton is TOLD
78
+ whether it is loading rather than deducing it from emptiness, why an error
79
+ arrives beside the data rather than in place of it, and what `dismissError()`
80
+ does and does not do. Read it before drawing a skeleton on `!data`, before
81
+ hiding content because `error` is set, and before adding a state flag of your
82
+ own next to what the hook already returns.
72
83
  - `docs/list_refresh.md` — what a write sends back to the network and what stays
73
84
  on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
74
85
  what updates from a response without any request, `rerunOn` and its defaults,
@@ -331,9 +342,14 @@ consistency across the app, not from any single call site.
331
342
  restored on reload without a route and without an entry per step: a search
332
343
  param signal handed to a `SlideContainer`, walked to rather than jumped to,
333
344
  and `history: "push"` for the states whose values ARE places (with the write
334
- that says otherwise, `signal.set(v, { history })`). Read it before writing any
335
- routing code — the position of the user belongs in the URL by default, and
336
- that decision is not retrofittable.
345
+ that says otherwise, `signal.set(v, { history })`). It also holds the rule for
346
+ an OVERLAY — something opened from every screen and drawn over whatever the
347
+ reader was on: the URL may say what is drawn over the screen, it must never
348
+ name a place the reader is not at, which is why `/me/settings` is wrong for it
349
+ and `/places?settings` is right (the layer's address contains the address of
350
+ what it covers, so closing is exact even from a shared link). Read it before
351
+ writing any routing code — the position of the user belongs in the URL by
352
+ default, and that decision is not retrofittable.
337
353
  - Source code and demos on GitHub:
338
354
  https://github.com/jsenv/core/tree/main/packages/frontend/navi — where to go
339
355
  when the built export's JSDoc doesn't answer the question (see "Where the
package/docs/actions.md CHANGED
@@ -130,10 +130,12 @@ const [user] = useAsyncData(userAction);
130
130
 
131
131
  `useAsyncData` suspends until the data is there and throws on failure, leaving
132
132
  both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
133
- or `{ error: true }` to handle either inside the component (stale data stays
134
- available while a rerun is in flight). Where a failure goes when no screen takes
135
- it — and why a run settles with its error rather than rejecting — is
136
- [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
133
+ or `{ error: true }` to receive either as a value instead, the component staying
134
+ mounted throughout and keeping the data it already had. What it then draws for
135
+ each state — the four combinations of `data` and `loading`, why a skeleton is
136
+ told whether it is loading — is [data_states.md](./data_states.md). Where a
137
+ failure goes when no screen takes it — and why a run settles with its error
138
+ rather than rejecting — is [error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
137
139
  state at once — `{ idle, loading, completed, aborted, error, data, params }` —
138
140
  for a component that needs to look at it rather than render it. It reads that
139
141
  very instance: to know what moves when a control is handed the action, see
@@ -0,0 +1,147 @@
1
+ # The states of a screen's data
2
+
3
+ A screen reading async data draws one of a handful of states, and there are two
4
+ ways to get it wrong. Both are common, and both come from the same shortcut.
5
+
6
+ - **A wait announced over something that is not coming.** A skeleton drawn on
7
+ "there is no data" alone keeps shimmering after the load failed, and after the
8
+ params turned out to be false. It says "hold on", and there is nothing to hold
9
+ on for.
10
+ - **Content taken away because something behind it went wrong.** A list that
11
+ disappears because its refresh failed; a page replaced by a message while the
12
+ rows it had are still perfectly good, and still true a second ago.
13
+
14
+ The shortcut behind both: collapsing "loading", "empty" and "failed" into one
15
+ state — _not ready_ — and drawing one thing for it. They are three separate
16
+ questions, and a screen that draws honestly answers them separately:
17
+
18
+ - **is there something to show?** → `data`
19
+ - **is anything on its way?** → `loading`
20
+ - **did the last attempt fail?** → `error`
21
+
22
+ navi's part is to answer all three, at every render, and to keep answering them.
23
+ What is drawn for each is the component's, always: navi has no opinion on
24
+ whether a missing list is a skeleton, a sentence or an illustration.
25
+
26
+ ## Asking for a state means receiving it as a value
27
+
28
+ Without options, `useAsyncData` **delegates**: it suspends while the data is not
29
+ there and throws when the action failed, so the states are drawn by the
30
+ `<Loading>` and `<ErrorBoundary>` above rather than by the component.
31
+
32
+ Each option turns one of those from something done _to_ the component into a
33
+ value the component reads:
34
+
35
+ | passed | the wait | the failure |
36
+ | --------------- | ------------------------- | ---------------------------- |
37
+ | nothing | suspends into `<Loading>` | throws to `<ErrorBoundary>` |
38
+ | `loading: true` | `loading`, a boolean | throws |
39
+ | `error: true` | suspends | `error`, plus `dismissError` |
40
+ | both | `loading` | `error` |
41
+
42
+ The invariant that makes this worth relying on: **`loading: true` never
43
+ suspends.** Not while the action runs, not while a debounced binding is
44
+ settling, not while it holds params nobody has started yet, not after a failure
45
+ whose message was dismissed. Every one of those comes back as a value.
46
+
47
+ That is not a detail of the wait — it is what keeps the component alive.
48
+ Suspending does not draw a spinner over a subtree: it takes the subtree away and
49
+ puts the boundary's fallback in its place, and everything under it is remounted
50
+ when it comes back. A dialog open inside it closes, a scroll position is lost,
51
+ half-typed text is gone, and the retry button the component was offering
52
+ disappears along with the component. A component that said it draws its own
53
+ states is never taken away from the person looking at it.
54
+
55
+ `error: true` says the same thing about failure, and it has the same
56
+ consequence: the component stays, so it is still there to offer the way out.
57
+
58
+ A component that delegates nothing needs no `<Loading>` above it. One that
59
+ delegates its wait does, since that is where the suspension lands, and navi says
60
+ so rather than leaving the subtree blank.
61
+
62
+ ## `data` and `loading` are independent
63
+
64
+ They answer different questions, so all four combinations happen and each one
65
+ means something different:
66
+
67
+ | `data` | `loading` | what it is | what a screen draws |
68
+ | ----------- | --------- | ----------------------------- | ------------------------ |
69
+ | `undefined` | `true` | first load | a skeleton, animated |
70
+ | set | `true` | a refresh over a known answer | the content, marked busy |
71
+ | set | `false` | settled | the content |
72
+ | `undefined` | `false` | nothing here, nothing coming | an empty state |
73
+
74
+ Two consequences worth stating out loud.
75
+
76
+ **The emptiness test is `data === undefined`, never `loading`.** Reading
77
+ `loading` as "there is nothing to display" blanks the screen on every refresh —
78
+ for a checkbox ticked on one row of a list, the whole list goes.
79
+
80
+ **A skeleton is told whether it is loading; it does not deduce it.** The last
81
+ row of the table is the one that gets forgotten, and it is reachable: a first
82
+ load that failed — its message on screen or dismissed — and a route action whose
83
+ params getter returns false. A skeleton drawn on emptiness alone shimmers there
84
+ forever, claiming a load nobody started.
85
+
86
+ ```jsx
87
+ const [items, loading] = useAsyncData(ACTION, { loading: true });
88
+ if (items === undefined) {
89
+ return <ItemsSkeleton loading={loading} />;
90
+ }
91
+ return <ItemList items={items} busy={loading} />;
92
+ ```
93
+
94
+ navi's own placeholders take that flag rather than inventing it: `<Text loading>`
95
+ shimmers, `<Text skeleton>` is the same bar held still, and `<Text skeleton
96
+ loading={loading}>` is one that tells the truth either way.
97
+
98
+ **Knowing the count beforehand beats guessing it.** How many skeleton rows to
99
+ draw is a question the response answers too late; a screen that already knows —
100
+ a count carried by the parent resource, a total returned alongside a previous
101
+ page — says so, and `<List loading loadingSkeletonCount={0}>` draws the empty
102
+ state right away instead of three rows that will vanish. Nothing jumps when the
103
+ response lands.
104
+
105
+ ## An error is a message on the screen, not the screen
106
+
107
+ `error: true` hands the failure over **beside** the data the action already had,
108
+ not in place of it. A refresh that failed does not unmake the previous answer,
109
+ so a screen that had something to show keeps it and says the failure over it:
110
+
111
+ ```jsx
112
+ const [items, loading, error, dismissError] = useAsyncData(ACTION, {
113
+ loading: true,
114
+ error: true,
115
+ });
116
+ if (error && !items) {
117
+ return <ErrorScreen error={error} />; // nothing behind it: the message is the screen
118
+ }
119
+ return (
120
+ <>
121
+ {error && <ErrorStrip error={error} onClose={dismissError} />}
122
+ <ItemList items={items} busy={loading} />
123
+ </>
124
+ );
125
+ ```
126
+
127
+ `dismissError()` removes the message and nothing else — nothing is asked again,
128
+ because nothing had to be: what closing it reveals was already there. What comes
129
+ back is the same state minus the error, which is why the two branches above stay
130
+ honest afterwards.
131
+
132
+ Taking the whole subtree away is `<ErrorBoundary>`'s job and it is a different
133
+ decision: reach for it when the failure means the screen genuinely cannot be
134
+ drawn, not as a stronger version of the same thing. Which of the two, and where
135
+ an error goes when neither takes it, is
136
+ [error_handling.md](./error_handling.md).
137
+
138
+ ## Reference
139
+
140
+ - [error_handling.md](./error_handling.md) — the two kinds of error, where each
141
+ is shown, and what happens to one nobody displays.
142
+ - [list_refresh.md](./list_refresh.md) — the same states for a list, plus what a
143
+ write puts back on screen with no request at all.
144
+ - [actions.md](./actions.md#reading-an-action) — reading an action versus
145
+ running one, `{ run: true }`, `{ onLoad }`.
146
+ - `src/state/demos/use_async_data_demo.html` — one card per shape: delegated,
147
+ loading inline, both inline, owning its request.
@@ -140,6 +140,15 @@ const [game, loading, error, dismissError] = useAsyncData(gameAction, {
140
140
  });
141
141
  ```
142
142
 
143
+ `game` and `error` arrive **together**: a refresh that failed does not unmake
144
+ the answer the action already had, so the screen keeps what it has and says the
145
+ failure on it, and `dismissError()` takes that message away without asking
146
+ anything again. That is the whole difference between the two shapes — a boundary
147
+ replaces the subtree by definition, this one writes on it — so reach for the
148
+ boundary when the failure means the screen cannot be drawn at all, and for this
149
+ when it means one thing about it is out of date. How the component draws the
150
+ states it is handed is [data_states.md](./data_states.md).
151
+
143
152
  And one row rather than the whole list: `<List.Item error>` replaces the row's
144
153
  content with the message — what the row stood for did not happen, so drawing it
145
154
  as if it had would be a lie.
@@ -27,13 +27,15 @@ The short answer:
27
27
  | never completed yet | `undefined` | `true` |
28
28
  | running after success | previous | `true` |
29
29
  | completed | fresh | `false` |
30
+ | failed after success | previous | `false` |
31
+ | failed, error closed | `undefined` | `false` |
30
32
 
31
33
  So the emptiness test is `data === undefined`, never `loading`:
32
34
 
33
35
  ```js
34
36
  const [items, loading] = useAsyncData(ACTION, { loading: true });
35
37
  if (items === undefined) {
36
- return null; // first load: there is nothing to show yet
38
+ return <RadarListSkeleton loading={loading} />; // nothing to show yet
37
39
  }
38
40
  return <RadarList radars={items} busy={loading} />; // re-read: stay on screen
39
41
  ```
@@ -48,10 +50,23 @@ if (loading) {
48
50
  Read `loading` as "what you are displaying is from before", not "there is
49
51
  nothing to display".
50
52
 
53
+ The two answer different questions — _is there anything to show_ and _is
54
+ anything on its way_ — so a screen reads both, and a skeleton is given `loading`
55
+ rather than deducing it from emptiness. The four combinations, and what each one
56
+ draws, are in [data_states.md](./data_states.md).
57
+
51
58
  `<List loading>` is the first-load answer, not the refresh one: it replaces the
52
59
  rows with skeletons. Pass it while stale rows exist and they disappear — same
53
60
  mistake as `loading ? null :`, one level down.
54
61
 
62
+ The `failed after success` row is the same rule at the other end: a refresh that
63
+ failed does not unmake the rows either. With `error: true` the failure comes
64
+ back **beside** them, so the list stays and the failure is said over it — a
65
+ strip above the rows, a retry, `dismissError()` to close the strip without
66
+ asking anything again. Taking the rows away is `<ErrorBoundary>`'s job, and only
67
+ when the screen genuinely cannot be drawn — see
68
+ [data_states.md](./data_states.md).
69
+
55
70
  ## What updates without a request
56
71
 
57
72
  An action's `data` is a computation over the resource store, not a snapshot of
@@ -19,6 +19,14 @@ linking to them, and turning them into tabs.
19
19
  - [Tabs that are not routes](#tabs-that-are-not-routes)
20
20
  - [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
21
21
  - [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
22
+ - [A layer over the screen: what its address may say](#a-layer-over-the-screen-what-its-address-may-say)
23
+ - [Why a layer is a routing question at all](#why-a-layer-is-a-routing-question-at-all)
24
+ - [`/me/settings` names a place the reader is not at](#mesettings-names-a-place-the-reader-is-not-at)
25
+ - [It is not "use a search param"](#it-is-not-use-a-search-param)
26
+ - [What the search param buys: the way back is in the address](#what-the-search-param-buys-the-way-back-is-in-the-address)
27
+ - [The wiring](#the-wiring)
28
+ - [Places inside the layer](#places-inside-the-layer)
29
+ - [What it costs](#what-it-costs)
22
30
 
23
31
  ## The rule that decides everything else: the position belongs in the URL
24
32
 
@@ -747,3 +755,162 @@ not one. What travels is the box, and the address is a label on where the box
747
755
  stands. A position several parts of the app must react to is still a route.
748
756
 
749
757
  Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
758
+
759
+ ## A layer over the screen: what its address may say
760
+
761
+ Some things are opened from everywhere and drawn OVER whatever the reader was
762
+ looking at: the settings behind a gear in the top bar, a photo, a help sheet.
763
+ Closing one puts the reader back exactly where they were — not on a page the app
764
+ picked, on the one they had. And the address has to hold it, for the same reasons
765
+ every other position does: a reload lands on it, a link opens on it, the back
766
+ button closes it.
767
+
768
+ ### Why a layer is a routing question at all
769
+
770
+ Layout should not decide routing. But routing decides a great deal of what is on
771
+ screen: which page is mounted, whose data loads, which bar entry lights up, which
772
+ movement plays between two screens. An address is not a label — it is read by
773
+ everything — so a layer, which is a pure fact of layout, cannot be given just any
774
+ address.
775
+
776
+ The rule that falls out of it, and the whole of this section:
777
+
778
+ > The URL may say what is drawn OVER the screen. It must never name a place the
779
+ > reader is not at.
780
+
781
+ ### `/me/settings` names a place the reader is not at
782
+
783
+ Settings opened from anywhere and drawn over anything are not inside `/me`, even
784
+ when they conceptually belong to "me". Written `/me/settings`, the router
785
+ believes otherwise, and everything that reads the router inherits the belief:
786
+
787
+ - the bar entry for the "me" section lights up while the settings cover a game;
788
+ - `/me` matches, so the screen the reader was on is REPLACED. The covering is a
789
+ fiction that lasts as long as the animation, and the page beneath is gone;
790
+ - a route transition plays a crossing between two pages that never crossed, and
791
+ its back half plays on every way out — the sheet lifts to reveal a page that
792
+ was never underneath;
793
+ - and it is wrong on the way out too: closing does not return to `/me`, it
794
+ returns to wherever the reader was, which the address never said.
795
+
796
+ These do not get fixed one at a time. `currentExcept` stops one link from
797
+ claiming "you are here"; a `"none"` relation silences one crossing; a hand-rolled
798
+ `active` replaces one reading. Each patches a reader of the lie without removing
799
+ it, and the next reader arrives quietly wrong.
800
+
801
+ ### It is not "use a search param"
802
+
803
+ `/settings` at the top level does not claim to be inside `/me` — it is honest
804
+ about not being there, and every symptom above goes with it. What it still is,
805
+ though, is a PAGE: it names a screen, so the screen beneath is gone, and closing
806
+ has nowhere written to return to. `whatever/settings` is the same answer in a
807
+ longer word.
808
+
809
+ So the rule is not about search params. It is about not claiming a position. A
810
+ path can be honest; what a path cannot do is carry what is underneath.
811
+
812
+ ### What the search param buys: the way back is in the address
813
+
814
+ `/places?settings` says two things at once — which screen the reader is on, and
815
+ what is drawn over it. That is what makes closing exact from a cold start: a
816
+ reload, a link someone sent, a new tab. The layer's address CONTAINS the address
817
+ of what it covers, so "close" needs no memory at all.
818
+
819
+ `/settings` has to remember instead. In practice the browser often does — after a
820
+ reload the session's history is still there, so a back-press lands somewhere —
821
+ but that is the BROWSER's memory of this tab, not the address. A link someone
822
+ else opens has none of it, and neither does a bookmark. Only the address travels,
823
+ which is what makes the param shape the right model rather than the convenient
824
+ one.
825
+
826
+ ### The wiring
827
+
828
+ ```js
829
+ const settingsSignal = stateSignal(false, {
830
+ id: "settings",
831
+ type: "boolean",
832
+ weak: true,
833
+ history: "push",
834
+ });
835
+ export const ROOT_ROUTE = route("/", {
836
+ searchParams: { settings: settingsSignal },
837
+ });
838
+ ```
839
+
840
+ ```jsx
841
+ <Button command="--navi-toggle" commandFor="settings_panel">⚙</Button>
842
+ …
843
+ <SidePanel id="settings_panel" signal={settingsSignal} side="top" expandY />
844
+ ```
845
+
846
+ Every piece of it answers something:
847
+
848
+ - **declared on the ROOT route**, so the layer opens over every screen. Declared
849
+ on one route it would only exist there, and a door in the furniture is on every
850
+ screen ([Search params](#search-params));
851
+ - **`weak`**, so a link built to a page never inherits a layer that happens to be
852
+ open. A layer qualifies one visit; it is not part of anyone's address;
853
+ - **`history: "push"`**, so the layer is a place one came from and the back button
854
+ closes it. The closing itself is never an entry of its own — see
855
+ [popup_open.md](./popup_open.md), which owns what a `signal` bound to a URL
856
+ writes on open, on close, and on cancel;
857
+ - **the panel is rendered outside the page area**, next to the router rather than
858
+ inside it. It is drawn over the pages and the fixed bars alike, and it stays
859
+ mounted while closed so it has somewhere to animate from;
860
+ - **`expandY`**, because a sheet is content-tall by default and a layer covers
861
+ the screen. It is also what any tab row inside it must NOT be given: `expand`
862
+ is both axes, and in the panel's own column that makes the row eat the height
863
+ and push everything below it off screen;
864
+ - **no `defineRouteTransition` for it.** Nothing crosses: the page beneath does
865
+ not change, so there is no pair of pages and no movement between them. A layer
866
+ has an entrance of its own, and route transitions are for pages replacing pages
867
+ ([route_transitions.md](./route_transitions.md)).
868
+
869
+ ### Places inside the layer
870
+
871
+ A layer big enough to have tabs holds them the way a wizard does — a search param
872
+ of its own, `oneOf` its pages, replacing rather than pushing, so the back button
873
+ closes the layer instead of stepping back one tab. Everything in
874
+ [A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
875
+ applies unchanged, and a layer is exactly the case that section names for
876
+ `SlideContainer` over `RouteTravel`: a popup lives in the browser's top layer,
877
+ where two pages cannot stand side by side.
878
+
879
+ ```js
880
+ const settingsTabSignal = stateSignal("account", {
881
+ id: "settings_tab",
882
+ oneOf: ["account", "alerts", "advanced"],
883
+ weak: true,
884
+ });
885
+ ```
886
+
887
+ The address then grows and shrinks with what the reader does: `/me` closed,
888
+ `/me?settings` open on the tab it opens on, `/me?settings&settings_tab=alerts`
889
+ one tab further, and back to `/me?settings` returning to the first.
890
+
891
+ Two params, because they answer two questions — is the layer there, and which of
892
+ its pages is shown. One param whose value is "which page of the layer, or none"
893
+ (`?settings=alerts`) is arguably the shape the address wants, and it is not
894
+ expressible today: a popup says its open state as a boolean, so it would
895
+ overwrite the tab on every open.
896
+
897
+ ### What it costs
898
+
899
+ This is a compromise, and it is worth stating rather than discovering:
900
+
901
+ - **the page beneath stays mounted and alive under an opaque layer.** Its actions
902
+ keep running, refreshing, retrying, for something nobody can see. Nothing tells
903
+ a route "you are covered", and nothing should be inferred from a layer being
904
+ open — the reader is still on that page, and will be back on it in a moment;
905
+ - **the router knows nothing about layers.** `<Route>` renders pages; the layer is
906
+ drawn beside them by the application. The address is honest about it, but "over"
907
+ is not a routing concept, and no `<Route>` shape expresses it;
908
+ - **the app really is on the page underneath**, which is right for `aria-current`,
909
+ for the bar, for a link built while the layer is open — and is exactly wrong for
910
+ anything wanting "the settings are what is current". If something needs to
911
+ answer that, it reads the layer's own state, not the router.
912
+
913
+ Demo:
914
+ [../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html](../src/nav/demos/route_transition_fixed_bars/route_transition_fixed_bars.html)
915
+ — a bar holding both doors side by side: notifications, a page that replaces the
916
+ screen with a route transition, and settings, a layer that covers it.
@@ -533,7 +533,10 @@ is nav state by construction.
533
533
 
534
534
  The two meet when the signal IS a route's: a search-param `stateSignal` given to
535
535
  `signal` puts the open state in the address itself, where a link can point at
536
- it.
536
+ it. That is also the shape a popup takes when it is a LAYER over the screen —
537
+ settings opened from every screen and closed back onto the one the reader was on
538
+ — and what the address may and may not claim there is a decision of its own:
539
+ [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
537
540
 
538
541
  ### `open`, and what it costs
539
542
 
@@ -65,6 +65,14 @@ happens to be everywhere. Write it for a page whose door really is furniture; a
65
65
  page reached from a screen has that screen to be paired with, and the pair says
66
66
  more.
67
67
 
68
+ **Such a page is still a PAGE**, and that is the question to settle before
69
+ writing the relation: it takes the screen's place, and the reader comes back to
70
+ whatever the router puts there — not necessarily to what they were on. A door in
71
+ the furniture whose destination must be drawn OVER the screen, and must give it
72
+ back exactly on closing, is not a page at all: it is a layer, its address says so
73
+ differently, and no relation is written for it. See
74
+ [navigation.md](./navigation.md#a-layer-over-the-screen-what-its-address-may-say).
75
+
68
76
  ## Choosing a movement
69
77
 
70
78
  - **`slide-x`** — going INTO something: a list item opened, a card followed, a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.165",
3
+ "version": "0.29.167",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {