@eifi1/ui-kit 0.49.0 → 0.50.1

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 (166) hide show
  1. package/README.md +12 -8
  2. package/dist/admin/user-roster.d.ts +16 -10
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/auth/verify-email.d.ts +9 -1
  6. package/dist/auth/verify-email.js +3 -1
  7. package/dist/auth/verify-email.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +2 -1
  9. package/dist/components/amount-input.js +3 -2
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.js +14 -0
  12. package/dist/components/autocomplete.js.map +1 -1
  13. package/dist/components/combobox-core.js +1 -0
  14. package/dist/components/combobox-core.js.map +1 -1
  15. package/dist/components/combobox-suggestions.js +1 -0
  16. package/dist/components/combobox-suggestions.js.map +1 -1
  17. package/dist/components/combobox.js +3 -1
  18. package/dist/components/combobox.js.map +1 -1
  19. package/dist/components/currency-select.d.ts +28 -2
  20. package/dist/components/currency-select.js +56 -10
  21. package/dist/components/currency-select.js.map +1 -1
  22. package/dist/components/data-table-mobile.d.ts +11 -2
  23. package/dist/components/data-table-mobile.js +62 -8
  24. package/dist/components/data-table-mobile.js.map +1 -1
  25. package/dist/components/data-table-row.d.ts +1 -1
  26. package/dist/components/data-table-row.js.map +1 -1
  27. package/dist/components/data-table.js +3 -1
  28. package/dist/components/data-table.js.map +1 -1
  29. package/dist/components/dropdown.js +1 -0
  30. package/dist/components/dropdown.js.map +1 -1
  31. package/dist/components/full-bleed-dialog.js +1 -0
  32. package/dist/components/full-bleed-dialog.js.map +1 -1
  33. package/dist/components/inline-entity-combobox.js +12 -0
  34. package/dist/components/inline-entity-combobox.js.map +1 -1
  35. package/dist/components/modal.js +1 -0
  36. package/dist/components/modal.js.map +1 -1
  37. package/dist/components/number-input.d.ts +2 -1
  38. package/dist/components/number-input.js +6 -2
  39. package/dist/components/number-input.js.map +1 -1
  40. package/dist/components/numpad-sheet.js +2 -0
  41. package/dist/components/numpad-sheet.js.map +1 -1
  42. package/dist/components/picker-sheet.js +2 -0
  43. package/dist/components/picker-sheet.js.map +1 -1
  44. package/dist/components/popover.js +1 -0
  45. package/dist/components/popover.js.map +1 -1
  46. package/dist/eslint/index.js +1 -1
  47. package/dist/i18n/defaults.js +2 -0
  48. package/dist/i18n/defaults.js.map +1 -1
  49. package/dist/i18n/german.js +14 -1
  50. package/dist/i18n/german.js.map +1 -1
  51. package/dist/i18n/kit-labels.d.ts +3 -0
  52. package/dist/i18n/kit-labels.js.map +1 -1
  53. package/dist/i18n/locales/en.js +16 -1
  54. package/dist/i18n/locales/en.js.map +3 -3
  55. package/dist/i18n/locales/es.js +14 -1
  56. package/dist/i18n/locales/es.js.map +1 -1
  57. package/dist/i18n/locales/fr.js +14 -1
  58. package/dist/i18n/locales/fr.js.map +1 -1
  59. package/dist/i18n/locales/hu.js +14 -1
  60. package/dist/i18n/locales/hu.js.map +1 -1
  61. package/dist/i18n/locales/it.js +14 -1
  62. package/dist/i18n/locales/it.js.map +1 -1
  63. package/dist/i18n/locales/zh.js +14 -1
  64. package/dist/i18n/locales/zh.js.map +1 -1
  65. package/dist/i18n/review.js +1 -0
  66. package/dist/i18n/review.js.map +1 -1
  67. package/dist/lib/data-attributes.d.ts +11 -0
  68. package/dist/lib/data-attributes.js +10 -0
  69. package/dist/lib/data-attributes.js.map +1 -0
  70. package/dist/lib/format.d.ts +1 -0
  71. package/dist/lib/format.js +2 -0
  72. package/dist/lib/format.js.map +1 -1
  73. package/dist/lib/minus.d.ts +10 -0
  74. package/dist/lib/minus.js.map +1 -1
  75. package/dist/lib/quiet-focus.d.ts +4 -0
  76. package/dist/lib/quiet-focus.js +19 -0
  77. package/dist/lib/quiet-focus.js.map +1 -0
  78. package/dist/testing/index.d.ts +1 -0
  79. package/dist/testing/index.js +4 -0
  80. package/dist/testing/index.js.map +1 -1
  81. package/dist/testing/react.d.ts +25 -0
  82. package/dist/testing/react.js +19 -0
  83. package/dist/testing/react.js.map +1 -0
  84. package/dist/testing/tour.d.ts +31 -0
  85. package/dist/testing/tour.js +12 -0
  86. package/dist/testing/tour.js.map +1 -0
  87. package/dist/tour/tour-dock.d.ts +42 -0
  88. package/dist/tour/tour-dock.js +80 -0
  89. package/dist/tour/tour-dock.js.map +1 -0
  90. package/dist/tour/tour-engine.d.ts +12 -0
  91. package/dist/tour/tour-engine.js +31 -0
  92. package/dist/tour/tour-engine.js.map +1 -0
  93. package/dist/tour/tour-gates.d.ts +71 -0
  94. package/dist/tour/tour-gates.js +114 -0
  95. package/dist/tour/tour-gates.js.map +1 -0
  96. package/dist/tour/tour-guide-button.d.ts +27 -0
  97. package/dist/tour/tour-guide-button.js +45 -0
  98. package/dist/tour/tour-guide-button.js.map +1 -0
  99. package/dist/tour/tour-guide-labels.d.ts +8 -0
  100. package/dist/tour/tour-guide-labels.js +9 -0
  101. package/dist/tour/tour-guide-labels.js.map +1 -0
  102. package/dist/tour/tour-labels.js +9 -1
  103. package/dist/tour/tour-labels.js.map +1 -1
  104. package/dist/tour/tour.d.ts +144 -9
  105. package/dist/tour/tour.js +598 -59
  106. package/dist/tour/tour.js.map +1 -1
  107. package/dist/tour/use-tour-url-sync.d.ts +7 -2
  108. package/dist/tour/use-tour-url-sync.js +27 -1
  109. package/dist/tour/use-tour-url-sync.js.map +1 -1
  110. package/dist/tour.d.ts +2 -0
  111. package/dist/tour.js +3 -0
  112. package/dist/tour.js.map +1 -1
  113. package/dist/wizard/stepper-nav.js +15 -2
  114. package/dist/wizard/stepper-nav.js.map +1 -1
  115. package/dist/wizard/types.d.ts +11 -0
  116. package/dist/wizard/types.js.map +1 -1
  117. package/dist/wizard/use-wizard.js +11 -5
  118. package/dist/wizard/use-wizard.js.map +1 -1
  119. package/package.json +9 -1
  120. package/src/admin/user-roster.tsx +22 -12
  121. package/src/auth/verify-email.tsx +10 -0
  122. package/src/components/amount-input.tsx +5 -4
  123. package/src/components/autocomplete.tsx +20 -0
  124. package/src/components/combobox-core.tsx +2 -0
  125. package/src/components/combobox-suggestions.tsx +2 -0
  126. package/src/components/combobox.tsx +5 -1
  127. package/src/components/currency-select.tsx +95 -6
  128. package/src/components/data-table-mobile.tsx +99 -8
  129. package/src/components/data-table-row.tsx +1 -0
  130. package/src/components/data-table.tsx +9 -1
  131. package/src/components/dropdown.tsx +2 -0
  132. package/src/components/full-bleed-dialog.tsx +3 -0
  133. package/src/components/inline-entity-combobox.tsx +17 -0
  134. package/src/components/modal.tsx +3 -0
  135. package/src/components/number-input.tsx +8 -3
  136. package/src/components/numpad-sheet.tsx +4 -0
  137. package/src/components/picker-sheet.tsx +4 -0
  138. package/src/components/popover.tsx +2 -0
  139. package/src/i18n/defaults.ts +2 -0
  140. package/src/i18n/german.ts +13 -0
  141. package/src/i18n/kit-labels.tsx +3 -0
  142. package/src/i18n/locales/es.ts +13 -0
  143. package/src/i18n/locales/fr.ts +13 -0
  144. package/src/i18n/locales/hu.ts +13 -0
  145. package/src/i18n/locales/it.ts +13 -0
  146. package/src/i18n/locales/zh.ts +13 -0
  147. package/src/i18n/review.ts +1 -0
  148. package/src/lib/data-attributes.ts +14 -0
  149. package/src/lib/format.ts +2 -0
  150. package/src/lib/minus.ts +10 -0
  151. package/src/lib/quiet-focus.ts +32 -0
  152. package/src/testing/index.ts +8 -0
  153. package/src/testing/react.ts +59 -0
  154. package/src/testing/tour.ts +41 -0
  155. package/src/tour/tour-dock.ts +132 -0
  156. package/src/tour/tour-engine.ts +41 -0
  157. package/src/tour/tour-gates.ts +195 -0
  158. package/src/tour/tour-guide-button.tsx +64 -0
  159. package/src/tour/tour-guide-labels.ts +15 -0
  160. package/src/tour/tour-labels.ts +8 -0
  161. package/src/tour/tour.tsx +989 -86
  162. package/src/tour/use-tour-url-sync.ts +43 -4
  163. package/src/tour.ts +3 -0
  164. package/src/wizard/stepper-nav.tsx +23 -3
  165. package/src/wizard/types.ts +12 -0
  166. package/src/wizard/use-wizard.ts +16 -9
