@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.
Files changed (39) hide show
  1. package/README.md +82 -1
  2. package/dist/house.css +66 -0
  3. package/dist/ux/errors.d.ts +26 -0
  4. package/dist/ux/errors.d.ts.map +1 -0
  5. package/dist/ux/errors.js +33 -0
  6. package/dist/ux/errors.js.map +1 -0
  7. package/dist/ux/feedback.d.ts +23 -0
  8. package/dist/ux/feedback.d.ts.map +1 -0
  9. package/dist/ux/feedback.js +22 -0
  10. package/dist/ux/feedback.js.map +1 -0
  11. package/dist/ux/index.d.ts +20 -0
  12. package/dist/ux/index.d.ts.map +1 -0
  13. package/dist/ux/index.js +20 -0
  14. package/dist/ux/index.js.map +1 -0
  15. package/dist/ux/navigation-index.d.ts +10 -0
  16. package/dist/ux/navigation-index.d.ts.map +1 -0
  17. package/dist/ux/navigation-index.js +10 -0
  18. package/dist/ux/navigation-index.js.map +1 -0
  19. package/dist/ux/navigation-progress.d.ts +53 -0
  20. package/dist/ux/navigation-progress.d.ts.map +1 -0
  21. package/dist/ux/navigation-progress.js +245 -0
  22. package/dist/ux/navigation-progress.js.map +1 -0
  23. package/dist/ux/optimistic.d.ts +139 -0
  24. package/dist/ux/optimistic.d.ts.map +1 -0
  25. package/dist/ux/optimistic.js +242 -0
  26. package/dist/ux/optimistic.js.map +1 -0
  27. package/dist/ux/use-action.d.ts +60 -0
  28. package/dist/ux/use-action.d.ts.map +1 -0
  29. package/dist/ux/use-action.js +100 -0
  30. package/dist/ux/use-action.js.map +1 -0
  31. package/dist/ux/use-optimistic-list.d.ts +69 -0
  32. package/dist/ux/use-optimistic-list.d.ts.map +1 -0
  33. package/dist/ux/use-optimistic-list.js +115 -0
  34. package/dist/ux/use-optimistic-list.js.map +1 -0
  35. package/dist/ux/use-optimistic-value.d.ts +40 -0
  36. package/dist/ux/use-optimistic-value.d.ts.map +1 -0
  37. package/dist/ux/use-optimistic-value.js +68 -0
  38. package/dist/ux/use-optimistic-value.js.map +1 -0
  39. 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"}
@@ -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"}