@connextar/house 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/dist/house.css +66 -0
- package/dist/ux/errors.d.ts +26 -0
- package/dist/ux/errors.d.ts.map +1 -0
- package/dist/ux/errors.js +33 -0
- package/dist/ux/errors.js.map +1 -0
- package/dist/ux/feedback.d.ts +23 -0
- package/dist/ux/feedback.d.ts.map +1 -0
- package/dist/ux/feedback.js +22 -0
- package/dist/ux/feedback.js.map +1 -0
- package/dist/ux/index.d.ts +20 -0
- package/dist/ux/index.d.ts.map +1 -0
- package/dist/ux/index.js +20 -0
- package/dist/ux/index.js.map +1 -0
- package/dist/ux/navigation-index.d.ts +10 -0
- package/dist/ux/navigation-index.d.ts.map +1 -0
- package/dist/ux/navigation-index.js +10 -0
- package/dist/ux/navigation-index.js.map +1 -0
- package/dist/ux/navigation-progress.d.ts +53 -0
- package/dist/ux/navigation-progress.d.ts.map +1 -0
- package/dist/ux/navigation-progress.js +245 -0
- package/dist/ux/navigation-progress.js.map +1 -0
- package/dist/ux/optimistic.d.ts +139 -0
- package/dist/ux/optimistic.d.ts.map +1 -0
- package/dist/ux/optimistic.js +242 -0
- package/dist/ux/optimistic.js.map +1 -0
- package/dist/ux/use-action.d.ts +60 -0
- package/dist/ux/use-action.d.ts.map +1 -0
- package/dist/ux/use-action.js +100 -0
- package/dist/ux/use-action.js.map +1 -0
- package/dist/ux/use-optimistic-list.d.ts +69 -0
- package/dist/ux/use-optimistic-list.d.ts.map +1 -0
- package/dist/ux/use-optimistic-list.js +115 -0
- package/dist/ux/use-optimistic-list.js.map +1 -0
- package/dist/ux/use-optimistic-value.d.ts +40 -0
- package/dist/ux/use-optimistic-value.d.ts.map +1 -0
- package/dist/ux/use-optimistic-value.js +68 -0
- package/dist/ux/use-optimistic-value.js.map +1 -0
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -39,6 +39,14 @@ The 0.2.0 modules are the second half of that rule. AltEd built a versioned
|
|
|
39
39
|
changelog, a navigation trail, a block editor and guide tables; every other app
|
|
40
40
|
was about to write each of them for the second time.
|
|
41
41
|
|
|
42
|
+
`ux`, in 0.3.0, is the clearest case of that clause yet. Every app in the
|
|
43
|
+
portfolio writes over `fetch` and refreshes afterwards, so every one of them has
|
|
44
|
+
the same dead-click problem — a tick that goes nowhere for half a second, a
|
|
45
|
+
button that looks broken while working perfectly — and none of them had solved
|
|
46
|
+
it. It is also the kind of code nobody should write twice: the rules about when
|
|
47
|
+
to let go of an optimistic change are small, subtle and easy to get quietly
|
|
48
|
+
wrong.
|
|
49
|
+
|
|
42
50
|
## Modules
|
|
43
51
|
|
|
44
52
|
### `@connextar/house/errors`
|
|
@@ -236,10 +244,78 @@ much for the editor to ask for theirs, and this is not a second design system.
|
|
|
236
244
|
If an app also depends on `@tiptap/*` directly, keep it in the same range as this
|
|
237
245
|
package so npm installs one copy. Two copies of `@tiptap/pm` break the editor.
|
|
238
246
|
|
|
247
|
+
### `@connextar/house/ux`
|
|
248
|
+
|
|
249
|
+
The interface answers the click: optimistic updates, pending state, and
|
|
250
|
+
navigation feedback. Headless — no components, no design system, no toast
|
|
251
|
+
library, no HTTP client.
|
|
252
|
+
|
|
253
|
+
`useOptimisticList(rows, { key })` holds the server's rows as a _base_ and every
|
|
254
|
+
change as a _patch_ beside them, folded in on render. `insert`, `update`,
|
|
255
|
+
`remove`, `move` and `bulk` each take a commit function; `isPending(key)` marks
|
|
256
|
+
one row without disabling the table. `useOptimisticValue` is the same for a
|
|
257
|
+
switch, a status or a settings record; `useAction` and `useKeyedAction` are for
|
|
258
|
+
work that should not be guessed at — a payment, a model call, a sign-in — and
|
|
259
|
+
give a control the `pending` flag it needs and nothing more.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
const tasks = useOptimisticList(rows, { key: (t) => t.id, onSettled: router.refresh });
|
|
263
|
+
|
|
264
|
+
tasks.update(task.id, { done: true }, () => save(task.id, { done: true }));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Why not `useOptimistic`.** React 19's hook is right when the write _is_ the
|
|
268
|
+
transition — a server action. It is wrong for an app that writes over `fetch` and
|
|
269
|
+
then calls `router.refresh()`, because `router.refresh()` is not awaitable: the
|
|
270
|
+
transition ends before the new rows arrive and the optimistic value snaps back to
|
|
271
|
+
the stale one, flashing the old answer on every single write.
|
|
272
|
+
|
|
273
|
+
**When a change is let go of.** A patch is `pending` while its write is in
|
|
274
|
+
flight, then dropped (it failed — the base alone is the truth again) or `settled`
|
|
275
|
+
(it landed, and keeps being applied, because the base is still the _old_ rows
|
|
276
|
+
until a refresh arrives). A settled patch goes when fresh rows account for it:
|
|
277
|
+
either they agree, or they disagree with what they said when the write landed, in
|
|
278
|
+
which case they win and the screen corrects itself. Evidence, never a timer — a
|
|
279
|
+
screen that does not refetch has no truth to fall back to, and snapping to stale
|
|
280
|
+
rows after N seconds is a bug that only appears on a slow connection.
|
|
281
|
+
|
|
282
|
+
**The three seams that keep it portable.** A commit is
|
|
283
|
+
`() => Promise<Row | void>` that **throws** on failure, which is the one failure
|
|
284
|
+
contract every client already has; an app whose client returns a result object
|
|
285
|
+
wraps it once. Failures are reported to an `ActionFeedbackProvider` as
|
|
286
|
+
`{ tone, title, description }`, so the app decides what a message looks like.
|
|
287
|
+
And `NavigationProgress` takes a `className` for its bar, so it never needs a
|
|
288
|
+
token.
|
|
289
|
+
|
|
290
|
+
`NavigationProgress` (from `@connextar/house/ux/navigation` — its own entry
|
|
291
|
+
point, so `next` stays an optional peer) also marks the link that was clicked
|
|
292
|
+
with `data-busy`, which `house.css` turns into a spinner. Two notes on it, both
|
|
293
|
+
learned the hard way:
|
|
294
|
+
|
|
295
|
+
- Its click listener is in the **capture** phase, and deliberately does not check
|
|
296
|
+
`defaultPrevented`. Next's `Link` cancels the click on its way to a
|
|
297
|
+
client-side navigation, so a bubble-phase listener guarding on
|
|
298
|
+
`defaultPrevented` fires for every link _except_ the app's own — which is
|
|
299
|
+
precisely backwards, and is what AltEd shipped unnoticed until it was measured
|
|
300
|
+
in a browser.
|
|
301
|
+
- `useAppRouter()` is `useRouter()` with the same feedback on `push` and
|
|
302
|
+
`replace`. Swapping the import is the whole change; no call site moves.
|
|
303
|
+
|
|
304
|
+
**Not everything should be optimistic.** Money, model calls, sign-in, imports and
|
|
305
|
+
exports: anything whose result the person cannot predict, or would be alarmed to
|
|
306
|
+
see undone. Guessing is only honest when the guess is nearly always right.
|
|
307
|
+
|
|
239
308
|
### Styles — `@connextar/house/house.css`
|
|
240
309
|
|
|
241
310
|
What the components need that utility classes cannot say: rich text, the editor's
|
|
242
|
-
block gutter and suggestion highlight, the trail's tinted pills
|
|
311
|
+
block gutter and suggestion highlight, the trail's tinted pills, and the busy
|
|
312
|
+
spinner `ux` puts on whatever was clicked.
|
|
313
|
+
|
|
314
|
+
The spinner is `[data-busy="true"]::after`, drawn in `currentColor` with no theme
|
|
315
|
+
token. A control that renders its own spinner marks it `.house-spinner` so it
|
|
316
|
+
does not end up with two. For `prefers-reduced-motion` the ring stays and stops
|
|
317
|
+
turning, rather than disappearing — it is the only thing on screen saying the
|
|
318
|
+
click was heard.
|
|
243
319
|
|
|
244
320
|
## Tailwind setup (required for every component)
|
|
245
321
|
|
|
@@ -331,6 +407,11 @@ Traps found adopting it the first time:
|
|
|
331
407
|
module"; delete `.next/dev` when no dev server is running.
|
|
332
408
|
- If the app also depends on `@tiptap/*` directly, keep the range aligned or drop
|
|
333
409
|
the direct dependency — two copies of `@tiptap/pm` break the editor.
|
|
410
|
+
- **Jest** (with `next/jest`) does not transform `node_modules`, and this package is
|
|
411
|
+
ESM, so the first import fails with `Unexpected token 'export'`. `next/jest` only
|
|
412
|
+
appends to `transformIgnorePatterns`, so export an async config that rewrites each
|
|
413
|
+
`/node_modules/(?!` rule to `/node_modules/(?!@connextar/house/)(?!`. Vitest needs
|
|
414
|
+
nothing.
|
|
334
415
|
|
|
335
416
|
**A note on local development.** Installing this with `npm install ../path` makes
|
|
336
417
|
a symlink, and Next's Turbopack will not resolve a package whose real path is
|
package/dist/house.css
CHANGED
|
@@ -260,8 +260,74 @@
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/* ── Busy controls (@connextar/house/ux) ────────────────────────────────
|
|
264
|
+
One rule for "this is working on it", wherever the click landed.
|
|
265
|
+
|
|
266
|
+
`data-busy` is set by two things that never meet: the app's own button, for
|
|
267
|
+
an action whose spinner it cannot render inside (a button that forwards to a
|
|
268
|
+
`Link` has nowhere to put one), and `NavigationProgress`, for whichever link
|
|
269
|
+
or menu item was clicked. Neither knows what the other is, so the agreement
|
|
270
|
+
is an attribute and this stylesheet.
|
|
271
|
+
|
|
272
|
+
The spinner is a pseudo-element on purpose: the navigation watcher marks DOM
|
|
273
|
+
nodes it has never rendered and cannot re-render — a sidebar link, a row in
|
|
274
|
+
a table, a dropdown item from a page this package has never heard of — and
|
|
275
|
+
CSS is the only thing that reaches all of them. It lays out as a flex item
|
|
276
|
+
where the control is a flex container, and inline after the label where it
|
|
277
|
+
is not, so it takes its own space instead of sitting on top of the words.
|
|
278
|
+
|
|
279
|
+
`currentColor` throughout: no theme token, so it lands in any app's palette. */
|
|
280
|
+
|
|
281
|
+
@layer components {
|
|
282
|
+
[data-busy="true"] {
|
|
283
|
+
cursor: progress;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
[data-busy="true"]::after {
|
|
287
|
+
content: "";
|
|
288
|
+
display: inline-block;
|
|
289
|
+
flex: none;
|
|
290
|
+
width: 0.875rem;
|
|
291
|
+
height: 0.875rem;
|
|
292
|
+
margin-inline-start: 0.4rem;
|
|
293
|
+
vertical-align: -0.1875em;
|
|
294
|
+
border-radius: 9999px;
|
|
295
|
+
border: 2px solid currentColor;
|
|
296
|
+
border-inline-end-color: transparent;
|
|
297
|
+
opacity: 0.65;
|
|
298
|
+
animation: house-spin 0.6s linear infinite;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* A flex control already spaces its children with `gap`. */
|
|
302
|
+
[data-busy="true"]:is(.inline-flex, .flex)::after {
|
|
303
|
+
margin-inline-start: 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* A control that renders its own spinner marks it `.house-spinner`, so it
|
|
307
|
+
does not end up with two. */
|
|
308
|
+
[data-busy="true"]:has(> .house-spinner)::after {
|
|
309
|
+
content: none;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Keyframes must sit outside @layer. */
|
|
314
|
+
@keyframes house-spin {
|
|
315
|
+
to {
|
|
316
|
+
transform: rotate(360deg);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
263
320
|
@media (prefers-reduced-motion: reduce) {
|
|
264
321
|
.house-block-handle {
|
|
265
322
|
transition: none;
|
|
266
323
|
}
|
|
324
|
+
|
|
325
|
+
/* Stillness was asked for, but silence is not the alternative: the ring
|
|
326
|
+
stays, it just stops going round. Removing it altogether would take away
|
|
327
|
+
the only thing on screen saying the click was heard. */
|
|
328
|
+
[data-busy="true"]::after {
|
|
329
|
+
animation: none !important;
|
|
330
|
+
border-inline-end-color: currentColor;
|
|
331
|
+
opacity: 0.45;
|
|
332
|
+
}
|
|
267
333
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing an optimistic commit has to be able to say: "that didn't work,
|
|
3
|
+
* and here is what to tell the person".
|
|
4
|
+
*
|
|
5
|
+
* The hooks in this folder take a commit function that throws on failure —
|
|
6
|
+
* the only failure contract every transport already has. An app whose client
|
|
7
|
+
* returns a result object instead of throwing wraps it once (see
|
|
8
|
+
* `commitApi` in `lib/api.ts`) rather than teaching this layer its envelope.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ActionError extends Error {
|
|
11
|
+
/** HTTP status, where the transport had one. */
|
|
12
|
+
readonly status?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Whatever the refusal carried. A conflict usually carries the record as it
|
|
15
|
+
* now is, which is what a caller merges into rather than rolling back to.
|
|
16
|
+
*/
|
|
17
|
+
readonly data?: unknown;
|
|
18
|
+
constructor(message: string, options?: {
|
|
19
|
+
status?: number;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** The wording to show for a thrown value, whatever it turned out to be. */
|
|
25
|
+
export declare function errorMessage(error: unknown, fallback?: string): string;
|
|
26
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/ux/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,gDAAgD;IAChD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEZ,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAMhG;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAyB,GAAG,MAAM,CAItF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing an optimistic commit has to be able to say: "that didn't work,
|
|
3
|
+
* and here is what to tell the person".
|
|
4
|
+
*
|
|
5
|
+
* The hooks in this folder take a commit function that throws on failure —
|
|
6
|
+
* the only failure contract every transport already has. An app whose client
|
|
7
|
+
* returns a result object instead of throwing wraps it once (see
|
|
8
|
+
* `commitApi` in `lib/api.ts`) rather than teaching this layer its envelope.
|
|
9
|
+
*/
|
|
10
|
+
export class ActionError extends Error {
|
|
11
|
+
/** HTTP status, where the transport had one. */
|
|
12
|
+
status;
|
|
13
|
+
/**
|
|
14
|
+
* Whatever the refusal carried. A conflict usually carries the record as it
|
|
15
|
+
* now is, which is what a caller merges into rather than rolling back to.
|
|
16
|
+
*/
|
|
17
|
+
data;
|
|
18
|
+
constructor(message, options = {}) {
|
|
19
|
+
super(message, { cause: options.cause });
|
|
20
|
+
this.name = "ActionError";
|
|
21
|
+
this.status = options.status;
|
|
22
|
+
this.data = options.data;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The wording to show for a thrown value, whatever it turned out to be. */
|
|
26
|
+
export function errorMessage(error, fallback = "Something went wrong") {
|
|
27
|
+
if (error instanceof Error && error.message)
|
|
28
|
+
return error.message;
|
|
29
|
+
if (typeof error === "string" && error)
|
|
30
|
+
return error;
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/ux/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,gDAAgD;IACvC,MAAM,CAAU;IACzB;;;OAGG;IACM,IAAI,CAAW;IAExB,YAAY,OAAe,EAAE,UAAgE,EAAE;QAC7F,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,QAAQ,GAAG,sBAAsB;IAC5E,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IAClE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACrD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* How this layer tells someone what happened, without knowing what the app
|
|
4
|
+
* uses to tell them.
|
|
5
|
+
*
|
|
6
|
+
* A toast library is a design decision, and the whole point of these hooks is
|
|
7
|
+
* that they can be lifted into a shared package. So the hooks report an event
|
|
8
|
+
* and the app decides what an event looks like — one provider at the root,
|
|
9
|
+
* wired to whatever that app already has.
|
|
10
|
+
*/
|
|
11
|
+
export interface ActionFeedback {
|
|
12
|
+
tone: "success" | "error";
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
export type ActionReporter = (feedback: ActionFeedback) => void;
|
|
17
|
+
export declare function ActionFeedbackProvider({ report, children }: {
|
|
18
|
+
report: ActionReporter;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}): React.JSX.Element;
|
|
21
|
+
/** The app's reporter, or the console fallback. Stable across renders. */
|
|
22
|
+
export declare function useActionReporter(): ActionReporter;
|
|
23
|
+
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/ux/feedback.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;AAchE,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,qBAEjH;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,IAAI,cAAc,CAElD"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const ReporterContext = React.createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Without a provider a failure still has to go somewhere: swallowing it is how
|
|
7
|
+
* an optimistic update becomes a lie. The console is a poor place to tell
|
|
8
|
+
* someone something, but it is not silence, and it shows up in a test.
|
|
9
|
+
*/
|
|
10
|
+
const fallbackReporter = (feedback) => {
|
|
11
|
+
if (feedback.tone !== "error")
|
|
12
|
+
return;
|
|
13
|
+
console.error(`[action] ${feedback.title}${feedback.description ? `: ${feedback.description}` : ""}`);
|
|
14
|
+
};
|
|
15
|
+
export function ActionFeedbackProvider({ report, children }) {
|
|
16
|
+
return _jsx(ReporterContext.Provider, { value: report, children: children });
|
|
17
|
+
}
|
|
18
|
+
/** The app's reporter, or the console fallback. Stable across renders. */
|
|
19
|
+
export function useActionReporter() {
|
|
20
|
+
return React.useContext(ReporterContext) ?? fallbackReporter;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.js","sourceRoot":"","sources":["../../src/ux/feedback.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAmB/B,MAAM,eAAe,GAAG,KAAK,CAAC,aAAa,CAAwB,IAAI,CAAC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,gBAAgB,GAAmB,CAAC,QAAQ,EAAE,EAAE;IACpD,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO;IACtC,OAAO,CAAC,KAAK,CAAC,YAAY,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACxG,CAAC,CAAC;AAEF,MAAM,UAAU,sBAAsB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAyD;IAChH,OAAO,KAAC,eAAe,CAAC,QAAQ,IAAC,KAAK,EAAE,MAAM,YAAG,QAAQ,GAA4B,CAAC;AACxF,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB;IAC/B,OAAO,KAAK,CAAC,UAAU,CAAC,eAAe,CAAC,IAAI,gBAAgB,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ux` — the interface answers the click.
|
|
3
|
+
*
|
|
4
|
+
* Optimistic updates and pending state, with no framework in them: React, and
|
|
5
|
+
* nothing else. The half that needs Next — the navigation bar and the spinner
|
|
6
|
+
* on the link you clicked — is `@connextar/house/ux/navigation`, kept apart so
|
|
7
|
+
* that `next` stays the optional peer dependency it is declared to be. A
|
|
8
|
+
* barrel that re-exported both made it mandatory for anyone importing so much
|
|
9
|
+
* as the pure patch engine, which a smoke test caught before 0.3.0 shipped.
|
|
10
|
+
*
|
|
11
|
+
* What this module needs from the host arrives as a function (`commit`), a
|
|
12
|
+
* provider (`ActionFeedbackProvider`) or a class name — never as an import.
|
|
13
|
+
*/
|
|
14
|
+
export { ActionError, errorMessage } from "./errors.js";
|
|
15
|
+
export { ActionFeedbackProvider, useActionReporter, type ActionFeedback, type ActionReporter } from "./feedback.js";
|
|
16
|
+
export { applyPatches, isRedundant, isTempKey, reconcile, sameRow, settlePatch, shouldDropSettled, supersede, tempKey, type Patch, type PatchKind, type RowKey, } from "./optimistic.js";
|
|
17
|
+
export { useAction, useKeyedAction, type Action, type ActionOptions, type KeyedAction } from "./use-action.js";
|
|
18
|
+
export { useOptimisticList, type OptimisticList, type OptimisticListOptions, type WriteOptions, } from "./use-optimistic-list.js";
|
|
19
|
+
export { useOptimisticValue, type OptimisticValue, type OptimisticValueOptions } from "./use-optimistic-value.js";
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,OAAO,EACP,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,MAAM,GACZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,KAAK,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/G,OAAO,EACL,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/ux/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ux` — the interface answers the click.
|
|
3
|
+
*
|
|
4
|
+
* Optimistic updates and pending state, with no framework in them: React, and
|
|
5
|
+
* nothing else. The half that needs Next — the navigation bar and the spinner
|
|
6
|
+
* on the link you clicked — is `@connextar/house/ux/navigation`, kept apart so
|
|
7
|
+
* that `next` stays the optional peer dependency it is declared to be. A
|
|
8
|
+
* barrel that re-exported both made it mandatory for anyone importing so much
|
|
9
|
+
* as the pure patch engine, which a smoke test caught before 0.3.0 shipped.
|
|
10
|
+
*
|
|
11
|
+
* What this module needs from the host arrives as a function (`commit`), a
|
|
12
|
+
* provider (`ActionFeedbackProvider`) or a class name — never as an import.
|
|
13
|
+
*/
|
|
14
|
+
export { ActionError, errorMessage } from "./errors.js";
|
|
15
|
+
export { ActionFeedbackProvider, useActionReporter } from "./feedback.js";
|
|
16
|
+
export { applyPatches, isRedundant, isTempKey, reconcile, sameRow, settlePatch, shouldDropSettled, supersede, tempKey, } from "./optimistic.js";
|
|
17
|
+
export { useAction, useKeyedAction } from "./use-action.js";
|
|
18
|
+
export { useOptimisticList, } from "./use-optimistic-list.js";
|
|
19
|
+
export { useOptimisticValue } from "./use-optimistic-value.js";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ux/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAA4C,MAAM,eAAe,CAAC;AACpH,OAAO,EACL,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACT,OAAO,EACP,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,OAAO,GAIR,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAqD,MAAM,iBAAiB,CAAC;AAC/G,OAAO,EACL,iBAAiB,GAIlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAqD,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ux/navigation` — feedback for moving between screens.
|
|
3
|
+
*
|
|
4
|
+
* Its own entry point because it is the only part of `ux` that imports `next`,
|
|
5
|
+
* which is an optional peer dependency: an app on another framework (or any
|
|
6
|
+
* app's Vitest suite running under plain Node) can still have the optimistic
|
|
7
|
+
* hooks from `@connextar/house/ux` without Next installed.
|
|
8
|
+
*/
|
|
9
|
+
export { BUSY_ATTRIBUTE, NavigationProgress, startNavigation, startRouteProgress, useAppRouter, useNavigate, } from "./navigation-progress.js";
|
|
10
|
+
//# sourceMappingURL=navigation-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-index.d.ts","sourceRoot":"","sources":["../../src/ux/navigation-index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ux/navigation` — feedback for moving between screens.
|
|
3
|
+
*
|
|
4
|
+
* Its own entry point because it is the only part of `ux` that imports `next`,
|
|
5
|
+
* which is an optional peer dependency: an app on another framework (or any
|
|
6
|
+
* app's Vitest suite running under plain Node) can still have the optimistic
|
|
7
|
+
* hooks from `@connextar/house/ux` without Next installed.
|
|
8
|
+
*/
|
|
9
|
+
export { BUSY_ATTRIBUTE, NavigationProgress, startNavigation, startRouteProgress, useAppRouter, useNavigate, } from "./navigation-progress.js";
|
|
10
|
+
//# sourceMappingURL=navigation-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-index.js","sourceRoot":"","sources":["../../src/ux/navigation-index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,WAAW,GACZ,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** The attribute every busy control carries, action or navigation alike. */
|
|
3
|
+
export declare const BUSY_ATTRIBUTE = "data-busy";
|
|
4
|
+
/**
|
|
5
|
+
* Show the navigation feedback for something this module cannot see on its
|
|
6
|
+
* own — a programmatic `router.push`. Pass the control that caused it and it
|
|
7
|
+
* gets the spinner too.
|
|
8
|
+
*/
|
|
9
|
+
export declare function startNavigation(element?: Element | null): void;
|
|
10
|
+
/** Backwards-compatible alias for the original name. */
|
|
11
|
+
export declare const startRouteProgress: typeof startNavigation;
|
|
12
|
+
/**
|
|
13
|
+
* `useRouter`, with the navigation feedback attached to `push` and `replace`.
|
|
14
|
+
*
|
|
15
|
+
* A click that runs `router.push(href)` is invisible to the watcher above —
|
|
16
|
+
* there is no anchor and no popstate — so those navigations were the ones with
|
|
17
|
+
* no feedback at all. The worst case is the one right after a save: the
|
|
18
|
+
* button's own spinner stops when the write lands, and then nothing happens
|
|
19
|
+
* for as long as the next screen takes to render.
|
|
20
|
+
*
|
|
21
|
+
* Swapping the import is the whole change; no call site moves. `refresh`,
|
|
22
|
+
* `back`, `forward` and `prefetch` pass straight through — a refresh re-renders
|
|
23
|
+
* the screen you are already on, which needs no bar.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useAppRouter(): {
|
|
26
|
+
push: (href: string, options?: import("next/dist/shared/lib/app-router-context.shared-runtime.js").NavigateOptions | undefined) => void;
|
|
27
|
+
replace: (href: string, options?: import("next/dist/shared/lib/app-router-context.shared-runtime.js").NavigateOptions | undefined) => void;
|
|
28
|
+
back(): void;
|
|
29
|
+
forward(): void;
|
|
30
|
+
refresh(): void;
|
|
31
|
+
prefetch(href: string, options?: import("next/dist/shared/lib/app-router-context.shared-runtime.js").PrefetchOptions): void;
|
|
32
|
+
experimental_gesturePush?(href: string, options?: import("next/dist/shared/lib/app-router-context.shared-runtime.js").NavigateOptions): void;
|
|
33
|
+
bfcacheId: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `router.push` with the feedback attached, and the control that caused it
|
|
37
|
+
* marked as busy. For a button that navigates rather than saves.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useNavigate(): (href: string, options?: {
|
|
40
|
+
replace?: boolean;
|
|
41
|
+
element?: Element | null;
|
|
42
|
+
}) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Mount once, above everything else — including anything that suspends, or the
|
|
45
|
+
* bar disappears for exactly the navigations it is there for.
|
|
46
|
+
*
|
|
47
|
+
* `className` styles the filled part of the bar, so the host app picks its own
|
|
48
|
+
* colour without this module knowing any of its tokens.
|
|
49
|
+
*/
|
|
50
|
+
export declare function NavigationProgress({ className }: {
|
|
51
|
+
className?: string;
|
|
52
|
+
}): React.JSX.Element;
|
|
53
|
+
//# sourceMappingURL=navigation-progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-progress.d.ts","sourceRoot":"","sources":["../../src/ux/navigation-progress.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAgC/B,4EAA4E;AAC5E,eAAO,MAAM,cAAc,cAAc,CAAC;AAqB1C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,wBAAkB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY;;;;;;;;;EAkB3B;AAED;;;GAGG;AACH,wBAAgB,WAAW,WAGhB,MAAM,YAAY;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,UAO3E;AA0ID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAMvE"}
|