@eifi1/ui-kit 0.42.0 → 0.43.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 +12 -6
- package/dist/admin/admin-area-labels.d.ts +4 -0
- package/dist/admin/admin-area-labels.js +3 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.d.ts +25 -8
- package/dist/admin/admin-overview.js +26 -3
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.d.ts +36 -5
- package/dist/admin/admin-sections.js +35 -18
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/billing/billing-banner.d.ts +8 -0
- package/dist/billing/billing-banner.js +25 -2
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +4 -0
- package/dist/billing/billing-labels.js +1 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +53 -0
- package/dist/billing/cancel-request.js +21 -0
- package/dist/billing/cancel-request.js.map +1 -0
- package/dist/billing/provider-page.d.ts +40 -0
- package/dist/billing/provider-page.js +33 -0
- package/dist/billing/provider-page.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +32 -1
- package/dist/billing/subscription-actions.js +57 -4
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/components/account-chips.d.ts +8 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-menu.d.ts +7 -4
- package/dist/feedback/feedback-menu.js +4 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/hooks/use-search-param-state.js +4 -1
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +36 -2
- 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 +51 -2
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +36 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +36 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +36 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +36 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/search/global-search.d.ts +3 -0
- package/dist/search/global-search.js +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/settings/settings-catalogue.d.ts +9 -1
- package/dist/settings/settings-catalogue.js +1 -1
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/shell/admin-menu.d.ts +26 -3
- package/dist/shell/admin-menu.js +8 -4
- package/dist/shell/admin-menu.js.map +1 -1
- package/dist/shell/dev-switchers.js +5 -0
- package/dist/shell/dev-switchers.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +6 -0
- package/dist/shell/top-bar-parts-labels.js +4 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/tour-menu.d.ts +19 -10
- package/dist/shell/tour-menu.js +20 -6
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour-catalogue-labels.d.ts +44 -0
- package/dist/tour/tour-catalogue-labels.js +32 -0
- package/dist/tour/tour-catalogue-labels.js.map +1 -0
- package/dist/tour/tour-catalogue.d.ts +68 -0
- package/dist/tour/tour-catalogue.js +189 -0
- package/dist/tour/tour-catalogue.js.map +1 -0
- package/dist/tour/tour-info.d.ts +112 -0
- package/dist/tour/tour-info.js +36 -0
- package/dist/tour/tour-info.js.map +1 -0
- package/dist/tour/use-tour-progress.d.ts +61 -0
- package/dist/tour/use-tour-progress.js +116 -0
- package/dist/tour/use-tour-progress.js.map +1 -0
- package/dist/tour/use-tour-url-sync.d.ts +58 -0
- package/dist/tour/use-tour-url-sync.js +110 -0
- package/dist/tour/use-tour-url-sync.js.map +1 -0
- package/dist/tour.d.ts +8 -0
- package/dist/tour.js +18 -0
- package/dist/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-area-labels.ts +7 -1
- package/src/admin/admin-overview.tsx +61 -10
- package/src/admin/admin-sections.tsx +109 -48
- package/src/billing/billing-banner.tsx +25 -2
- package/src/billing/billing-labels.ts +6 -0
- package/src/billing/cancel-request.ts +75 -0
- package/src/billing/provider-page.ts +75 -0
- package/src/billing/subscription-actions.tsx +97 -11
- package/src/components/account-chips.tsx +8 -1
- package/src/feedback/feedback-menu.tsx +8 -3
- package/src/hooks/use-search-param-state.ts +6 -1
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +36 -1
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/en.ts +13 -0
- package/src/i18n/locales/es.ts +36 -1
- package/src/i18n/locales/fr.ts +36 -1
- package/src/i18n/locales/hu.ts +36 -1
- package/src/i18n/locales/it.ts +36 -1
- package/src/i18n/locales/zh.ts +36 -1
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +8 -0
- package/src/search/global-search.tsx +5 -0
- package/src/settings/settings-catalogue.ts +10 -2
- package/src/shell/admin-menu.tsx +34 -5
- package/src/shell/dev-switchers.tsx +8 -0
- package/src/shell/top-bar-parts-labels.ts +9 -0
- package/src/shell/tour-menu.tsx +50 -15
- package/src/shell.ts +3 -0
- package/src/tour/tour-catalogue-labels.ts +75 -0
- package/src/tour/tour-catalogue.tsx +316 -0
- package/src/tour/tour-info.ts +144 -0
- package/src/tour/use-tour-progress.ts +198 -0
- package/src/tour/use-tour-url-sync.ts +221 -0
- package/src/tour.ts +11 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, type Context } from "react";
|
|
2
|
+
import * as ReactRouter from "react-router";
|
|
3
|
+
import { useLocation, useNavigate, type Navigator } from "react-router";
|
|
4
|
+
import { useSearchParamsState } from "../hooks/use-search-param-state";
|
|
5
|
+
import { useTourOptional } from "./tour";
|
|
6
|
+
|
|
7
|
+
/* ── useTourUrlSync ─────────────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
export interface UseTourUrlSyncOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Start the tour `id` at `startIndex` (0-based) — the app's launcher, which builds the
|
|
12
|
+
* steps, awaits its own `prepare()` and passes `onFinish` (the completion, the points
|
|
13
|
+
* toast) to `useTour().start`. An unknown id should do nothing. May return a promise:
|
|
14
|
+
* the parameters are kept until it settles.
|
|
15
|
+
*/
|
|
16
|
+
launch: (id: string, options: { startIndex: number }) => void | Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Are the facts a tour's steps are built from loaded yet (keksdose's `ready`: the
|
|
19
|
+
* caches a branching tour reads)? A deep link waits for it, keeping its parameters, so
|
|
20
|
+
* an F5 mid-tour resumes the right variant at the right step. Default `true`.
|
|
21
|
+
*/
|
|
22
|
+
ready?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Older names of `tourStep`, read when it is absent and removed with it — kastlan's
|
|
25
|
+
* `ts` (§2.7, for one minor). Their value is a 0-based step index, as kastlan wrote it.
|
|
26
|
+
*/
|
|
27
|
+
stepAliases?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Place = { pathname: string; search: string; hash: string };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Has the router already moved on from the location React is still rendering?
|
|
34
|
+
* keksdose's check (tour-url-sync.tsx, tours audit 2026-10-02): a mirror write built
|
|
35
|
+
* from the page being LEFT is a replace back onto it, which cancels a step's `go()`.
|
|
36
|
+
*
|
|
37
|
+
* - A data router (`createBrowserRouter`, kastlan): its navigator has no location (build
|
|
38
|
+
* review 0.43, kastlan A2), so the router's own state answers — a navigation still
|
|
39
|
+
* loading, or a location it has committed that React has not rendered yet.
|
|
40
|
+
* - A declarative router (`BrowserRouter`, keksdose): its navigator is its history and
|
|
41
|
+
* carries where it is.
|
|
42
|
+
* - Anything else answers "not in flight".
|
|
43
|
+
*/
|
|
44
|
+
function navigationInFlight(history: Navigator | undefined, dataRouter: DataRouterProbe, rendered: Place) {
|
|
45
|
+
const state = dataRouter?.router?.state;
|
|
46
|
+
if (state && (state.navigation?.state ?? "idle") !== "idle") return true;
|
|
47
|
+
const live = state?.location ?? (history as (Navigator & { location?: Partial<Place> }) | undefined)?.location;
|
|
48
|
+
if (!live) return false;
|
|
49
|
+
return (
|
|
50
|
+
live.pathname !== rendered.pathname || (live.search ?? "") !== rendered.search || (live.hash ?? "") !== rendered.hash
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type NavigationProbe = { navigator?: Navigator } | null;
|
|
55
|
+
const NO_NAVIGATION = createContext<NavigationProbe>(null);
|
|
56
|
+
/** The data router's state, as `useSearchParamState` reads it. */
|
|
57
|
+
type DataRouterProbe = {
|
|
58
|
+
router?: { state?: { navigation?: { state?: string }; location?: Partial<Place> } };
|
|
59
|
+
} | null;
|
|
60
|
+
const NO_DATA_ROUTER = createContext<DataRouterProbe>(null);
|
|
61
|
+
const DataRouterContext: Context<DataRouterProbe> = (() => {
|
|
62
|
+
try {
|
|
63
|
+
return (
|
|
64
|
+
(ReactRouter as unknown as { UNSAFE_DataRouterContext?: Context<DataRouterProbe> }).UNSAFE_DataRouterContext ??
|
|
65
|
+
NO_DATA_ROUTER
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
return NO_DATA_ROUTER;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
/** Read off the namespace inside a `try`, as `useSearchParamState` reads the data router:
|
|
72
|
+
* a test's partial `vi.mock("react-router")` throws on an export it does not define. */
|
|
73
|
+
const NavigationContext: Context<NavigationProbe> = (() => {
|
|
74
|
+
try {
|
|
75
|
+
return (
|
|
76
|
+
(ReactRouter as unknown as { UNSAFE_NavigationContext?: Context<NavigationProbe> }).UNSAFE_NavigationContext ??
|
|
77
|
+
NO_NAVIGATION
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
return NO_NAVIGATION;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
/** The step a link asks for, 0-based: `tourStep` is 1-based (keksdose), an alias 0-based. */
|
|
85
|
+
function stepOf(params: URLSearchParams, aliases: readonly string[]): number {
|
|
86
|
+
const step = Number(params.get("tourStep"));
|
|
87
|
+
if (params.has("tourStep")) return Number.isFinite(step) && step > 0 ? step - 1 : 0;
|
|
88
|
+
for (const alias of aliases) {
|
|
89
|
+
const value = Number(params.get(alias));
|
|
90
|
+
if (params.has(alias)) return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Tours and the URL (docs/tours-harmonization.md §2.7, §6.4) — keksdose's `TourUrlSync`,
|
|
97
|
+
* on the kit's engine (`useTourOptional`):
|
|
98
|
+
*
|
|
99
|
+
* - **`?tour=<id>` starts that tour whenever it APPEARS** — on load, and on in-app
|
|
100
|
+
* navigation too (kastlan's assistant links to `/help/tours?tour=<id>`, ka F4) —
|
|
101
|
+
* resuming at `&tourStep=<n>` (1-based). It waits for `ready`.
|
|
102
|
+
* - **While a tour runs, `?tour=&tourStep=` mirror where it is**, so a reload or a copied
|
|
103
|
+
* link pins the step (kk K-3). A step that navigates drops them; this puts them back.
|
|
104
|
+
* - **At the end it strips only its own parameters**; `?q=`, `?state=` and the hash stay.
|
|
105
|
+
*
|
|
106
|
+
* Mount it once inside the router and the `TourProvider`. Without a provider it still
|
|
107
|
+
* launches through `launch` and never mirrors.
|
|
108
|
+
*/
|
|
109
|
+
export function useTourUrlSync({ launch, ready = true, stepAliases = [] }: UseTourUrlSyncOptions): void {
|
|
110
|
+
const navigate = useNavigate();
|
|
111
|
+
const location = useLocation();
|
|
112
|
+
const { pathname, search, hash } = location;
|
|
113
|
+
const history = useContext(NavigationContext)?.navigator;
|
|
114
|
+
const dataRouter = useContext(DataRouterContext);
|
|
115
|
+
const engine = useTourOptional();
|
|
116
|
+
const active = engine?.active ?? false;
|
|
117
|
+
const activeId = engine?.activeId ?? null;
|
|
118
|
+
const index = engine?.index ?? 0;
|
|
119
|
+
const urlTour = new URLSearchParams(search).get("tour");
|
|
120
|
+
const aliases = stepAliases.join("\n");
|
|
121
|
+
|
|
122
|
+
const launchRef = useRef(launch);
|
|
123
|
+
const activeIdRef = useRef(activeId);
|
|
124
|
+
useLayoutEffect(() => {
|
|
125
|
+
launchRef.current = launch;
|
|
126
|
+
activeIdRef.current = active ? activeId : null;
|
|
127
|
+
});
|
|
128
|
+
/** The `tour` value last seen in the URL: a tour starts when it CHANGES to an id. */
|
|
129
|
+
const seenRef = useRef<string | null>(null);
|
|
130
|
+
/** A deep link waiting for `ready`. */
|
|
131
|
+
const pendingRef = useRef<{ id: string; startIndex: number } | null>(null);
|
|
132
|
+
/** A launch under way (an app's `prepare()`). */
|
|
133
|
+
const launchingRef = useRef(false);
|
|
134
|
+
/** Bumped when a launch settles, so the mirror looks again. */
|
|
135
|
+
const [settled, setSettled] = useState(0);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (urlTour !== seenRef.current) {
|
|
139
|
+
seenRef.current = urlTour;
|
|
140
|
+
// Its own mirror writing the running tour back is no new link.
|
|
141
|
+
pendingRef.current =
|
|
142
|
+
urlTour && urlTour !== activeIdRef.current
|
|
143
|
+
? { id: urlTour, startIndex: stepOf(new URLSearchParams(search), aliases ? aliases.split("\n") : []) }
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
const pending = pendingRef.current;
|
|
147
|
+
if (!pending || !ready) return;
|
|
148
|
+
pendingRef.current = null;
|
|
149
|
+
launchingRef.current = true;
|
|
150
|
+
void Promise.resolve()
|
|
151
|
+
.then(() => launchRef.current(pending.id, { startIndex: pending.startIndex }))
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
.finally(() => {
|
|
154
|
+
launchingRef.current = false;
|
|
155
|
+
setSettled((n) => n + 1);
|
|
156
|
+
});
|
|
157
|
+
// Only a change of the parameter, or readiness, starts a tour.
|
|
158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
|
+
}, [urlTour, ready]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (navigationInFlight(history, dataRouter, { pathname, search, hash })) return;
|
|
163
|
+
// A link not yet used keeps its parameters: a reload in that window resumes.
|
|
164
|
+
if (pendingRef.current || launchingRef.current) return;
|
|
165
|
+
const tourId = active && activeId ? activeId : null;
|
|
166
|
+
const own = ["tour", "tourStep", ...(aliases ? aliases.split("\n") : [])];
|
|
167
|
+
const current = new URLSearchParams(search);
|
|
168
|
+
// Nothing to mirror and nothing to strip: never touch the URL (a no-op replace
|
|
169
|
+
// clobbers a page's own just-issued navigation, keksdose feedback #4).
|
|
170
|
+
if (!tourId && !own.some((key) => current.has(key))) return;
|
|
171
|
+
const next = new URLSearchParams(search);
|
|
172
|
+
// `set` keeps a parameter where it stands; only the aliases (and, at the end, the
|
|
173
|
+
// tour's own) go.
|
|
174
|
+
for (const key of tourId ? own.slice(2) : own) next.delete(key);
|
|
175
|
+
if (tourId) {
|
|
176
|
+
next.set("tour", tourId);
|
|
177
|
+
next.set("tourStep", String(index + 1));
|
|
178
|
+
}
|
|
179
|
+
if (next.toString() === current.toString()) return;
|
|
180
|
+
// Spelled out, not `setSearchParams` ("?…" drops the hash, keksdose dev#495).
|
|
181
|
+
void navigate({ pathname, search: next.toString(), hash }, { replace: true, state: location.state });
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, [active, activeId, index, pathname, search, hash, settled, aliases]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── useTourCatalogueUrlState ───────────────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
/** The catalogue's state filter. */
|
|
189
|
+
export type TourStateFilter = "all" | "todo" | "done";
|
|
190
|
+
|
|
191
|
+
/** The catalogue's filters: the search, the group (`null`: every group), the state. */
|
|
192
|
+
export interface TourCatalogueFilter {
|
|
193
|
+
query: string;
|
|
194
|
+
group: string | null;
|
|
195
|
+
state: TourStateFilter;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const STATES: readonly TourStateFilter[] = ["all", "todo", "done"];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The catalogue's filters in the URL (§2.8) — keksdose's `?q`, `?topic`, `?state`, each
|
|
202
|
+
* written with `replace` (typing does not bury the Back button), the defaults as the
|
|
203
|
+
* clean URL. Spread it onto the catalogue: `<TourCatalogue {...useTourCatalogueUrlState()} … />`.
|
|
204
|
+
* `params` renames them (kastlan's `?group`). Requires a react-router Router.
|
|
205
|
+
*/
|
|
206
|
+
export function useTourCatalogueUrlState(
|
|
207
|
+
params: { query?: string; group?: string; state?: string } = {},
|
|
208
|
+
): TourCatalogueFilter & { onChange: (patch: Partial<TourCatalogueFilter>) => void } {
|
|
209
|
+
const [filter, setFilter] = useSearchParamsState<TourCatalogueFilter>({
|
|
210
|
+
query: { param: params.query ?? "q", default: "", history: "replace" },
|
|
211
|
+
group: { param: params.group ?? "topic", default: null, history: "replace", parse: (raw) => raw || undefined },
|
|
212
|
+
state: {
|
|
213
|
+
param: params.state ?? "state",
|
|
214
|
+
default: "all",
|
|
215
|
+
history: "replace",
|
|
216
|
+
parse: (raw) => STATES.find((x) => x === raw),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
// `group: null` is a value to the setter (it clears `?topic`); `undefined` leaves it.
|
|
220
|
+
return { ...filter, onChange: setFilter };
|
|
221
|
+
}
|
package/src/tour.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
// `@eifi1/ui-kit/tour` — guided tours.
|
|
2
2
|
// A re-slicing of the main barrel, not a new API.
|
|
3
3
|
export * from "./tour/tour";
|
|
4
|
+
// 0.43.0: the tours page (docs/tours-harmonization.md §3, §8) — keksdose's catalogue, its
|
|
5
|
+
// card, the NEW and reset rules, device progress and the URL. `useTourUrlSync` and
|
|
6
|
+
// `useTourCatalogueUrlState` need react-router (a peer already, for /shell).
|
|
7
|
+
export { TourCatalogue, TourCatalogueCard, DEFAULT_TOUR_CATALOGUE_LABELS } from "./tour/tour-catalogue";
|
|
8
|
+
export type { TourCatalogueProps, TourCatalogueCardProps, TourCatalogueLabels } from "./tour/tour-catalogue";
|
|
9
|
+
export { isTourNew, isTourReset, isTourDone, doneCount, earnedPoints } from "./tour/tour-info";
|
|
10
|
+
export type { TourInfo, TourGroup, TourUpdate, TourCompletions } from "./tour/tour-info";
|
|
11
|
+
export { useTourProgress, resetTourProgress } from "./tour/use-tour-progress";
|
|
12
|
+
export type { UseTourProgressOptions, TourProgress } from "./tour/use-tour-progress";
|
|
13
|
+
export { useTourUrlSync, useTourCatalogueUrlState } from "./tour/use-tour-url-sync";
|
|
14
|
+
export type { UseTourUrlSyncOptions, TourCatalogueFilter, TourStateFilter } from "./tour/use-tour-url-sync";
|