@eifi1/ui-kit 0.49.0 → 0.50.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 +11 -7
- package/dist/admin/user-roster.d.ts +16 -10
- package/dist/admin/user-roster.js +7 -3
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +3 -1
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +1 -0
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/combobox-core.js +1 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox-suggestions.js +1 -0
- package/dist/components/combobox-suggestions.js.map +1 -1
- package/dist/components/currency-select.d.ts +28 -2
- package/dist/components/currency-select.js +56 -10
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +11 -2
- package/dist/components/data-table-mobile.js +62 -8
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row.d.ts +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table.js +3 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dropdown.js +1 -0
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +1 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.js +1 -0
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -1
- package/dist/components/number-input.js +6 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +2 -0
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +1 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +14 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +16 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +14 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +14 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +14 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +14 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +14 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +1 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/lib/data-attributes.d.ts +11 -0
- package/dist/lib/data-attributes.js +10 -0
- package/dist/lib/data-attributes.js.map +1 -0
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +10 -0
- package/dist/lib/minus.js.map +1 -1
- package/dist/testing/react.d.ts +25 -0
- package/dist/testing/react.js +19 -0
- package/dist/testing/react.js.map +1 -0
- package/dist/tour/tour-dock.d.ts +42 -0
- package/dist/tour/tour-dock.js +80 -0
- package/dist/tour/tour-dock.js.map +1 -0
- package/dist/tour/tour-gates.d.ts +71 -0
- package/dist/tour/tour-gates.js +114 -0
- package/dist/tour/tour-gates.js.map +1 -0
- package/dist/tour/tour-guide-button.d.ts +27 -0
- package/dist/tour/tour-guide-button.js +45 -0
- package/dist/tour/tour-guide-button.js.map +1 -0
- package/dist/tour/tour-guide-labels.d.ts +8 -0
- package/dist/tour/tour-guide-labels.js +9 -0
- package/dist/tour/tour-guide-labels.js.map +1 -0
- package/dist/tour/tour-labels.js +9 -1
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +133 -9
- package/dist/tour/tour.js +594 -70
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -2
- package/dist/tour/use-tour-url-sync.js +7 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.js +15 -2
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +11 -0
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +11 -5
- package/dist/wizard/use-wizard.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/user-roster.tsx +22 -12
- package/src/auth/verify-email.tsx +10 -0
- package/src/components/amount-input.tsx +5 -4
- package/src/components/autocomplete.tsx +2 -0
- package/src/components/combobox-core.tsx +2 -0
- package/src/components/combobox-suggestions.tsx +2 -0
- package/src/components/currency-select.tsx +95 -6
- package/src/components/data-table-mobile.tsx +99 -8
- package/src/components/data-table-row.tsx +1 -0
- package/src/components/data-table.tsx +9 -1
- package/src/components/dropdown.tsx +2 -0
- package/src/components/full-bleed-dialog.tsx +3 -0
- package/src/components/modal.tsx +3 -0
- package/src/components/number-input.tsx +8 -3
- package/src/components/numpad-sheet.tsx +4 -0
- package/src/components/picker-sheet.tsx +4 -0
- package/src/components/popover.tsx +2 -0
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +13 -0
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/es.ts +13 -0
- package/src/i18n/locales/fr.ts +13 -0
- package/src/i18n/locales/hu.ts +13 -0
- package/src/i18n/locales/it.ts +13 -0
- package/src/i18n/locales/zh.ts +13 -0
- package/src/i18n/review.ts +1 -0
- package/src/lib/data-attributes.ts +14 -0
- package/src/lib/format.ts +2 -0
- package/src/lib/minus.ts +10 -0
- package/src/testing/react.ts +59 -0
- package/src/tour/tour-dock.ts +132 -0
- package/src/tour/tour-gates.ts +195 -0
- package/src/tour/tour-guide-button.tsx +64 -0
- package/src/tour/tour-guide-labels.ts +15 -0
- package/src/tour/tour-labels.ts +8 -0
- package/src/tour/tour.tsx +949 -86
- package/src/tour/use-tour-url-sync.ts +14 -4
- package/src/tour.ts +3 -0
- package/src/wizard/stepper-nav.tsx +23 -3
- package/src/wizard/types.ts +12 -0
- package/src/wizard/use-wizard.ts +16 -9
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { readKeyboardInset } from "../hooks/use-anchored-panel.js";
|
|
3
|
+
const DOCK_MAX_WIDTH = 640;
|
|
4
|
+
const GUTTER = 8;
|
|
5
|
+
const FULL_SCREEN_SLACK = 64;
|
|
6
|
+
const MIN_DOCK = 140;
|
|
7
|
+
function readBand(target, card) {
|
|
8
|
+
const vh = window.innerHeight;
|
|
9
|
+
let top = 0;
|
|
10
|
+
let bottom = vh;
|
|
11
|
+
const vv = window.visualViewport;
|
|
12
|
+
if (vv && readKeyboardInset() > 0) {
|
|
13
|
+
top = vv.offsetTop;
|
|
14
|
+
bottom = vv.offsetTop + vv.height;
|
|
15
|
+
}
|
|
16
|
+
for (const el of Array.from(document.querySelectorAll("[data-tour-obstruction]"))) {
|
|
17
|
+
if (target && el.contains(target) || card && el.contains(card)) continue;
|
|
18
|
+
const r = el.getBoundingClientRect();
|
|
19
|
+
if (r.width <= 0 || r.height <= 0 || r.top <= top + FULL_SCREEN_SLACK) continue;
|
|
20
|
+
if (r.top < bottom) bottom = r.top;
|
|
21
|
+
}
|
|
22
|
+
return { top, bottom };
|
|
23
|
+
}
|
|
24
|
+
function room(edge, spot, band) {
|
|
25
|
+
return edge === "bottom" ? band.bottom - (spot.top + spot.height) - GUTTER * 2 : spot.top - band.top - GUTTER * 2;
|
|
26
|
+
}
|
|
27
|
+
function dockEdge(spot, band, previous, cardHeight) {
|
|
28
|
+
const preferred = spot.top + spot.height / 2 < band.top + (band.bottom - band.top) / 2 ? "bottom" : "top";
|
|
29
|
+
if (!previous || previous === preferred) return preferred;
|
|
30
|
+
return room(previous, spot, band) >= Math.min(MIN_DOCK, cardHeight) ? previous : preferred;
|
|
31
|
+
}
|
|
32
|
+
function dockStyle(edge, spot, band) {
|
|
33
|
+
const vh = window.innerHeight;
|
|
34
|
+
const cap = spot ? Math.max(MIN_DOCK / 2, room(edge, spot, band)) : band.bottom - band.top - GUTTER * 2;
|
|
35
|
+
const base = {
|
|
36
|
+
left: `max(${GUTTER}px, env(safe-area-inset-left))`,
|
|
37
|
+
right: `max(${GUTTER}px, env(safe-area-inset-right))`,
|
|
38
|
+
width: "auto",
|
|
39
|
+
maxWidth: "none",
|
|
40
|
+
maxHeight: cap
|
|
41
|
+
};
|
|
42
|
+
if (edge === "top") {
|
|
43
|
+
return { ...base, top: band.top <= 0 ? `max(${GUTTER}px, env(safe-area-inset-top))` : band.top + GUTTER, bottom: "auto" };
|
|
44
|
+
}
|
|
45
|
+
const fromBottom = vh - band.bottom;
|
|
46
|
+
return {
|
|
47
|
+
...base,
|
|
48
|
+
top: "auto",
|
|
49
|
+
bottom: fromBottom <= 0 ? `max(${GUTTER}px, env(safe-area-inset-bottom))` : fromBottom + GUTTER
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function scrollParent(el) {
|
|
53
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
54
|
+
const { overflowY } = getComputedStyle(node);
|
|
55
|
+
if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight) return node;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function scrollIntoFreeHalf(el, card, previous) {
|
|
60
|
+
const r = el.getBoundingClientRect();
|
|
61
|
+
const band = readBand(el, card);
|
|
62
|
+
const edge = dockEdge(r, band, previous, card instanceof HTMLElement ? card.offsetHeight : MIN_DOCK);
|
|
63
|
+
const mid = band.top + (band.bottom - band.top) / 2;
|
|
64
|
+
const half = edge === "bottom" ? { top: band.top + GUTTER, bottom: mid } : { top: mid, bottom: band.bottom - GUTTER };
|
|
65
|
+
if (r.top >= half.top && r.top + r.height <= half.bottom) return edge;
|
|
66
|
+
const delta = r.height > half.bottom - half.top ? r.top - half.top : r.top + r.height / 2 - (half.top + half.bottom) / 2;
|
|
67
|
+
const scroller = scrollParent(el);
|
|
68
|
+
if (scroller) scroller.scrollTop += delta;
|
|
69
|
+
else if (!el.closest("[data-kit-layer]")) window.scrollBy?.(0, delta);
|
|
70
|
+
return edge;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
DOCK_MAX_WIDTH,
|
|
74
|
+
dockEdge,
|
|
75
|
+
dockStyle,
|
|
76
|
+
readBand,
|
|
77
|
+
scrollIntoFreeHalf,
|
|
78
|
+
scrollParent
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=tour-dock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-dock.ts"],"sourcesContent":["import type { CSSProperties } from \"react\";\nimport { readKeyboardInset } from \"../hooks/use-anchored-panel\";\n\n/*\n * # The docked card (docs/tours-harmonization.md §11.7, contract round 054 §6)\n *\n * On a phone a field step's card is pinned full-width to the top or bottom edge of the\n * USABLE BAND — the visual viewport less the keyboard, raised above the highest\n * `[data-tour-obstruction]` (the number pad, a picker sheet) — on the edge away from the\n * spotlighted field, so it covers neither the field nor the keyboard nor the pad.\n */\n\nexport type DockEdge = \"top\" | \"bottom\";\n\nexport interface DockRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\n/** The band, in layout-viewport px (what `position: fixed` measures from). */\nexport interface Band {\n top: number;\n bottom: number;\n}\n\n/** Below this the docked layout applies (Tailwind's `sm`). */\nexport const DOCK_MAX_WIDTH = 640;\n/** The gutter between the card and the band's edge, and between the card and the field. */\nconst GUTTER = 8;\n/** An obstruction whose top is within this of the band's top covers the screen. */\nconst FULL_SCREEN_SLACK = 64;\n/** Under this much free space on its edge the card flips (the hysteresis floor). */\nconst MIN_DOCK = 140;\n\n/**\n * The usable band now: the visual viewport while a keyboard is up (`readKeyboardInset`'s\n * rules — pinch zoom is not a keyboard, 100 px minimum), else the layout viewport; its\n * bottom raised to the top of the highest visible obstruction that holds neither the field\n * nor the card. An obstruction that starts at the band's top (a full-screen picker, opened\n * over everything) is not a bottom layer and is ignored; a tall one (the number pad at\n * Large text is over half a phone) still counts.\n */\nexport function readBand(target: Element | null, card: Element | null): Band {\n const vh = window.innerHeight;\n let top = 0;\n let bottom = vh;\n const vv = window.visualViewport;\n if (vv && readKeyboardInset() > 0) {\n top = vv.offsetTop;\n bottom = vv.offsetTop + vv.height;\n }\n for (const el of Array.from(document.querySelectorAll(\"[data-tour-obstruction]\"))) {\n if ((target && el.contains(target)) || (card && el.contains(card))) continue;\n const r = el.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0 || r.top <= top + FULL_SCREEN_SLACK) continue;\n if (r.top < bottom) bottom = r.top;\n }\n return { top, bottom };\n}\n\n/** The card's free space on `edge`, between the band's edge and the spotlight. */\nfunction room(edge: DockEdge, spot: DockRect, band: Band): number {\n return edge === \"bottom\" ? band.bottom - (spot.top + spot.height) - GUTTER * 2 : spot.top - band.top - GUTTER * 2;\n}\n\n/**\n * The edge away from the field: the bottom when the spotlight's centre is in the band's\n * upper half, else the top. With a `previous` edge, it stays unless the card would have to\n * cover the spotlight there (no flip on every keyboard animation frame).\n */\nexport function dockEdge(spot: DockRect, band: Band, previous: DockEdge | null, cardHeight: number): DockEdge {\n const preferred: DockEdge = spot.top + spot.height / 2 < band.top + (band.bottom - band.top) / 2 ? \"bottom\" : \"top\";\n if (!previous || previous === preferred) return preferred;\n return room(previous, spot, band) >= Math.min(MIN_DOCK, cardHeight) ? previous : preferred;\n}\n\n/** The docked card's position: full-width with 8 px (safe-area aware) gutters, its height\n * capped to the free space so its body scrolls and its title and buttons show. */\nexport function dockStyle(edge: DockEdge, spot: DockRect | null, band: Band): CSSProperties {\n const vh = window.innerHeight;\n const cap = spot ? Math.max(MIN_DOCK / 2, room(edge, spot, band)) : band.bottom - band.top - GUTTER * 2;\n const base: CSSProperties = {\n left: `max(${GUTTER}px, env(safe-area-inset-left))`,\n right: `max(${GUTTER}px, env(safe-area-inset-right))`,\n width: \"auto\",\n maxWidth: \"none\",\n maxHeight: cap,\n };\n if (edge === \"top\") {\n return { ...base, top: band.top <= 0 ? `max(${GUTTER}px, env(safe-area-inset-top))` : band.top + GUTTER, bottom: \"auto\" };\n }\n const fromBottom = vh - band.bottom;\n return {\n ...base,\n top: \"auto\",\n bottom: fromBottom <= 0 ? `max(${GUTTER}px, env(safe-area-inset-bottom))` : fromBottom + GUTTER,\n };\n}\n\n/** The target's nearest ancestor that scrolls vertically now (a sheet's body), or null. */\nexport function scrollParent(el: Element): HTMLElement | null {\n for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {\n const { overflowY } = getComputedStyle(node);\n if ((overflowY === \"auto\" || overflowY === \"scroll\") && node.scrollHeight > node.clientHeight) return node;\n }\n return null;\n}\n\n/**\n * Scroll the field into the half of the band away from the card, through its nearest\n * scrollable ancestor (the window only when there is none — the page behind a sheet never\n * moves). A field already inside that half is left where it is. Returns the edge the card\n * takes.\n */\nexport function scrollIntoFreeHalf(el: Element, card: Element | null, previous: DockEdge | null): DockEdge {\n const r = el.getBoundingClientRect();\n const band = readBand(el, card);\n const edge = dockEdge(r, band, previous, card instanceof HTMLElement ? card.offsetHeight : MIN_DOCK);\n const mid = band.top + (band.bottom - band.top) / 2;\n const half = edge === \"bottom\" ? { top: band.top + GUTTER, bottom: mid } : { top: mid, bottom: band.bottom - GUTTER };\n if (r.top >= half.top && r.top + r.height <= half.bottom) return edge;\n // Centred in the half, or its top at the half's top when it is taller than the half.\n const delta =\n r.height > half.bottom - half.top ? r.top - half.top : r.top + r.height / 2 - (half.top + half.bottom) / 2;\n const scroller = scrollParent(el);\n if (scroller) scroller.scrollTop += delta;\n // A field in a kit layer with nothing to scroll: the page behind it stays put.\n else if (!el.closest(\"[data-kit-layer]\")) window.scrollBy?.(0, delta);\n return edge;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** One publisher of a key. */
|
|
2
|
+
interface GateEntry {
|
|
3
|
+
satisfied: boolean;
|
|
4
|
+
}
|
|
5
|
+
/** `release(key)` / `refuse(key, reason)` from the app, as the provider hears them. */
|
|
6
|
+
export type GateSignal = {
|
|
7
|
+
kind: "release";
|
|
8
|
+
key: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "refuse";
|
|
11
|
+
key: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
};
|
|
14
|
+
export interface GateRegistry {
|
|
15
|
+
/** The current publisher's value; `undefined` while nobody publishes `key`. */
|
|
16
|
+
get(key: string): boolean | undefined;
|
|
17
|
+
/** Is anyone publishing `key`? */
|
|
18
|
+
has(key: string): boolean;
|
|
19
|
+
/** For `useSyncExternalStore`: bumped on every change. */
|
|
20
|
+
subscribe(listener: () => void): () => void;
|
|
21
|
+
version(): number;
|
|
22
|
+
push(key: string, entry: GateEntry): void;
|
|
23
|
+
pop(key: string, entry: GateEntry): void;
|
|
24
|
+
changed(key: string): void;
|
|
25
|
+
/** Whether a tour runs (for the two-publishers warning). */
|
|
26
|
+
setActive(active: boolean): void;
|
|
27
|
+
/** The release bus. */
|
|
28
|
+
listen(listener: (signal: GateSignal) => void): () => void;
|
|
29
|
+
emit(signal: GateSignal): void;
|
|
30
|
+
}
|
|
31
|
+
/** The kit's dev switch (as number-input's): a development build, never under vitest. */
|
|
32
|
+
export declare function devWarnings(): boolean;
|
|
33
|
+
export declare function createGateRegistry(): GateRegistry;
|
|
34
|
+
/** What the provider shares with the gate hooks and `TourGuideButton` — internal. */
|
|
35
|
+
export interface TourInternals {
|
|
36
|
+
gates: GateRegistry;
|
|
37
|
+
/** Did the provider get a `launch`? */
|
|
38
|
+
canLaunch: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare const TourInternalContext: import("react").Context<TourInternals | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Publish whether this field holds what a tour waits for (§11.1): `useTourGate("txn.amount",
|
|
43
|
+
* amount !== "")` beside the field. A step with `gate: "txn.amount"` keeps its Next locked
|
|
44
|
+
* until it is `true`. A key no step names as `gate` is a FACT a step's `when` can ask
|
|
45
|
+
* (`ctx.gate("txn.transfer")`).
|
|
46
|
+
*
|
|
47
|
+
* - `key` `null`/`undefined` publishes nothing: one field component used by a create and
|
|
48
|
+
* an edit form takes part only in create (`mode === "create" ? "txn.memo" : null`).
|
|
49
|
+
* - Publish the value's state. Do not invent a "touched" gate: a field already filled
|
|
50
|
+
* when the step arrives (a date prefilled with today) shows Next open at once, which is
|
|
51
|
+
* intended.
|
|
52
|
+
* - Qualify keys by form (`txn.amount`, not `amount`). Two publishers of one key: the one
|
|
53
|
+
* mounted last wins.
|
|
54
|
+
* - A no-op without a `TourProvider`, so a form's tests need none. It keeps publishing
|
|
55
|
+
* while no tour runs, because "Guide me" can start inside a form already filled.
|
|
56
|
+
*/
|
|
57
|
+
export declare function useTourGate(key: string | null | undefined, satisfied: boolean): void;
|
|
58
|
+
/** What {@link useTourGateRelease} hands back. */
|
|
59
|
+
export interface TourGateRelease {
|
|
60
|
+
/** The awaited thing happened (a save succeeded): the tour moves past the step that awaits `key`. */
|
|
61
|
+
release(key: string): void;
|
|
62
|
+
/** It was refused (a save failed): the step awaiting `key` stays and shows `reason`. */
|
|
63
|
+
refuse(key: string, reason?: string): void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The signals of `awaitGate` steps (§11.4), usually from a mutation's `onSuccess` /
|
|
67
|
+
* `onError`. Stable identities. No-ops without a provider, and while no tour runs or no
|
|
68
|
+
* step of the run awaits the key: a release is an event and is never buffered.
|
|
69
|
+
*/
|
|
70
|
+
export declare function useTourGateRelease(): TourGateRelease;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext, useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
function devWarnings() {
|
|
4
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
5
|
+
}
|
|
6
|
+
function createGateRegistry() {
|
|
7
|
+
let active = false;
|
|
8
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
9
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
const signals = /* @__PURE__ */ new Set();
|
|
11
|
+
const warned = /* @__PURE__ */ new Set();
|
|
12
|
+
let version = 0;
|
|
13
|
+
const notify = (key) => {
|
|
14
|
+
version++;
|
|
15
|
+
const stack = stacks.get(key);
|
|
16
|
+
if (stack && stack.length > 1 && active && devWarnings() && !warned.has(key)) {
|
|
17
|
+
const values = new Set(stack.map((e) => e.satisfied));
|
|
18
|
+
if (values.size > 1) {
|
|
19
|
+
warned.add(key);
|
|
20
|
+
console.warn(
|
|
21
|
+
`[ui-kit] Two components publish the tour gate "${key}" with different values; the one mounted last wins. Qualify gate keys by form (\`txn.amount\`, \`account.name\`).`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
get(key) {
|
|
29
|
+
const stack = stacks.get(key);
|
|
30
|
+
return stack && stack.length > 0 ? stack[stack.length - 1].satisfied : void 0;
|
|
31
|
+
},
|
|
32
|
+
has(key) {
|
|
33
|
+
return (stacks.get(key)?.length ?? 0) > 0;
|
|
34
|
+
},
|
|
35
|
+
subscribe(listener) {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
version: () => version,
|
|
42
|
+
push(key, entry) {
|
|
43
|
+
const stack = stacks.get(key) ?? [];
|
|
44
|
+
stack.push(entry);
|
|
45
|
+
stacks.set(key, stack);
|
|
46
|
+
notify(key);
|
|
47
|
+
},
|
|
48
|
+
pop(key, entry) {
|
|
49
|
+
const stack = stacks.get(key);
|
|
50
|
+
if (!stack) return;
|
|
51
|
+
const at = stack.indexOf(entry);
|
|
52
|
+
if (at >= 0) stack.splice(at, 1);
|
|
53
|
+
if (stack.length === 0) stacks.delete(key);
|
|
54
|
+
notify(key);
|
|
55
|
+
},
|
|
56
|
+
changed: notify,
|
|
57
|
+
setActive(value) {
|
|
58
|
+
active = value;
|
|
59
|
+
},
|
|
60
|
+
listen(listener) {
|
|
61
|
+
signals.add(listener);
|
|
62
|
+
return () => {
|
|
63
|
+
signals.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
emit(signal) {
|
|
67
|
+
for (const listener of signals) listener(signal);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const TourInternalContext = createContext(null);
|
|
72
|
+
function useTourGate(key, satisfied) {
|
|
73
|
+
const gates = useContext(TourInternalContext)?.gates ?? null;
|
|
74
|
+
const entryRef = useRef(null);
|
|
75
|
+
const satisfiedRef = useRef(satisfied);
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
satisfiedRef.current = satisfied;
|
|
78
|
+
const entry = entryRef.current;
|
|
79
|
+
if (!gates || key == null || !entry || entry.satisfied === satisfied) return;
|
|
80
|
+
entry.satisfied = satisfied;
|
|
81
|
+
gates.changed(key);
|
|
82
|
+
}, [gates, key, satisfied]);
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
if (!gates || key == null) return;
|
|
85
|
+
const entry = { satisfied: satisfiedRef.current };
|
|
86
|
+
entryRef.current = entry;
|
|
87
|
+
gates.push(key, entry);
|
|
88
|
+
return () => {
|
|
89
|
+
entryRef.current = null;
|
|
90
|
+
gates.pop(key, entry);
|
|
91
|
+
};
|
|
92
|
+
}, [gates, key]);
|
|
93
|
+
}
|
|
94
|
+
const NO_RELEASE = { release: () => {
|
|
95
|
+
}, refuse: () => {
|
|
96
|
+
} };
|
|
97
|
+
function useTourGateRelease() {
|
|
98
|
+
const gates = useContext(TourInternalContext)?.gates ?? null;
|
|
99
|
+
return useMemo(
|
|
100
|
+
() => gates ? {
|
|
101
|
+
release: (key) => gates.emit({ kind: "release", key }),
|
|
102
|
+
refuse: (key, reason) => gates.emit({ kind: "refuse", key, reason })
|
|
103
|
+
} : NO_RELEASE,
|
|
104
|
+
[gates]
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
TourInternalContext,
|
|
109
|
+
createGateRegistry,
|
|
110
|
+
devWarnings,
|
|
111
|
+
useTourGate,
|
|
112
|
+
useTourGateRelease
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=tour-gates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-gates.ts"],"sourcesContent":["import { createContext, useContext, useLayoutEffect, useMemo, useRef } from \"react\";\n\n/*\n * # Value gates (docs/tours-harmonization.md §11.1)\n *\n * A form publishes whether a field holds what a tour waits for (`useTourGate`), a step\n * names the key (`TourStep.gate`). The form knows nothing about steps, the step nothing\n * about the form's state: the registry between them is a `Map` held by the provider.\n *\n * - Each key holds a STACK of publishers in mount order; the most recently mounted one\n * is current. When it unmounts, the one below it is current again. StrictMode's\n * double mount is push, pop, push.\n * - Writes happen in a layout effect, only when `key` or `satisfied` changes, and never\n * re-render the provider: only the overlay subscribes (`useSyncExternalStore`), and it\n * is mounted only while a tour runs.\n * - The same object carries the release bus of `awaitGate` steps (§11.4): `release` and\n * `refuse` are EVENTS, not states, and are not buffered.\n */\n\n/** One publisher of a key. */\ninterface GateEntry {\n satisfied: boolean;\n}\n\n/** `release(key)` / `refuse(key, reason)` from the app, as the provider hears them. */\nexport type GateSignal = { kind: \"release\"; key: string } | { kind: \"refuse\"; key: string; reason?: string };\n\nexport interface GateRegistry {\n /** The current publisher's value; `undefined` while nobody publishes `key`. */\n get(key: string): boolean | undefined;\n /** Is anyone publishing `key`? */\n has(key: string): boolean;\n /** For `useSyncExternalStore`: bumped on every change. */\n subscribe(listener: () => void): () => void;\n version(): number;\n push(key: string, entry: GateEntry): void;\n pop(key: string, entry: GateEntry): void;\n changed(key: string): void;\n /** Whether a tour runs (for the two-publishers warning). */\n setActive(active: boolean): void;\n /** The release bus. */\n listen(listener: (signal: GateSignal) => void): () => void;\n emit(signal: GateSignal): void;\n}\n\n/** The kit's dev switch (as number-input's): a development build, never under vitest. */\nexport function devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\nexport function createGateRegistry(): GateRegistry {\n let active = false;\n const stacks = new Map<string, GateEntry[]>();\n const listeners = new Set<() => void>();\n const signals = new Set<(signal: GateSignal) => void>();\n const warned = new Set<string>();\n let version = 0;\n const notify = (key: string) => {\n version++;\n const stack = stacks.get(key);\n // Two publishers of one key that disagree while a tour runs: the most recent wins,\n // which is right, but an app keying fields `amount` in two forms wants to hear it.\n if (stack && stack.length > 1 && active && devWarnings() && !warned.has(key)) {\n const values = new Set(stack.map((e) => e.satisfied));\n if (values.size > 1) {\n warned.add(key);\n console.warn(\n `[ui-kit] Two components publish the tour gate \"${key}\" with different values; the one mounted last ` +\n \"wins. Qualify gate keys by form (`txn.amount`, `account.name`).\",\n );\n }\n }\n for (const listener of listeners) listener();\n };\n return {\n get(key) {\n const stack = stacks.get(key);\n return stack && stack.length > 0 ? stack[stack.length - 1].satisfied : undefined;\n },\n has(key) {\n return (stacks.get(key)?.length ?? 0) > 0;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n version: () => version,\n push(key, entry) {\n const stack = stacks.get(key) ?? [];\n stack.push(entry);\n stacks.set(key, stack);\n notify(key);\n },\n pop(key, entry) {\n const stack = stacks.get(key);\n if (!stack) return;\n const at = stack.indexOf(entry);\n if (at >= 0) stack.splice(at, 1);\n if (stack.length === 0) stacks.delete(key);\n notify(key);\n },\n changed: notify,\n setActive(value) {\n active = value;\n },\n listen(listener) {\n signals.add(listener);\n return () => {\n signals.delete(listener);\n };\n },\n emit(signal) {\n for (const listener of signals) listener(signal);\n },\n };\n}\n\n/** What the provider shares with the gate hooks and `TourGuideButton` — internal. */\nexport interface TourInternals {\n gates: GateRegistry;\n /** Did the provider get a `launch`? */\n canLaunch: boolean;\n}\n\nexport const TourInternalContext = createContext<TourInternals | null>(null);\n\n/**\n * Publish whether this field holds what a tour waits for (§11.1): `useTourGate(\"txn.amount\",\n * amount !== \"\")` beside the field. A step with `gate: \"txn.amount\"` keeps its Next locked\n * until it is `true`. A key no step names as `gate` is a FACT a step's `when` can ask\n * (`ctx.gate(\"txn.transfer\")`).\n *\n * - `key` `null`/`undefined` publishes nothing: one field component used by a create and\n * an edit form takes part only in create (`mode === \"create\" ? \"txn.memo\" : null`).\n * - Publish the value's state. Do not invent a \"touched\" gate: a field already filled\n * when the step arrives (a date prefilled with today) shows Next open at once, which is\n * intended.\n * - Qualify keys by form (`txn.amount`, not `amount`). Two publishers of one key: the one\n * mounted last wins.\n * - A no-op without a `TourProvider`, so a form's tests need none. It keeps publishing\n * while no tour runs, because \"Guide me\" can start inside a form already filled.\n */\nexport function useTourGate(key: string | null | undefined, satisfied: boolean): void {\n const gates = useContext(TourInternalContext)?.gates ?? null;\n const entryRef = useRef<GateEntry | null>(null);\n const satisfiedRef = useRef(satisfied);\n useLayoutEffect(() => {\n satisfiedRef.current = satisfied;\n const entry = entryRef.current;\n if (!gates || key == null || !entry || entry.satisfied === satisfied) return;\n entry.satisfied = satisfied;\n gates.changed(key);\n }, [gates, key, satisfied]);\n useLayoutEffect(() => {\n if (!gates || key == null) return;\n const entry: GateEntry = { satisfied: satisfiedRef.current };\n entryRef.current = entry;\n gates.push(key, entry);\n return () => {\n entryRef.current = null;\n gates.pop(key, entry);\n };\n }, [gates, key]);\n}\n\n/** What {@link useTourGateRelease} hands back. */\nexport interface TourGateRelease {\n /** The awaited thing happened (a save succeeded): the tour moves past the step that awaits `key`. */\n release(key: string): void;\n /** It was refused (a save failed): the step awaiting `key` stays and shows `reason`. */\n refuse(key: string, reason?: string): void;\n}\n\nconst NO_RELEASE: TourGateRelease = { release: () => {}, refuse: () => {} };\n\n/**\n * The signals of `awaitGate` steps (§11.4), usually from a mutation's `onSuccess` /\n * `onError`. Stable identities. No-ops without a provider, and while no tour runs or no\n * step of the run awaits the key: a release is an event and is never buffered.\n */\nexport function useTourGateRelease(): TourGateRelease {\n const gates = useContext(TourInternalContext)?.gates ?? null;\n return useMemo(\n () =>\n gates\n ? {\n release: (key: string) => gates.emit({ kind: \"release\", key }),\n refuse: (key: string, reason?: string) => gates.emit({ kind: \"refuse\", key, reason }),\n }\n : NO_RELEASE,\n [gates],\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
3
|
+
import { type TourAt } from "./tour.js";
|
|
4
|
+
import { DEFAULT_TOUR_GUIDE_LABELS, type TourGuideLabels } from "./tour-guide-labels.js";
|
|
5
|
+
export { DEFAULT_TOUR_GUIDE_LABELS };
|
|
6
|
+
export type { TourGuideLabels };
|
|
7
|
+
export interface TourGuideButtonProps {
|
|
8
|
+
/** The tour id handed to the provider's `launch`. */
|
|
9
|
+
tour: string;
|
|
10
|
+
/** Usually the form's first in-form anchor (`start(tour, { at })`). */
|
|
11
|
+
at?: TourAt;
|
|
12
|
+
/** Default `"ghost"`. */
|
|
13
|
+
variant?: "ghost" | "secondary";
|
|
14
|
+
/** Default `"sm"`. */
|
|
15
|
+
size?: "sm" | "md";
|
|
16
|
+
labels?: LabelOverride<TourGuideLabels>;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* "Guide me" inside a form (docs/tours-harmonization.md §11.5): starts the tour `tour` by id,
|
|
21
|
+
* at `at`, through `TourProvider`'s `launch` — the run starts inside the open form, with no
|
|
22
|
+
* intro and no Back past `at`, and counts as completing the tour (Marcel, Q2).
|
|
23
|
+
*
|
|
24
|
+
* A decoration: it renders nothing without a provider, without `launch`, or while a tour
|
|
25
|
+
* runs. Place it in the form's header or in `FormActions`' `start` slot.
|
|
26
|
+
*/
|
|
27
|
+
export declare function TourGuideButton({ tour, at, variant, size, labels, className, }: TourGuideButtonProps): ReactElement | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext, useId } from "react";
|
|
4
|
+
import { Compass } from "lucide-react";
|
|
5
|
+
import { Button } from "../components/button.js";
|
|
6
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
7
|
+
import { useTourOptional } from "./tour.js";
|
|
8
|
+
import { TourInternalContext } from "./tour-gates.js";
|
|
9
|
+
import { DEFAULT_TOUR_GUIDE_LABELS } from "./tour-guide-labels.js";
|
|
10
|
+
function TourGuideButton({
|
|
11
|
+
tour,
|
|
12
|
+
at,
|
|
13
|
+
variant = "ghost",
|
|
14
|
+
size = "sm",
|
|
15
|
+
labels,
|
|
16
|
+
className
|
|
17
|
+
}) {
|
|
18
|
+
const engine = useTourOptional();
|
|
19
|
+
const internals = useContext(TourInternalContext);
|
|
20
|
+
const words = useKitLabels("tourGuide", DEFAULT_TOUR_GUIDE_LABELS, labels);
|
|
21
|
+
const descriptionId = useId();
|
|
22
|
+
if (!engine || !internals?.canLaunch || engine.active) return null;
|
|
23
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
24
|
+
/* @__PURE__ */ jsxs(
|
|
25
|
+
Button,
|
|
26
|
+
{
|
|
27
|
+
variant,
|
|
28
|
+
size,
|
|
29
|
+
className,
|
|
30
|
+
"aria-describedby": descriptionId,
|
|
31
|
+
onClick: () => engine.start(tour, { at }),
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsx(Compass, { className: "size-4", "aria-hidden": "true" }),
|
|
34
|
+
words.label
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx("span", { id: descriptionId, hidden: true, children: words.description })
|
|
39
|
+
] });
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
DEFAULT_TOUR_GUIDE_LABELS,
|
|
43
|
+
TourGuideButton
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=tour-guide-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-guide-button.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type { ReactElement } from \"react\";\nimport { Compass } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useTourOptional, type TourAt } from \"./tour\";\nimport { TourInternalContext } from \"./tour-gates\";\nimport { DEFAULT_TOUR_GUIDE_LABELS, type TourGuideLabels } from \"./tour-guide-labels\";\n\nexport { DEFAULT_TOUR_GUIDE_LABELS };\nexport type { TourGuideLabels };\n\nexport interface TourGuideButtonProps {\n /** The tour id handed to the provider's `launch`. */\n tour: string;\n /** Usually the form's first in-form anchor (`start(tour, { at })`). */\n at?: TourAt;\n /** Default `\"ghost\"`. */\n variant?: \"ghost\" | \"secondary\";\n /** Default `\"sm\"`. */\n size?: \"sm\" | \"md\";\n labels?: LabelOverride<TourGuideLabels>;\n className?: string;\n}\n\n/**\n * \"Guide me\" inside a form (docs/tours-harmonization.md §11.5): starts the tour `tour` by id,\n * at `at`, through `TourProvider`'s `launch` — the run starts inside the open form, with no\n * intro and no Back past `at`, and counts as completing the tour (Marcel, Q2).\n *\n * A decoration: it renders nothing without a provider, without `launch`, or while a tour\n * runs. Place it in the form's header or in `FormActions`' `start` slot.\n */\nexport function TourGuideButton({\n tour,\n at,\n variant = \"ghost\",\n size = \"sm\",\n labels,\n className,\n}: TourGuideButtonProps): ReactElement | null {\n const engine = useTourOptional();\n const internals = useContext(TourInternalContext);\n const words = useKitLabels(\"tourGuide\", DEFAULT_TOUR_GUIDE_LABELS, labels);\n const descriptionId = useId();\n if (!engine || !internals?.canLaunch || engine.active) return null;\n return (\n <>\n <Button\n variant={variant}\n size={size}\n className={className}\n aria-describedby={descriptionId}\n onClick={() => engine.start(tour, { at })}\n >\n <Compass className=\"size-4\" aria-hidden=\"true\" />\n {words.label}\n </Button>\n <span id={descriptionId} hidden>\n {words.description}\n </span>\n </>\n );\n}\n"],"mappings":";AA+CI,mBAQI,KAPF,YADF;AA/CJ,SAAS,YAAY,aAAa;AAElC,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,oBAAwC;AACjD,SAAS,uBAAoC;AAC7C,SAAS,2BAA2B;AACpC,SAAS,iCAAuD;AA0BzD,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAA8C;AAC5C,QAAM,SAAS,gBAAgB;AAC/B,QAAM,YAAY,WAAW,mBAAmB;AAChD,QAAM,QAAQ,aAAa,aAAa,2BAA2B,MAAM;AACzE,QAAM,gBAAgB,MAAM;AAC5B,MAAI,CAAC,UAAU,CAAC,WAAW,aAAa,OAAO,OAAQ,QAAO;AAC9D,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,oBAAkB;AAAA,QAClB,SAAS,MAAM,OAAO,MAAM,MAAM,EAAE,GAAG,CAAC;AAAA,QAExC;AAAA,8BAAC,WAAQ,WAAU,UAAS,eAAY,QAAO;AAAA,UAC9C,MAAM;AAAA;AAAA;AAAA,IACT;AAAA,IACA,oBAAC,UAAK,IAAI,eAAe,QAAM,MAC5B,gBAAM,aACT;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The `tourGuide` namespace: the form's "Guide me". */
|
|
2
|
+
export interface TourGuideLabels {
|
|
3
|
+
/** The button's text. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Its accessible description. */
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_TOUR_GUIDE_LABELS: TourGuideLabels;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-guide-labels.ts"],"sourcesContent":["// The English words of `TourGuideButton` (docs/tours-harmonization.md §11), in a module with\n// no component code, like every `*-labels.ts` (0.34, KS U8).\n\n/** The `tourGuide` namespace: the form's \"Guide me\". */\nexport interface TourGuideLabels {\n /** The button's text. */\n label: string;\n /** Its accessible description. */\n description: string;\n}\n\nexport const DEFAULT_TOUR_GUIDE_LABELS: TourGuideLabels = {\n label: \"Guide me\",\n description: \"Walks you through this form, field by field\",\n};\n"],"mappings":";AAWO,MAAM,4BAA6C;AAAA,EACxD,OAAO;AAAA,EACP,aAAa;AACf;","names":[]}
|
package/dist/tour/tour-labels.js
CHANGED
|
@@ -6,7 +6,15 @@ const DEFAULT_TOUR_LABELS = {
|
|
|
6
6
|
done: "Done",
|
|
7
7
|
awaitClickHint: "Click the highlighted element to continue",
|
|
8
8
|
awaitInputHint: "Enter a value in the highlighted field to continue",
|
|
9
|
-
step: (c, t) => `${c} / ${t}
|
|
9
|
+
step: (c, t) => `${c} / ${t}`,
|
|
10
|
+
// 0.50: guided form tours (docs/tours-harmonization.md §11).
|
|
11
|
+
leaveEmpty: "Leave empty",
|
|
12
|
+
optional: "Optional",
|
|
13
|
+
gateHint: "Fill in the highlighted field to continue",
|
|
14
|
+
gateReady: "Ready. Press Next or Enter to continue.",
|
|
15
|
+
awaitGateHint: "Save to continue",
|
|
16
|
+
awaitGateRefused: "That wasn\u2019t saved yet. Check the form and try again.",
|
|
17
|
+
backToStep: (title) => `Back to \u201C${title}\u201D`
|
|
10
18
|
};
|
|
11
19
|
export {
|
|
12
20
|
DEFAULT_TOUR_LABELS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tour/tour-labels.ts"],"sourcesContent":["// The English words of ./tour, in a module with no component code: the English catalogue\n// (src/i18n/defaults.ts) imports them without the component, so `en` loads as one chunk\n// like the other languages (0.34, KS U8). ./tour re-exports them.\n\nimport type { TourLabels } from \"./tour\";\n\nexport const DEFAULT_TOUR_LABELS: TourLabels = {\n next: \"Next\",\n back: \"Back\",\n skip: \"Skip\",\n done: \"Done\",\n awaitClickHint: \"Click the highlighted element to continue\",\n awaitInputHint: \"Enter a value in the highlighted field to continue\",\n step: (c, t) => `${c} / ${t}`,\n};\n"],"mappings":";AAMO,MAAM,sBAAkC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"sources":["../../src/tour/tour-labels.ts"],"sourcesContent":["// The English words of ./tour, in a module with no component code: the English catalogue\n// (src/i18n/defaults.ts) imports them without the component, so `en` loads as one chunk\n// like the other languages (0.34, KS U8). ./tour re-exports them.\n\nimport type { TourLabels } from \"./tour\";\n\nexport const DEFAULT_TOUR_LABELS: TourLabels = {\n next: \"Next\",\n back: \"Back\",\n skip: \"Skip\",\n done: \"Done\",\n awaitClickHint: \"Click the highlighted element to continue\",\n awaitInputHint: \"Enter a value in the highlighted field to continue\",\n step: (c, t) => `${c} / ${t}`,\n // 0.50: guided form tours (docs/tours-harmonization.md §11).\n leaveEmpty: \"Leave empty\",\n optional: \"Optional\",\n gateHint: \"Fill in the highlighted field to continue\",\n gateReady: \"Ready. Press Next or Enter to continue.\",\n awaitGateHint: \"Save to continue\",\n awaitGateRefused: \"That wasn’t saved yet. Check the form and try again.\",\n backToStep: (title) => `Back to “${title}”`,\n};\n"],"mappings":";AAMO,MAAM,sBAAkC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;AAAA;AAAA,EAE3B,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AAAA,EACX,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,YAAY,CAAC,UAAU,iBAAY,KAAK;AAC1C;","names":[]}
|