@jsenv/navi 0.29.11 → 0.29.12
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 +9 -0
- package/dist/jsenv_navi.js +180 -15
- package/dist/jsenv_navi.js.map +6 -5
- package/dist/jsenv_navi_side_effects.js +8 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +9 -0
- package/docs/actions.md +250 -0
- package/docs/resource.md +267 -0
- package/docs/resource_dependencies.md +103 -0
- package/docs/resource_with_params.md +80 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,15 @@ Routes are flexible: you can create route groups to share logic, state, or UI ac
|
|
|
12
12
|
|
|
13
13
|
Actions are async operations with lifecycle management — pending, success, error. You can declare actions that run when navigating to a route, and any component can subscribe to them via `useAsyncData` to reflect what is happening: loading states, results, errors. No manual wiring.
|
|
14
14
|
|
|
15
|
+
## REST state
|
|
16
|
+
|
|
17
|
+
`resource()` turns a REST endpoint into a reactive store: one action per verb, a
|
|
18
|
+
shared signal store every component reads from, and automatic invalidation
|
|
19
|
+
between related actions. Parent/child relations are first-class — `.one`,
|
|
20
|
+
`.many`, `.scopedOne`, `.scopedMany` model a backend sub-resource such as
|
|
21
|
+
`/games/:id/candidates` rather than leaving you to hand-roll it. See
|
|
22
|
+
[docs/resource.md](./docs/resource.md).
|
|
23
|
+
|
|
15
24
|
## Layout & Typography
|
|
16
25
|
|
|
17
26
|
**`Box`** is the main layout primitive. It wraps CSS Flexbox with a friendlier API: `flex` for horizontal layout, `flex="y"` for vertical (no more guessing what `flex-direction: column` does visually). Supports `grid`, `inline`, alignment via `alignX`/`alignY`, and spacing props.
|
package/dist/jsenv_navi.js
CHANGED
|
@@ -29676,6 +29676,36 @@ const debug$2 = (args) => {
|
|
|
29676
29676
|
}
|
|
29677
29677
|
};
|
|
29678
29678
|
|
|
29679
|
+
/**
|
|
29680
|
+
* Creates a reactive REST resource backed by a shared signal store.
|
|
29681
|
+
* Returns a `stateFacade` exposing one action per REST callback provided
|
|
29682
|
+
* (`USER.GET`, `USER.GET_MANY`, `USER.POST`, …) plus `.withParams()` and the
|
|
29683
|
+
* relationship methods `.one()`, `.many()`, `.scopedOne()`, `.scopedMany()`.
|
|
29684
|
+
*
|
|
29685
|
+
* Each REST callback receives the params passed to the action call and must return
|
|
29686
|
+
* the data that will be upserted into the store:
|
|
29687
|
+
* - GET / POST / PUT / PATCH → the full item object, e.g. `{ id, name }`
|
|
29688
|
+
* - DELETE → the id or `{ id }` of the removed item
|
|
29689
|
+
* - GET_MANY / POST_MANY / … → an array of item objects
|
|
29690
|
+
*
|
|
29691
|
+
* A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
|
|
29692
|
+
* relationship method, never as an `op`/`type` discriminator dispatched inside one
|
|
29693
|
+
* verb's callback.
|
|
29694
|
+
*
|
|
29695
|
+
* @param {string} name - resource name, used in action names and error messages
|
|
29696
|
+
* @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
29697
|
+
* @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"` (or the first `uniqueKeys` entry)
|
|
29698
|
+
* @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
|
|
29699
|
+
* @see docs/resource.md — relationships, callback return contracts, decision table
|
|
29700
|
+
*
|
|
29701
|
+
* @example
|
|
29702
|
+
* const USER = resource("user", {
|
|
29703
|
+
* GET: ({ id }) => fetchJson(`/users/${id}`),
|
|
29704
|
+
* GET_MANY: () => fetchJson(`/users`),
|
|
29705
|
+
* POST: (user) => fetchJson(`/users`, { method: "POST", body: user }),
|
|
29706
|
+
* DELETE: ({ id }) => fetchJson(`/users/${id}`, { method: "DELETE" }),
|
|
29707
|
+
* });
|
|
29708
|
+
*/
|
|
29679
29709
|
const resource = (
|
|
29680
29710
|
name,
|
|
29681
29711
|
{
|
|
@@ -29830,7 +29860,7 @@ const createResource = (
|
|
|
29830
29860
|
* @param {Object} params - Parameters to bind to all actions of this resource (required)
|
|
29831
29861
|
* @param {Object} options - Additional options for the parameterized resource
|
|
29832
29862
|
* @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
|
|
29833
|
-
* @see
|
|
29863
|
+
* @see docs/resource_with_params.md for detailed documentation and examples
|
|
29834
29864
|
*
|
|
29835
29865
|
* @example
|
|
29836
29866
|
* const ROLE = resource("role", { GET: (params) => fetchRole(params) });
|
|
@@ -29877,6 +29907,31 @@ const createResource = (
|
|
|
29877
29907
|
};
|
|
29878
29908
|
stateFacade.withParams = withParams;
|
|
29879
29909
|
|
|
29910
|
+
/**
|
|
29911
|
+
* Links a property on each item to a single item in an independent child store.
|
|
29912
|
+
* The property is reactive: updating the child item anywhere propagates immediately.
|
|
29913
|
+
* The child resource exists independently — it is not owned by, nor deleted with, the parent.
|
|
29914
|
+
*
|
|
29915
|
+
* Use it when the child is a first-class entity with its own store, shared across
|
|
29916
|
+
* parents (a user referenced by many games). When the child only exists inside its
|
|
29917
|
+
* owner, use `.scopedOne()` instead.
|
|
29918
|
+
*
|
|
29919
|
+
* Callback return contracts:
|
|
29920
|
+
* - GET / PUT → the parent object with the relationship nested inside:
|
|
29921
|
+
* `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
|
|
29922
|
+
* - DELETE → the parent id (or `{ id }`); the property is set to `null`
|
|
29923
|
+
*
|
|
29924
|
+
* The backend may also embed the child inline in a parent GET/POST response — the
|
|
29925
|
+
* setter on the property upserts the nested object into the child store.
|
|
29926
|
+
*
|
|
29927
|
+
* Returns the child relationship resource, itself chainable:
|
|
29928
|
+
* `USER_SESSION.one("device", DEVICE)` adds a reactive `.device` property to each session.
|
|
29929
|
+
*
|
|
29930
|
+
* @param {string} propertyName - property holding the child on each parent item
|
|
29931
|
+
* @param {Object} childResource - the independent resource created by `resource()`
|
|
29932
|
+
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
|
|
29933
|
+
* @see docs/resource.md
|
|
29934
|
+
*/
|
|
29880
29935
|
stateFacade.one = (
|
|
29881
29936
|
propertyName,
|
|
29882
29937
|
childResource,
|
|
@@ -30044,6 +30099,29 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
30044
30099
|
});
|
|
30045
30100
|
};
|
|
30046
30101
|
|
|
30102
|
+
/**
|
|
30103
|
+
* Links a property on each item to an array of items in an independent child store.
|
|
30104
|
+
* Items in the array are full entries in the shared child store — if the same item is
|
|
30105
|
+
* referenced by several parents, a single update propagates to all of them.
|
|
30106
|
+
*
|
|
30107
|
+
* Use it when children are first-class entities shared across parents (a game's players,
|
|
30108
|
+
* who are users). When the children only exist inside their owner, or when the relation
|
|
30109
|
+
* itself carries fields (`seen_at`, `slot`), use `.scopedMany()` instead.
|
|
30110
|
+
*
|
|
30111
|
+
* Callback return contracts:
|
|
30112
|
+
* - GET_MANY → the parent object with the array nested inside:
|
|
30113
|
+
* `async ({ id }) => ({ id, friends: [{ id: 2 }, { id: 3 }] })` — a full-parent
|
|
30114
|
+
* response is absorbed as-is; the array replaces the relationship
|
|
30115
|
+
* - GET / POST / PUT / PATCH → the child object; it is upserted into the child store
|
|
30116
|
+
* but does NOT join the parent's array, which only a GET_MANY refresh changes
|
|
30117
|
+
* - DELETE → `[parentId, childId]`
|
|
30118
|
+
* - DELETE_MANY → `[parentId, [childId, childId, …]]`
|
|
30119
|
+
*
|
|
30120
|
+
* @param {string} propertyName - property holding the child array on each parent item
|
|
30121
|
+
* @param {Object} childResource - the independent resource created by `resource()`
|
|
30122
|
+
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
30123
|
+
* @see docs/resource.md
|
|
30124
|
+
*/
|
|
30047
30125
|
stateFacade.many = (
|
|
30048
30126
|
propertyName,
|
|
30049
30127
|
childResource,
|
|
@@ -30340,6 +30418,29 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
30340
30418
|
});
|
|
30341
30419
|
};
|
|
30342
30420
|
|
|
30421
|
+
/**
|
|
30422
|
+
* Attaches a single private sub-object to each item. The child has no identity outside
|
|
30423
|
+
* its owner and is not shared across items; each owner gets its own private signal.
|
|
30424
|
+
*
|
|
30425
|
+
* Use it for a sub-resource the backend exposes under the parent (`/users/:id/profile`)
|
|
30426
|
+
* whose content is meaningless without that parent.
|
|
30427
|
+
*
|
|
30428
|
+
* All callbacks must return `[ownerId, props | null]`:
|
|
30429
|
+
* - `GET: async ({ id }) => [id, { bio: "Hello", avatar: "alice.png" }]`
|
|
30430
|
+
* - `PATCH: async ({ id, bio }) => [id, { bio, avatar: "alice.png" }]`
|
|
30431
|
+
* - `DELETE: async ({ id }) => [id, null]`
|
|
30432
|
+
*
|
|
30433
|
+
* `ownerId` may also be `{ [uniqueKey]: value }` when the owner is known by an alternate key.
|
|
30434
|
+
* The property is `null` until a callback provides data; setting it to `null` clears it.
|
|
30435
|
+
* Mutations apply directly to the owner's signal, so the parent GET is never rerun.
|
|
30436
|
+
*
|
|
30437
|
+
* Returns the child relationship resource, itself chainable:
|
|
30438
|
+
* `USER_PROFILE.one("theme", THEME)` adds a reactive `.theme` property on each profile.
|
|
30439
|
+
*
|
|
30440
|
+
* @param {string} propertyName - property holding the sub-object on each owner item
|
|
30441
|
+
* @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, POST, PUT, PATCH, DELETE }`
|
|
30442
|
+
* @see docs/resource.md
|
|
30443
|
+
*/
|
|
30343
30444
|
stateFacade.scopedOne = (
|
|
30344
30445
|
propertyName,
|
|
30345
30446
|
{
|
|
@@ -30461,6 +30562,36 @@ ${originalActionName} source location: ${locationInfo}`,
|
|
|
30461
30562
|
return childResource;
|
|
30462
30563
|
};
|
|
30463
30564
|
|
|
30565
|
+
/**
|
|
30566
|
+
* Attaches a private ordered collection of sub-objects to each item. The child objects
|
|
30567
|
+
* have no identity outside their owner — two owners can hold items with the same id that
|
|
30568
|
+
* are completely independent. Each owner gets its own private arraySignalStore.
|
|
30569
|
+
*
|
|
30570
|
+
* This is the shape for a backend sub-route (`/games/:id/candidates`,
|
|
30571
|
+
* `…/candidates/:userId/accept`) and for a relation carrying its own fields
|
|
30572
|
+
* (`candidate_since`, `seen_at`): those fields belong to the pair, not to a shared child
|
|
30573
|
+
* store where they would corrupt the entity for every other reader.
|
|
30574
|
+
*
|
|
30575
|
+
* All callbacks must return `[ownerId, ...rest]`:
|
|
30576
|
+
* - `GET_MANY: async ({ id }) => [id, [{ name: "id", type: "int" }, …]]` — replaces the collection
|
|
30577
|
+
* - `POST: async ({ id, name, type }) => [id, { name, type }]`
|
|
30578
|
+
* - `PUT: async ({ id, oldName, name, type }) => [id, oldName, { name, type }]` (id rename)
|
|
30579
|
+
* - `DELETE: async ({ id, name }) => [id, name]`
|
|
30580
|
+
* - `*_MANY: [ownerId, itemArray]` — any plural verb replaces the whole collection,
|
|
30581
|
+
* which is how a backend answering a sub-route with the refreshed parent is absorbed
|
|
30582
|
+
*
|
|
30583
|
+
* `ownerId` may also be `{ [uniqueKey]: value }` when the owner is known by an alternate key.
|
|
30584
|
+
* A singular POST upserts the child but does not append it to the collection; ordering is
|
|
30585
|
+
* the backend's, so the owner's GET is rerun instead (only when its last response embedded
|
|
30586
|
+
* `propertyName`), and the child's own GET_MANY reruns per its `rerunOn`.
|
|
30587
|
+
*
|
|
30588
|
+
* Returns the child relationship resource, itself chainable:
|
|
30589
|
+
* `TABLE_COLUMNS.one("dataType", DATA_TYPE)` adds a reactive `.dataType` property on each column.
|
|
30590
|
+
*
|
|
30591
|
+
* @param {string} propertyName - property holding the collection on each owner item
|
|
30592
|
+
* @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
30593
|
+
* @see docs/resource.md
|
|
30594
|
+
*/
|
|
30464
30595
|
stateFacade.scopedMany = (
|
|
30465
30596
|
propertyName,
|
|
30466
30597
|
{
|
|
@@ -37600,8 +37731,8 @@ const withPixelUnit = value => {
|
|
|
37600
37731
|
* because the document is the scrollport in the common case and an anchor
|
|
37601
37732
|
* landing under a bar is never what anyone wants.
|
|
37602
37733
|
*
|
|
37603
|
-
* The variables hold the
|
|
37604
|
-
* sets them.
|
|
37734
|
+
* The variables hold the measured size of the bars on that edge — see the
|
|
37735
|
+
* comment where FixedBar sets them.
|
|
37605
37736
|
*/
|
|
37606
37737
|
|
|
37607
37738
|
const FIXED_BAR_SPACE_CSS = /* css */ `
|
|
@@ -37631,17 +37762,42 @@ const FIXED_BAR_SPACE_CSS = /* css */ `
|
|
|
37631
37762
|
}
|
|
37632
37763
|
`;
|
|
37633
37764
|
|
|
37765
|
+
// Several bars can share an edge — during a page transition the outgoing and
|
|
37766
|
+
// the incoming one are both mounted. They are all pinned to that same edge, so
|
|
37767
|
+
// they overlap: the room to give back is the largest of them, not their sum,
|
|
37768
|
+
// and one leaving must leave the others' room in place.
|
|
37769
|
+
const sizeMapByArea = new Map();
|
|
37770
|
+
|
|
37634
37771
|
/**
|
|
37635
37772
|
* @param {"top"|"bottom"|"left"|"right"} area
|
|
37636
|
-
* @param {
|
|
37773
|
+
* @param {Element} barElement - Which bar this size belongs to.
|
|
37774
|
+
* @param {number|null} size - In px; `null` gives that bar's room back to the
|
|
37775
|
+
* content.
|
|
37637
37776
|
*/
|
|
37638
|
-
const setFixedBarSpace = (area,
|
|
37777
|
+
const setFixedBarSpace = (area, barElement, size) => {
|
|
37778
|
+
let sizeMap = sizeMapByArea.get(area);
|
|
37779
|
+
if (!sizeMap) {
|
|
37780
|
+
sizeMap = new Map();
|
|
37781
|
+
sizeMapByArea.set(area, sizeMap);
|
|
37782
|
+
}
|
|
37783
|
+
if (size === null) {
|
|
37784
|
+
sizeMap.delete(barElement);
|
|
37785
|
+
} else {
|
|
37786
|
+
sizeMap.set(barElement, size);
|
|
37787
|
+
}
|
|
37788
|
+
|
|
37789
|
+
let largestSize = 0;
|
|
37790
|
+
for (const barSize of sizeMap.values()) {
|
|
37791
|
+
if (barSize > largestSize) {
|
|
37792
|
+
largestSize = barSize;
|
|
37793
|
+
}
|
|
37794
|
+
}
|
|
37639
37795
|
const property = `--navi-fixed-bar-space-${area}`;
|
|
37640
37796
|
const { style } = document.documentElement;
|
|
37641
|
-
if (
|
|
37797
|
+
if (sizeMap.size === 0) {
|
|
37642
37798
|
style.removeProperty(property);
|
|
37643
37799
|
} else {
|
|
37644
|
-
style.setProperty(property,
|
|
37800
|
+
style.setProperty(property, `${largestSize}px`);
|
|
37645
37801
|
}
|
|
37646
37802
|
};
|
|
37647
37803
|
|
|
@@ -37818,6 +37974,9 @@ const FixedBar = ({
|
|
|
37818
37974
|
// rebuilt as a calc() expression, so a size coming from anywhere — a prop, a
|
|
37819
37975
|
// theme variable, the content itself — is reserved just the same, and each
|
|
37820
37976
|
// `env()` inset stays the browser's business alone.
|
|
37977
|
+
// And measured again whenever it changes: a ResizeObserver on the bar covers
|
|
37978
|
+
// in one go a size prop that changes, content arriving or leaving, a font
|
|
37979
|
+
// loading late, a rotation moving the notch.
|
|
37821
37980
|
const vertical = area === "left" || area === "right";
|
|
37822
37981
|
const {
|
|
37823
37982
|
ref
|
|
@@ -37827,15 +37986,21 @@ const FixedBar = ({
|
|
|
37827
37986
|
if (!barElement) {
|
|
37828
37987
|
return undefined;
|
|
37829
37988
|
}
|
|
37830
|
-
const {
|
|
37831
|
-
|
|
37832
|
-
|
|
37833
|
-
|
|
37834
|
-
|
|
37989
|
+
const publishSize = () => {
|
|
37990
|
+
const {
|
|
37991
|
+
width,
|
|
37992
|
+
height
|
|
37993
|
+
} = barElement.getBoundingClientRect();
|
|
37994
|
+
setFixedBarSpace(area, barElement, vertical ? width : height);
|
|
37995
|
+
};
|
|
37996
|
+
publishSize();
|
|
37997
|
+
const resizeObserver = new ResizeObserver(publishSize);
|
|
37998
|
+
resizeObserver.observe(barElement);
|
|
37835
37999
|
return () => {
|
|
37836
|
-
|
|
38000
|
+
resizeObserver.disconnect();
|
|
38001
|
+
setFixedBarSpace(area, barElement, null);
|
|
37837
38002
|
};
|
|
37838
|
-
}, [area]);
|
|
38003
|
+
}, [area, vertical]);
|
|
37839
38004
|
return jsx(Box, {
|
|
37840
38005
|
baseClassName: "navi_fixed_bar",
|
|
37841
38006
|
"data-area": area,
|
|
@@ -38619,7 +38784,7 @@ const useCheckableProps = (props, options) => {
|
|
|
38619
38784
|
installImportMetaCssBuild(import.meta);const css$F = /* css */`
|
|
38620
38785
|
@layer navi {
|
|
38621
38786
|
.navi_checkbox {
|
|
38622
|
-
--border-radius: var(--navi-
|
|
38787
|
+
--border-radius: var(--navi-checkbox-border-radius);
|
|
38623
38788
|
--border-width: var(--navi-control-border-width);
|
|
38624
38789
|
/* Focus outline */
|
|
38625
38790
|
--outline-width: var(--navi-focus-outline-width);
|