@@ -0,0 +1,195 @@
1
+ import { createContext, useContext, useLayoutEffect, useMemo, useRef } from "react";
2
+
3
+ /*
4
+ * # Value gates (docs/tours-harmonization.md §11.1)
5
+ *
6
+ * A form publishes whether a field holds what a tour waits for (`useTourGate`), a step
7
+ * names the key (`TourStep.gate`). The form knows nothing about steps, the step nothing
8
+ * about the form's state: the registry between them is a `Map` held by the provider.
9
+ *
10
+ * - Each key holds a STACK of publishers in mount order; the most recently mounted one
11
+ * is current. When it unmounts, the one below it is current again. StrictMode's
12
+ * double mount is push, pop, push.
13
+ * - Writes happen in a layout effect, only when `key` or `satisfied` changes, and never
14
+ * re-render the provider: only the overlay subscribes (`useSyncExternalStore`), and it
15
+ * is mounted only while a tour runs.
16
+ * - The same object carries the release bus of `awaitGate` steps (§11.4): `release` and
17
+ * `refuse` are EVENTS, not states, and are not buffered.
18
+ */
19
+
20
+ /** One publisher of a key. */
21
+ interface GateEntry {
22
+ satisfied: boolean;
23
+ }
24
+
25
+ /** `release(key)` / `refuse(key, reason)` from the app, as the provider hears them. */
26
+ export type GateSignal = { kind: "release"; key: string } | { kind: "refuse"; key: string; reason?: string };
27
+
28
+ export interface GateRegistry {
29
+ /** The current publisher's value; `undefined` while nobody publishes `key`. */
30
+ get(key: string): boolean | undefined;
31
+ /** Is anyone publishing `key`? */
32
+ has(key: string): boolean;
33
+ /** For `useSyncExternalStore`: bumped on every change. */
34
+ subscribe(listener: () => void): () => void;
35
+ version(): number;
36
+ push(key: string, entry: GateEntry): void;
37
+ pop(key: string, entry: GateEntry): void;
38
+ changed(key: string): void;
39
+ /** Whether a tour runs (for the two-publishers warning). */
40
+ setActive(active: boolean): void;
41
+ /** The release bus. */
42
+ listen(listener: (signal: GateSignal) => void): () => void;
43
+ emit(signal: GateSignal): void;
44
+ }
45
+
46
+ /** The kit's dev switch (as number-input's): a development build, never under vitest. */
47
+ export function devWarnings(): boolean {
48
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
49
+ }
50
+
51
+ export function createGateRegistry(): GateRegistry {
52
+ let active = false;
53
+ const stacks = new Map<string, GateEntry[]>();
54
+ const listeners = new Set<() => void>();
55
+ const signals = new Set<(signal: GateSignal) => void>();
56
+ const warned = new Set<string>();
57
+ let version = 0;
58
+ const notify = (key: string) => {
59
+ version++;
60
+ const stack = stacks.get(key);
61
+ // Two publishers of one key that disagree while a tour runs: the most recent wins,
62
+ // which is right, but an app keying fields `amount` in two forms wants to hear it.
63
+ if (stack && stack.length > 1 && active && devWarnings() && !warned.has(key)) {
64
+ const values = new Set(stack.map((e) => e.satisfied));
65
+ if (values.size > 1) {
66
+ warned.add(key);
67
+ console.warn(
68
+ `[ui-kit] Two components publish the tour gate "${key}" with different values; the one mounted last ` +
69
+ "wins. Qualify gate keys by form (`txn.amount`, `account.name`).",
70
+ );
71
+ }
72
+ }
73
+ for (const listener of listeners) listener();
74
+ };
75
+ return {
76
+ get(key) {
77
+ const stack = stacks.get(key);
78
+ return stack && stack.length > 0 ? stack[stack.length - 1].satisfied : undefined;
79
+ },
80
+ has(key) {
81
+ return (stacks.get(key)?.length ?? 0) > 0;
82
+ },
83
+ subscribe(listener) {
84
+ listeners.add(listener);
85
+ return () => {
86
+ listeners.delete(listener);
87
+ };
88
+ },
89
+ version: () => version,
90
+ push(key, entry) {
91
+ const stack = stacks.get(key) ?? [];
92
+ stack.push(entry);
93
+ stacks.set(key, stack);
94
+ notify(key);
95
+ },
96
+ pop(key, entry) {
97
+ const stack = stacks.get(key);
98
+ if (!stack) return;
99
+ const at = stack.indexOf(entry);
100
+ if (at >= 0) stack.splice(at, 1);
101
+ if (stack.length === 0) stacks.delete(key);
102
+ notify(key);
103
+ },
104
+ changed: notify,
105
+ setActive(value) {
106
+ active = value;
107
+ },
108
+ listen(listener) {
109
+ signals.add(listener);
110
+ return () => {
111
+ signals.delete(listener);
112
+ };
113
+ },
114
+ emit(signal) {
115
+ for (const listener of signals) listener(signal);
116
+ },
117
+ };
118
+ }
119
+
120
+ /** What the provider shares with the gate hooks and `TourGuideButton` — internal. */
121
+ export interface TourInternals {
122
+ gates: GateRegistry;
123
+ /** Did the provider get a `launch`? */
124
+ canLaunch: boolean;
125
+ }
126
+
127
+ export const TourInternalContext = createContext<TourInternals | null>(null);
128
+
129
+ /**
130
+ * Publish whether this field holds what a tour waits for (§11.1): `useTourGate("txn.amount",
131
+ * amount !== "")` beside the field. A step with `gate: "txn.amount"` keeps its Next locked
132
+ * until it is `true`. A key no step names as `gate` is a FACT a step's `when` can ask
133
+ * (`ctx.gate("txn.transfer")`).
134
+ *
135
+ * - `key` `null`/`undefined` publishes nothing: one field component used by a create and
136
+ * an edit form takes part only in create (`mode === "create" ? "txn.memo" : null`).
137
+ * - Publish the value's state. Do not invent a "touched" gate: a field already filled
138
+ * when the step arrives (a date prefilled with today) shows Next open at once, which is
139
+ * intended.
140
+ * - Qualify keys by form (`txn.amount`, not `amount`). Two publishers of one key: the one
141
+ * mounted last wins.
142
+ * - A no-op without a `TourProvider`, so a form's tests need none. It keeps publishing
143
+ * while no tour runs, because "Guide me" can start inside a form already filled.
144
+ */
145
+ export function useTourGate(key: string | null | undefined, satisfied: boolean): void {
146
+ const gates = useContext(TourInternalContext)?.gates ?? null;
147
+ const entryRef = useRef<GateEntry | null>(null);
148
+ const satisfiedRef = useRef(satisfied);
149
+ useLayoutEffect(() => {
150
+ satisfiedRef.current = satisfied;
151
+ const entry = entryRef.current;
152
+ if (!gates || key == null || !entry || entry.satisfied === satisfied) return;
153
+ entry.satisfied = satisfied;
154
+ gates.changed(key);
155
+ }, [gates, key, satisfied]);
156
+ useLayoutEffect(() => {
157
+ if (!gates || key == null) return;
158
+ const entry: GateEntry = { satisfied: satisfiedRef.current };
159
+ entryRef.current = entry;
160
+ gates.push(key, entry);
161
+ return () => {
162
+ entryRef.current = null;
163
+ gates.pop(key, entry);
164
+ };
165
+ }, [gates, key]);
166
+ }
167
+
168
+ /** What {@link useTourGateRelease} hands back. */
169
+ export interface TourGateRelease {
170
+ /** The awaited thing happened (a save succeeded): the tour moves past the step that awaits `key`. */
171
+ release(key: string): void;
172
+ /** It was refused (a save failed): the step awaiting `key` stays and shows `reason`. */
173
+ refuse(key: string, reason?: string): void;
174
+ }
175
+
176
+ const NO_RELEASE: TourGateRelease = { release: () => {}, refuse: () => {} };
177
+
178
+ /**
179
+ * The signals of `awaitGate` steps (§11.4), usually from a mutation's `onSuccess` /
180
+ * `onError`. Stable identities. No-ops without a provider, and while no tour runs or no
181
+ * step of the run awaits the key: a release is an event and is never buffered.
182
+ */
183
+ export function useTourGateRelease(): TourGateRelease {
184
+ const gates = useContext(TourInternalContext)?.gates ?? null;
185
+ return useMemo(
186
+ () =>
187
+ gates
188
+ ? {
189
+ release: (key: string) => gates.emit({ kind: "release", key }),
190
+ refuse: (key: string, reason?: string) => gates.emit({ kind: "refuse", key, reason }),
191
+ }
192
+ : NO_RELEASE,
193
+ [gates],
194
+ );
195
+ }
@@ -0,0 +1,64 @@
1
+ import { useContext, useId } from "react";
2
+ import type { ReactElement } from "react";
3
+ import { Compass } from "lucide-react";
4
+ import { Button } from "../components/button";
5
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
6
+ import { useTourOptional, type TourAt } from "./tour";
7
+ import { TourInternalContext } from "./tour-gates";
8
+ import { DEFAULT_TOUR_GUIDE_LABELS, type TourGuideLabels } from "./tour-guide-labels";
9
+
10
+ export { DEFAULT_TOUR_GUIDE_LABELS };
11
+ export type { TourGuideLabels };
12
+
13
+ export interface TourGuideButtonProps {
14
+ /** The tour id handed to the provider's `launch`. */
15
+ tour: string;
16
+ /** Usually the form's first in-form anchor (`start(tour, { at })`). */
17
+ at?: TourAt;
18
+ /** Default `"ghost"`. */
19
+ variant?: "ghost" | "secondary";
20
+ /** Default `"sm"`. */
21
+ size?: "sm" | "md";
22
+ labels?: LabelOverride<TourGuideLabels>;
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * "Guide me" inside a form (docs/tours-harmonization.md §11.5): starts the tour `tour` by id,
28
+ * at `at`, through `TourProvider`'s `launch` — the run starts inside the open form, with no
29
+ * intro and no Back past `at`, and counts as completing the tour (Marcel, Q2).
30
+ *
31
+ * A decoration: it renders nothing without a provider, without `launch`, or while a tour
32
+ * runs. Place it in the form's header or in `FormActions`' `start` slot.
33
+ */
34
+ export function TourGuideButton({
35
+ tour,
36
+ at,
37
+ variant = "ghost",
38
+ size = "sm",
39
+ labels,
40
+ className,
41
+ }: TourGuideButtonProps): ReactElement | null {
42
+ const engine = useTourOptional();
43
+ const internals = useContext(TourInternalContext);
44
+ const words = useKitLabels("tourGuide", DEFAULT_TOUR_GUIDE_LABELS, labels);
45
+ const descriptionId = useId();
46
+ if (!engine || !internals?.canLaunch || engine.active) return null;
47
+ return (
48
+ <>
49
+ <Button
50
+ variant={variant}
51
+ size={size}
52
+ className={className}
53
+ aria-describedby={descriptionId}
54
+ onClick={() => engine.start(tour, { at })}
55
+ >
56
+ <Compass className="size-4" aria-hidden="true" />
57
+ {words.label}
58
+ </Button>
59
+ <span id={descriptionId} hidden>
60
+ {words.description}
61
+ </span>
62
+ </>
63
+ );
64
+ }
@@ -0,0 +1,15 @@
1
+ // The English words of `TourGuideButton` (docs/tours-harmonization.md §11), in a module with
2
+ // no component code, like every `*-labels.ts` (0.34, KS U8).
3
+
4
+ /** The `tourGuide` namespace: the form's "Guide me". */
5
+ export interface TourGuideLabels {
6
+ /** The button's text. */
7
+ label: string;
8
+ /** Its accessible description. */
9
+ description: string;
10
+ }
11
+
12
+ export const DEFAULT_TOUR_GUIDE_LABELS: TourGuideLabels = {
13
+ label: "Guide me",
14
+ description: "Walks you through this form, field by field",
15
+ };
@@ -12,4 +12,12 @@ export const DEFAULT_TOUR_LABELS: TourLabels = {
12
12
  awaitClickHint: "Click the highlighted element to continue",
13
13
  awaitInputHint: "Enter a value in the highlighted field to continue",
14
14
  step: (c, t) => `${c} / ${t}`,
15
+ // 0.50: guided form tours (docs/tours-harmonization.md §11).
16
+ leaveEmpty: "Leave empty",
17
+ optional: "Optional",
18
+ gateHint: "Fill in the highlighted field to continue",
19
+ gateReady: "Ready. Press Next or Enter to continue.",
20
+ awaitGateHint: "Save to continue",
21
+ awaitGateRefused: "That wasn’t saved yet. Check the form and try again.",
22
+ backToStep: (title) => `Back to “${title}”`,
15
23
  };