@connextar/house 0.2.0 → 0.4.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 +213 -2
- 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/dist/wizard/index.d.ts +26 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +26 -0
- package/dist/wizard/index.js.map +1 -0
- package/dist/wizard/invalid.d.ts +91 -0
- package/dist/wizard/invalid.d.ts.map +1 -0
- package/dist/wizard/invalid.js +121 -0
- package/dist/wizard/invalid.js.map +1 -0
- package/dist/wizard/react-hook-form-index.d.ts +12 -0
- package/dist/wizard/react-hook-form-index.d.ts.map +1 -0
- package/dist/wizard/react-hook-form-index.js +12 -0
- package/dist/wizard/react-hook-form-index.js.map +1 -0
- package/dist/wizard/rhf-adapter.d.ts +32 -0
- package/dist/wizard/rhf-adapter.d.ts.map +1 -0
- package/dist/wizard/rhf-adapter.js +58 -0
- package/dist/wizard/rhf-adapter.js.map +1 -0
- package/dist/wizard/rules.d.ts +92 -0
- package/dist/wizard/rules.d.ts.map +1 -0
- package/dist/wizard/rules.js +105 -0
- package/dist/wizard/rules.js.map +1 -0
- package/dist/wizard/wizard.d.ts +127 -0
- package/dist/wizard/wizard.d.ts.map +1 -0
- package/dist/wizard/wizard.js +195 -0
- package/dist/wizard/wizard.js.map +1 -0
- package/package.json +24 -3
package/README.md
CHANGED
|
@@ -39,6 +39,27 @@ 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
|
+
|
|
50
|
+
`wizard`, in 0.4.0, is the first module extracted from five copies that had
|
|
51
|
+
already been written. It is here because of what those copies cost: four of the
|
|
52
|
+
five shipped a defect in it, and not one of the four was a mistake about
|
|
53
|
+
wizards. PharmaLine created a booking from step two, because a `type="button"`
|
|
54
|
+
Continue and a `type="submit"` Create shared a slot and React switched the one
|
|
55
|
+
DOM node's type mid-click. Three PM Tool forms refused to save with nothing on
|
|
56
|
+
screen saying why. MAS checked no step at all on submit, so a second press after
|
|
57
|
+
a successful create sent a record whose first step had been cleared. Every one of
|
|
58
|
+
those is a mistake about what a form must do before it agrees to send, and there
|
|
59
|
+
is one right answer to that — which is exactly the shape of thing that belongs
|
|
60
|
+
here and nowhere else. The module is the union of the five, not a copy of the
|
|
61
|
+
best one.
|
|
62
|
+
|
|
42
63
|
## Modules
|
|
43
64
|
|
|
44
65
|
### `@connextar/house/errors`
|
|
@@ -236,10 +257,166 @@ much for the editor to ask for theirs, and this is not a second design system.
|
|
|
236
257
|
If an app also depends on `@tiptap/*` directly, keep it in the same range as this
|
|
237
258
|
package so npm installs one copy. Two copies of `@tiptap/pm` break the editor.
|
|
238
259
|
|
|
260
|
+
### `@connextar/house/ux`
|
|
261
|
+
|
|
262
|
+
The interface answers the click: optimistic updates, pending state, and
|
|
263
|
+
navigation feedback. Headless — no components, no design system, no toast
|
|
264
|
+
library, no HTTP client.
|
|
265
|
+
|
|
266
|
+
`useOptimisticList(rows, { key })` holds the server's rows as a _base_ and every
|
|
267
|
+
change as a _patch_ beside them, folded in on render. `insert`, `update`,
|
|
268
|
+
`remove`, `move` and `bulk` each take a commit function; `isPending(key)` marks
|
|
269
|
+
one row without disabling the table. `useOptimisticValue` is the same for a
|
|
270
|
+
switch, a status or a settings record; `useAction` and `useKeyedAction` are for
|
|
271
|
+
work that should not be guessed at — a payment, a model call, a sign-in — and
|
|
272
|
+
give a control the `pending` flag it needs and nothing more.
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
const tasks = useOptimisticList(rows, { key: (t) => t.id, onSettled: router.refresh });
|
|
276
|
+
|
|
277
|
+
tasks.update(task.id, { done: true }, () => save(task.id, { done: true }));
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Why not `useOptimistic`.** React 19's hook is right when the write _is_ the
|
|
281
|
+
transition — a server action. It is wrong for an app that writes over `fetch` and
|
|
282
|
+
then calls `router.refresh()`, because `router.refresh()` is not awaitable: the
|
|
283
|
+
transition ends before the new rows arrive and the optimistic value snaps back to
|
|
284
|
+
the stale one, flashing the old answer on every single write.
|
|
285
|
+
|
|
286
|
+
**When a change is let go of.** A patch is `pending` while its write is in
|
|
287
|
+
flight, then dropped (it failed — the base alone is the truth again) or `settled`
|
|
288
|
+
(it landed, and keeps being applied, because the base is still the _old_ rows
|
|
289
|
+
until a refresh arrives). A settled patch goes when fresh rows account for it:
|
|
290
|
+
either they agree, or they disagree with what they said when the write landed, in
|
|
291
|
+
which case they win and the screen corrects itself. Evidence, never a timer — a
|
|
292
|
+
screen that does not refetch has no truth to fall back to, and snapping to stale
|
|
293
|
+
rows after N seconds is a bug that only appears on a slow connection.
|
|
294
|
+
|
|
295
|
+
**The three seams that keep it portable.** A commit is
|
|
296
|
+
`() => Promise<Row | void>` that **throws** on failure, which is the one failure
|
|
297
|
+
contract every client already has; an app whose client returns a result object
|
|
298
|
+
wraps it once. Failures are reported to an `ActionFeedbackProvider` as
|
|
299
|
+
`{ tone, title, description }`, so the app decides what a message looks like.
|
|
300
|
+
And `NavigationProgress` takes a `className` for its bar, so it never needs a
|
|
301
|
+
token.
|
|
302
|
+
|
|
303
|
+
`NavigationProgress` (from `@connextar/house/ux/navigation` — its own entry
|
|
304
|
+
point, so `next` stays an optional peer) also marks the link that was clicked
|
|
305
|
+
with `data-busy`, which `house.css` turns into a spinner. Two notes on it, both
|
|
306
|
+
learned the hard way:
|
|
307
|
+
|
|
308
|
+
- Its click listener is in the **capture** phase, and deliberately does not check
|
|
309
|
+
`defaultPrevented`. Next's `Link` cancels the click on its way to a
|
|
310
|
+
client-side navigation, so a bubble-phase listener guarding on
|
|
311
|
+
`defaultPrevented` fires for every link _except_ the app's own — which is
|
|
312
|
+
precisely backwards, and is what AltEd shipped unnoticed until it was measured
|
|
313
|
+
in a browser.
|
|
314
|
+
- `useAppRouter()` is `useRouter()` with the same feedback on `push` and
|
|
315
|
+
`replace`. Swapping the import is the whole change; no call site moves.
|
|
316
|
+
|
|
317
|
+
**Not everything should be optimistic.** Money, model calls, sign-in, imports and
|
|
318
|
+
exports: anything whose result the person cannot predict, or would be alarmed to
|
|
319
|
+
see undone. Guessing is only honest when the guess is nearly always right.
|
|
320
|
+
|
|
321
|
+
### `@connextar/house/wizard`, `/wizard/react-hook-form`
|
|
322
|
+
|
|
323
|
+
`FormWizard` — one long form split into steps that check themselves. **It renders
|
|
324
|
+
the `<form>`**, because two of its promises cannot be kept from inside somebody
|
|
325
|
+
else's: that Enter has one place to land, and that nothing reaches `onSubmit`
|
|
326
|
+
unchecked.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
const check = useStepValidator(form); // react-hook-form; or write your own
|
|
330
|
+
|
|
331
|
+
<FormWizard
|
|
332
|
+
steps={[
|
|
333
|
+
{ id: "learner", title: "Learner", fields: ["studentId", "level"], content: <LearnerFields /> },
|
|
334
|
+
{ id: "period", title: "Period", fields: ["startDate", "endDate"], content: <PeriodFields /> },
|
|
335
|
+
{ id: "build", title: "Build", fields: [], content: <ReviewFields /> },
|
|
336
|
+
]}
|
|
337
|
+
onValidateStep={check}
|
|
338
|
+
onSubmit={() => void form.handleSubmit(submit, report)()}
|
|
339
|
+
onCancel={() => router.push(routes.plans)}
|
|
340
|
+
submitLabel="Build my curriculum"
|
|
341
|
+
pending={pending}
|
|
342
|
+
/>;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The rules it keeps, each one because a copy of it did not:
|
|
346
|
+
|
|
347
|
+
- **Submitting checks every step, in order, and lands on the first that fails**,
|
|
348
|
+
with its message visible. Checking only the last step lets a marker jump back,
|
|
349
|
+
a cleared field and a press of Save refuse with the problem three screens away.
|
|
350
|
+
Checking none — MAS — sends the record anyway.
|
|
351
|
+
- **Enter before the last step advances.** It validates the step it is on and
|
|
352
|
+
opens the next; only the last step's Enter submits. A booking that emails a
|
|
353
|
+
patient must not be creatable from the step before Confirm.
|
|
354
|
+
- **One submit button, whose `type` never changes.** Never two controls sharing
|
|
355
|
+
the slot. jsdom's timing hid this, and only Chromium showed it, so the test
|
|
356
|
+
that guards it is structural: one `button[type=submit]`, the same node on every
|
|
357
|
+
step.
|
|
358
|
+
- **Continue is never disabled for being incomplete.** A greyed-out button that
|
|
359
|
+
will not say what is missing is a refusal with no words. It is disabled only
|
|
360
|
+
while something is in flight.
|
|
361
|
+
- **Visited markers are reachable, later steps locked** (`allReachable` opens
|
|
362
|
+
them all, for editing values that are already there).
|
|
363
|
+
- **Every step stays mounted, hidden rather than unmounted**, so there is one
|
|
364
|
+
field register and nothing typed is lost stepping back.
|
|
365
|
+
|
|
366
|
+
**Form-library-agnostic, because the five hosts are not the same.** A step is
|
|
367
|
+
complete when the check says so, and a check may answer either way the cohort
|
|
368
|
+
already writes them: `true`/`null` for complete, `false` for "not complete, and I
|
|
369
|
+
am showing why beside the field", or **a sentence** for "not complete, and this is
|
|
370
|
+
what to say" — which the wizard then shows in a `role="alert"` of its own. So
|
|
371
|
+
react-hook-form, plain `useState` and MAS's state forms all host it unchanged. A
|
|
372
|
+
check may live on the form (`onValidateStep`) or on the step (`step.validate`);
|
|
373
|
+
both are asked, the step's first.
|
|
374
|
+
|
|
375
|
+
`stepValidator(form, report)` and `useStepValidator(form)` are in
|
|
376
|
+
`@connextar/house/wizard/react-hook-form` — its own entry point because
|
|
377
|
+
`react-hook-form` is an **optional** peer dependency. Every import of it there is
|
|
378
|
+
`import type`, so the emitted JavaScript never mentions it; what would break an
|
|
379
|
+
app without it is a declaration file naming a package it has not installed, which
|
|
380
|
+
is why the names sit behind a subpath MAS never reaches for.
|
|
381
|
+
`src/wizard/entry-points.test.ts` keeps that honest against the built output.
|
|
382
|
+
|
|
383
|
+
**When the error is somewhere nobody can look.** A wizard's fields live on steps
|
|
384
|
+
that are mounted but hidden, and a form library will not call a submit handler
|
|
385
|
+
while any field is invalid — so a message beside a field the person cannot see
|
|
386
|
+
makes the button look broken. `useInvalidReporter()` returns the function to pass
|
|
387
|
+
as the invalid handler (`form.handleSubmit(onSubmit, report)`); it waits for the
|
|
388
|
+
form to re-render and the wizard to move, then says the message out loud **only
|
|
389
|
+
if it is not already on screen**. It goes to the `ActionFeedbackProvider` from
|
|
390
|
+
[`ux`](#connextarhouseux), so this module imports no toast library. A field's
|
|
391
|
+
message opts in by carrying `data-error-for="<name>"` — `FieldError` does it, and
|
|
392
|
+
an app's own message component adds one attribute.
|
|
393
|
+
|
|
394
|
+
The look is theme-token classes, replaced part by part through `classNames`, the
|
|
395
|
+
same way the guide renderer allows — there is still no `Button` in this package,
|
|
396
|
+
so the wizard draws plain elements and wears the app's classes. The busy state is
|
|
397
|
+
`data-busy`, which [`house.css`](#styles--connextarhousehousecss) already turns
|
|
398
|
+
into a spinner.
|
|
399
|
+
|
|
400
|
+
**What not to make a wizard.** A form of four fields is shorter than a wizard
|
|
401
|
+
around it, and a stepper over one screen of content is decoration that costs a
|
|
402
|
+
click. This is for a form long enough that a person would otherwise lose their
|
|
403
|
+
place — five steps in the curriculum builder, a booking that has to reach Confirm
|
|
404
|
+
— and specifically for one where **later steps depend on earlier answers**. Three
|
|
405
|
+
independent sections are a form with three headings. A flow whose steps are
|
|
406
|
+
separate saves, each landing before the next opens, is not one form and must not
|
|
407
|
+
pretend to be: this wizard keeps one register and submits once.
|
|
408
|
+
|
|
239
409
|
### Styles — `@connextar/house/house.css`
|
|
240
410
|
|
|
241
411
|
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
|
|
412
|
+
block gutter and suggestion highlight, the trail's tinted pills, and the busy
|
|
413
|
+
spinner `ux` puts on whatever was clicked.
|
|
414
|
+
|
|
415
|
+
The spinner is `[data-busy="true"]::after`, drawn in `currentColor` with no theme
|
|
416
|
+
token. A control that renders its own spinner marks it `.house-spinner` so it
|
|
417
|
+
does not end up with two. For `prefers-reduced-motion` the ring stays and stops
|
|
418
|
+
turning, rather than disappearing — it is the only thing on screen saying the
|
|
419
|
+
click was heard.
|
|
243
420
|
|
|
244
421
|
## Tailwind setup (required for every component)
|
|
245
422
|
|
|
@@ -331,6 +508,40 @@ Traps found adopting it the first time:
|
|
|
331
508
|
module"; delete `.next/dev` when no dev server is running.
|
|
332
509
|
- If the app also depends on `@tiptap/*` directly, keep the range aligned or drop
|
|
333
510
|
the direct dependency — two copies of `@tiptap/pm` break the editor.
|
|
511
|
+
- **`FormWizard` renders the `<form>`, so the app's wrapper one has to go.** Every
|
|
512
|
+
local copy but two sat inside a host `<form onSubmit={preventDefault}>`; nested
|
|
513
|
+
forms are invalid HTML and the outer one is where the Enter bug lives. Keep the
|
|
514
|
+
app's `<Form {...form}>` provider if it has one — that is context, not an
|
|
515
|
+
element — and delete the `<form>` inside it. MAS also loses its
|
|
516
|
+
`finishControl`: the submit button is the wizard's now, and passing one back in
|
|
517
|
+
is the defect the wizard exists to prevent.
|
|
518
|
+
- **Jest** (with `next/jest`) does not transform `node_modules`, and this package is
|
|
519
|
+
ESM, so the first import fails with `Unexpected token 'export'`. `next/jest` only
|
|
520
|
+
appends to `transformIgnorePatterns`, so export an async config that rewrites each
|
|
521
|
+
`/node_modules/(?!` rule to `/node_modules/(?!@connextar/house/)(?!`.
|
|
522
|
+
- **Vitest needs one line to mock anything the package imports.** It loads
|
|
523
|
+
`node_modules` through native Node ESM, where `vi.mock` does not reach: a test
|
|
524
|
+
that mocks `next/navigation` keeps passing while the package's components call
|
|
525
|
+
the real router, until one of them throws `invariant expected app router to be
|
|
526
|
+
mounted`. Inline the package so vite transforms it:
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
test: {
|
|
530
|
+
server: {
|
|
531
|
+
deps: {
|
|
532
|
+
inline: [/@connextar\/house/];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
- **Mock `next/navigation.js`, not just `next/navigation`.** This package is built
|
|
539
|
+
with `moduleResolution: nodenext`, and `next` ships no `exports` map — so an
|
|
540
|
+
extensionless subpath does not resolve and the source has to say
|
|
541
|
+
`next/navigation.js`. Vitest keys mocks by specifier, so an app's existing mock
|
|
542
|
+
of `next/navigation` does not cover the package. Register both, with the factory
|
|
543
|
+
written out twice: `vi.mock` is hoisted above every `const`, so a shared factory
|
|
544
|
+
variable throws `Cannot access ... before initialization`.
|
|
334
545
|
|
|
335
546
|
**A note on local development.** Installing this with `npm install ../path` makes
|
|
336
547
|
a symlink, and Next's Turbopack will not resolve a package whose real path is
|
|
@@ -340,7 +551,7 @@ behaves exactly like a registry install:
|
|
|
340
551
|
|
|
341
552
|
```bash
|
|
342
553
|
npm pack --pack-destination /tmp # in this repo
|
|
343
|
-
npm install /tmp/connextar-house-0.
|
|
554
|
+
npm install /tmp/connextar-house-0.4.0.tgz # in the app
|
|
344
555
|
```
|
|
345
556
|
|
|
346
557
|
## Adding to it
|
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"}
|