@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.
Files changed (130) hide show
  1. package/README.md +12 -6
  2. package/dist/admin/admin-area-labels.d.ts +4 -0
  3. package/dist/admin/admin-area-labels.js +3 -1
  4. package/dist/admin/admin-area-labels.js.map +1 -1
  5. package/dist/admin/admin-overview.d.ts +25 -8
  6. package/dist/admin/admin-overview.js +26 -3
  7. package/dist/admin/admin-overview.js.map +1 -1
  8. package/dist/admin/admin-sections.d.ts +36 -5
  9. package/dist/admin/admin-sections.js +35 -18
  10. package/dist/admin/admin-sections.js.map +1 -1
  11. package/dist/billing/billing-banner.d.ts +8 -0
  12. package/dist/billing/billing-banner.js +25 -2
  13. package/dist/billing/billing-banner.js.map +1 -1
  14. package/dist/billing/billing-labels.d.ts +4 -0
  15. package/dist/billing/billing-labels.js +1 -0
  16. package/dist/billing/billing-labels.js.map +1 -1
  17. package/dist/billing/cancel-request.d.ts +53 -0
  18. package/dist/billing/cancel-request.js +21 -0
  19. package/dist/billing/cancel-request.js.map +1 -0
  20. package/dist/billing/provider-page.d.ts +40 -0
  21. package/dist/billing/provider-page.js +33 -0
  22. package/dist/billing/provider-page.js.map +1 -0
  23. package/dist/billing/subscription-actions.d.ts +32 -1
  24. package/dist/billing/subscription-actions.js +57 -4
  25. package/dist/billing/subscription-actions.js.map +1 -1
  26. package/dist/components/account-chips.d.ts +8 -1
  27. package/dist/components/account-chips.js.map +1 -1
  28. package/dist/eslint/index.js +1 -1
  29. package/dist/feedback/feedback-menu.d.ts +7 -4
  30. package/dist/feedback/feedback-menu.js +4 -2
  31. package/dist/feedback/feedback-menu.js.map +1 -1
  32. package/dist/hooks/use-search-param-state.js +4 -1
  33. package/dist/hooks/use-search-param-state.js.map +1 -1
  34. package/dist/i18n/defaults.js +2 -0
  35. package/dist/i18n/defaults.js.map +1 -1
  36. package/dist/i18n/german.js +36 -2
  37. package/dist/i18n/german.js.map +1 -1
  38. package/dist/i18n/kit-labels.d.ts +3 -0
  39. package/dist/i18n/kit-labels.js.map +1 -1
  40. package/dist/i18n/locales/en.js +51 -2
  41. package/dist/i18n/locales/en.js.map +3 -3
  42. package/dist/i18n/locales/es.js +36 -2
  43. package/dist/i18n/locales/es.js.map +1 -1
  44. package/dist/i18n/locales/fr.js +36 -2
  45. package/dist/i18n/locales/fr.js.map +1 -1
  46. package/dist/i18n/locales/hu.js +36 -2
  47. package/dist/i18n/locales/hu.js.map +1 -1
  48. package/dist/i18n/locales/it.js +36 -2
  49. package/dist/i18n/locales/it.js.map +1 -1
  50. package/dist/i18n/locales/zh.js +36 -2
  51. package/dist/i18n/locales/zh.js.map +1 -1
  52. package/dist/i18n/review.js +15 -1
  53. package/dist/i18n/review.js.map +1 -1
  54. package/dist/index.d.ts +6 -0
  55. package/dist/index.js +6 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/search/global-search.d.ts +3 -0
  58. package/dist/search/global-search.js +3 -1
  59. package/dist/search/global-search.js.map +1 -1
  60. package/dist/settings/settings-catalogue.d.ts +9 -1
  61. package/dist/settings/settings-catalogue.js +1 -1
  62. package/dist/settings/settings-catalogue.js.map +1 -1
  63. package/dist/shell/admin-menu.d.ts +26 -3
  64. package/dist/shell/admin-menu.js +8 -4
  65. package/dist/shell/admin-menu.js.map +1 -1
  66. package/dist/shell/dev-switchers.js +5 -0
  67. package/dist/shell/dev-switchers.js.map +1 -1
  68. package/dist/shell/top-bar-parts-labels.d.ts +6 -0
  69. package/dist/shell/top-bar-parts-labels.js +4 -1
  70. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  71. package/dist/shell/tour-menu.d.ts +19 -10
  72. package/dist/shell/tour-menu.js +20 -6
  73. package/dist/shell/tour-menu.js.map +1 -1
  74. package/dist/shell.d.ts +2 -0
  75. package/dist/shell.js +2 -0
  76. package/dist/shell.js.map +1 -1
  77. package/dist/tour/tour-catalogue-labels.d.ts +44 -0
  78. package/dist/tour/tour-catalogue-labels.js +32 -0
  79. package/dist/tour/tour-catalogue-labels.js.map +1 -0
  80. package/dist/tour/tour-catalogue.d.ts +68 -0
  81. package/dist/tour/tour-catalogue.js +189 -0
  82. package/dist/tour/tour-catalogue.js.map +1 -0
  83. package/dist/tour/tour-info.d.ts +112 -0
  84. package/dist/tour/tour-info.js +36 -0
  85. package/dist/tour/tour-info.js.map +1 -0
  86. package/dist/tour/use-tour-progress.d.ts +61 -0
  87. package/dist/tour/use-tour-progress.js +116 -0
  88. package/dist/tour/use-tour-progress.js.map +1 -0
  89. package/dist/tour/use-tour-url-sync.d.ts +58 -0
  90. package/dist/tour/use-tour-url-sync.js +110 -0
  91. package/dist/tour/use-tour-url-sync.js.map +1 -0
  92. package/dist/tour.d.ts +8 -0
  93. package/dist/tour.js +18 -0
  94. package/dist/tour.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/admin/admin-area-labels.ts +7 -1
  97. package/src/admin/admin-overview.tsx +61 -10
  98. package/src/admin/admin-sections.tsx +109 -48
  99. package/src/billing/billing-banner.tsx +25 -2
  100. package/src/billing/billing-labels.ts +6 -0
  101. package/src/billing/cancel-request.ts +75 -0
  102. package/src/billing/provider-page.ts +75 -0
  103. package/src/billing/subscription-actions.tsx +97 -11
  104. package/src/components/account-chips.tsx +8 -1
  105. package/src/feedback/feedback-menu.tsx +8 -3
  106. package/src/hooks/use-search-param-state.ts +6 -1
  107. package/src/i18n/defaults.ts +2 -0
  108. package/src/i18n/german.ts +36 -1
  109. package/src/i18n/kit-labels.tsx +3 -0
  110. package/src/i18n/locales/en.ts +13 -0
  111. package/src/i18n/locales/es.ts +36 -1
  112. package/src/i18n/locales/fr.ts +36 -1
  113. package/src/i18n/locales/hu.ts +36 -1
  114. package/src/i18n/locales/it.ts +36 -1
  115. package/src/i18n/locales/zh.ts +36 -1
  116. package/src/i18n/review.ts +14 -0
  117. package/src/index.ts +8 -0
  118. package/src/search/global-search.tsx +5 -0
  119. package/src/settings/settings-catalogue.ts +10 -2
  120. package/src/shell/admin-menu.tsx +34 -5
  121. package/src/shell/dev-switchers.tsx +8 -0
  122. package/src/shell/top-bar-parts-labels.ts +9 -0
  123. package/src/shell/tour-menu.tsx +50 -15
  124. package/src/shell.ts +3 -0
  125. package/src/tour/tour-catalogue-labels.ts +75 -0
  126. package/src/tour/tour-catalogue.tsx +316 -0
  127. package/src/tour/tour-info.ts +144 -0
  128. package/src/tour/use-tour-progress.ts +198 -0
  129. package/src/tour/use-tour-url-sync.ts +221 -0
  130. package/src/tour.ts +11 -0
@@ -3,13 +3,17 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useLocation } from "react-router";
4
4
  import { Check, Compass } from "lucide-react";
5
5
  import { Chip } from "../components/chip.js";
6
+ import { isTourDone, isTourNew, isTourReset } from "../tour/tour-info.js";
6
7
  import { TopBarActionMenu } from "./topbar-action-menu.js";
7
8
  import { useTopBarPartsLabels } from "./top-bar-parts-labels.js";
8
9
  const pageOf = (path) => `/${path.split(/[?#]/)[0].split("/")[1] ?? ""}`;
9
- const isFresh = (t) => !!t.isNew && !t.completed;
10
+ const isFresh = (t) => !!t.isNew && !t.completed || !!t.reset;
11
+ const newsOf = (t) => (t.reset ? t.updated?.date : void 0) ?? t.since ?? "";
10
12
  function TourMenu({
11
- tours,
13
+ tours: toursProp,
12
14
  onLaunch,
15
+ completed: completions,
16
+ joined,
13
17
  href,
14
18
  pathname: pathnameProp,
15
19
  maxNew = 3,
@@ -20,16 +24,26 @@ function TourMenu({
20
24
  const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : void 0).tours;
21
25
  const location = useLocation();
22
26
  const here = pageOf(pathnameProp ?? location.pathname);
27
+ const tours = completions === void 0 && joined === void 0 ? toursProp : toursProp.map((t) => {
28
+ const at = completions?.[t.id];
29
+ const ever = completions ? at !== void 0 : !!t.completed;
30
+ return {
31
+ ...t,
32
+ completed: completions ? isTourDone(t, at) : t.completed,
33
+ reset: completions ? isTourReset(t, at) : t.reset,
34
+ isNew: joined !== void 0 ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew
35
+ };
36
+ });
23
37
  const forThisPage = tours.filter((t) => t.route !== void 0 && pageOf(t.route) === here);
24
38
  const fresh = tours.filter(isFresh);
25
- const newRows = fresh.filter((t) => !forThisPage.includes(t)).sort((a, b) => (b.since ?? "").localeCompare(a.since ?? "")).slice(0, maxNew);
39
+ const newRows = fresh.filter((t) => !forThisPage.includes(t)).sort((a, b) => newsOf(b).localeCompare(newsOf(a))).slice(0, maxNew);
26
40
  const rest = href && list !== "all" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));
27
41
  if (tours.length === 0 && !href) return null;
28
42
  const entryFor = (tour) => ({
29
43
  key: tour.id,
30
44
  icon: tour.icon,
31
45
  label: tour.title,
32
- trailing: tour.completed ? /* @__PURE__ */ jsxs(Fragment, { children: [
46
+ trailing: tour.reset ? /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "brand", caps: true, className: "px-1.5", children: words.badgeUpdated }) : tour.completed ? /* @__PURE__ */ jsxs(Fragment, { children: [
33
47
  /* @__PURE__ */ jsx(Check, { "aria-hidden": true, className: "size-4 text-success" }),
34
48
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: words.completed })
35
49
  ] }) : tour.isNew ? (
@@ -40,7 +54,7 @@ function TourMenu({
40
54
  });
41
55
  const blocks = [
42
56
  { key: "this-page", label: words.thisPage, tours: forThisPage },
43
- { key: "new", label: words.newTours, tours: newRows }
57
+ { key: "new", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows }
44
58
  ].filter((b) => b.tours.length > 0);
45
59
  const tail = [
46
60
  ...rest.map(entryFor),
@@ -58,7 +72,7 @@ function TourMenu({
58
72
  TopBarActionMenu,
59
73
  {
60
74
  icon: /* @__PURE__ */ jsx(Compass, { className: "size-5" }),
61
- iconBadge: fresh.length > 0 ? { label: words.newDot, tone: "brand" } : null,
75
+ iconBadge: fresh.length > 0 ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: "brand" } : null,
62
76
  "aria-label": words.menu,
63
77
  href,
64
78
  panelClassName: "w-72 max-h-[70vh] overflow-y-auto",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n */\nexport interface TourMenuTour {\n id: string;\n title: ReactNode;\n /** The page the tour starts on. Its first segment decides \"On this page\":\n * \"/reports/spending\" belongs to \"/reports\". */\n route?: string;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** When the tour shipped, as an ISO date: the newest of the new come first. */\n since?: string;\n}\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => !!t.isNew && !t.completed;\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours,\n onLaunch,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => (b.since ?? \"\").localeCompare(a.since ?? \"\"))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [{ kind: \"link\" as const, key: \"all-tours\", icon: <Compass className=\"size-4\" />, label: words.allTours, to: href }]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={fresh.length > 0 ? { label: words.newDot, tone: \"brand\" } : null}\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/tour-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useLocation } from \"react-router\";\nimport { Check, Compass } from \"lucide-react\";\nimport { Chip } from \"../components/chip\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from \"../tour/tour-info\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"./topbar-action-menu\";\nimport { useTopBarPartsLabels, type TourMenuLabels } from \"./top-bar-parts-labels\";\n\n/**\n * One tour as the menu needs it — data, so the menu depends on no app's tour registry.\n * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,\n * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,\n * its progress store).\n *\n * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md\n * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`\n * and `joined`, and it works out ✓, NEW and \"Updated\" itself. Given the record, the kit's\n * ✓ and \"Updated\" win over a row's `completed` and `reset`; given `joined`, its NEW wins\n * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,\n * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.\n */\nexport type TourMenuTour = Partial<Pick<TourInfo, \"route\" | \"since\" | \"updated\">> & {\n id: string;\n title: ReactNode;\n /** The row's icon, drawn at 16px. */\n icon?: ReactNode;\n /** Shipped after this account was opened and not yet taken: a NEW chip on the row,\n * and the dot on the compass. */\n isNew?: boolean;\n /** Taken: a ✓ on the row. */\n completed?: boolean;\n /** Taken before its last update (§8): an \"Updated\" chip, and the dot, as for NEW. */\n reset?: boolean;\n};\n\nexport interface TourMenuProps {\n tours: readonly TourMenuTour[];\n /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */\n onLaunch: (id: string) => void;\n /** The device's completions (`useTourProgress().completed`): ✓ and \"Updated\" from them. */\n completed?: TourCompletions;\n /** The account's `created_at`: NEW from it, by `isTourNew`. */\n joined?: string | null;\n /**\n * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a\n * hover menu, and the menu ends in \"All tours…\". Without it (§6.7, Kurvenschmiede) the\n * compass opens the menu, and the tours on neither list follow below a rule.\n */\n href?: string;\n /** The open page's path. Default: the router's location. */\n pathname?: string;\n /** How many new tours the menu offers beyond the page's own. Default 3. */\n maxNew?: number;\n /**\n * `\"all\"`: list every tour — this page's and the new ones first, then the rest below a\n * rule — with `href` too, before \"All tours…\". For an app whose tours carry no `route`,\n * `isNew` or `since` (kastlan, build review A1), where the short list would be empty\n * off the tours' page. Default `\"short\"`: with `href`, only this page's and the new.\n */\n list?: \"short\" | \"all\";\n /** Prop > `<UiKitProvider labels={{ topBarParts: { tours } }}>` > English. */\n labels?: LabelOverride<TourMenuLabels>;\n /** `data-*` attributes go on the compass — a tour's anchor (`data-tour=\"tours\"`). */\n [key: `data-${string}`]: string | number | boolean | undefined;\n}\n\n/** A path's first segment — \"/reports/spending?g=month\" → \"/reports\". */\nconst pageOf = (path: string) => `/${path.split(/[?#]/)[0].split(\"/\")[1] ?? \"\"}`;\n\nconst isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;\n/** When it last became news: its update, else its ship date. */\nconst newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? \"\";\n\n/**\n * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted\n * from keksdose's: the lucide `Compass`, with a NEW dot while a tour is new, and a short\n * list — the tours that start on this page, up to three new ones (newest first), a rule,\n * and \"All tours…\". A taken tour's row carries a ✓, a new one a NEW chip.\n */\nexport function TourMenu({\n tours: toursProp,\n onLaunch,\n completed: completions,\n joined,\n href,\n pathname: pathnameProp,\n maxNew = 3,\n list = \"short\",\n labels: labelsProp,\n ...attributes\n}: TourMenuProps) {\n const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;\n const location = useLocation();\n const here = pageOf(pathnameProp ?? location.pathname);\n const tours =\n completions === undefined && joined === undefined\n ? toursProp\n : toursProp.map((t) => {\n const at = completions?.[t.id];\n // Ever completed: the record when there is one, else the row's own word.\n const ever = completions ? at !== undefined : !!t.completed;\n return {\n ...t,\n completed: completions ? isTourDone(t, at) : t.completed,\n reset: completions ? isTourReset(t, at) : t.reset,\n isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,\n };\n });\n\n const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);\n const fresh = tours.filter(isFresh);\n // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.\n const newRows = fresh\n .filter((t) => !forThisPage.includes(t))\n .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))\n .slice(0, maxNew);\n const rest = href && list !== \"all\" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));\n\n if (tours.length === 0 && !href) return null;\n\n const entryFor = (tour: TourMenuTour): TopBarMenuEntry => ({\n key: tour.id,\n icon: tour.icon,\n label: tour.title,\n trailing: tour.reset ? (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n ) : tour.completed ? (\n <>\n <Check aria-hidden className=\"size-4 text-success\" />\n <span className=\"sr-only\">{words.completed}</span>\n </>\n ) : tour.isNew ? (\n // The kit's inert Chip: a plain <span>, no role of its own inside the menu.\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n ) : undefined,\n onSelect: () => onLaunch(tour.id),\n });\n\n const blocks = [\n { key: \"this-page\", label: words.thisPage, tours: forThisPage },\n { key: \"new\", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },\n ].filter((b) => b.tours.length > 0);\n const tail: TopBarMenuEntry[] = [\n ...rest.map(entryFor),\n ...(href\n ? [{ kind: \"link\" as const, key: \"all-tours\", icon: <Compass className=\"size-4\" />, label: words.allTours, to: href }]\n : []),\n ];\n\n const entries: TopBarMenuEntry[] = [\n ...blocks.flatMap((b): TopBarMenuEntry[] => [\n { kind: \"heading\", key: `head-${b.key}`, label: b.label },\n ...b.tours.map(entryFor),\n ]),\n ...(blocks.length > 0 && tail.length > 0 ? [{ kind: \"divider\" as const, key: \"sep-all\" }] : []),\n ...tail,\n ];\n\n return (\n <TopBarActionMenu\n icon={<Compass className=\"size-5\" />}\n iconBadge={\n fresh.length > 0\n ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: \"brand\" }\n : null\n }\n aria-label={words.menu}\n href={href}\n panelClassName=\"w-72 max-h-[70vh] overflow-y-auto\"\n entries={entries}\n triggerAttributes={Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith(\"data-\")))}\n />\n );\n}\n"],"mappings":"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","names":[]}
package/dist/shell.d.ts CHANGED
@@ -10,6 +10,8 @@ export { FillHeight, FILL_HEIGHT_CLASS } from "./shell/fill-height.js";
10
10
  export type { FillHeightProps } from "./shell/fill-height.js";
11
11
  export { adminMenuEntry, AdminMenuRow } from "./shell/admin-menu.js";
12
12
  export type { AdminMenuEntryOptions, AdminMenuRowProps } from "./shell/admin-menu.js";
13
+ export { useAdminMenuEntry } from "./shell/admin-menu.js";
14
+ export type { UseAdminMenuEntryOptions } from "./shell/admin-menu.js";
13
15
  export { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout.js";
14
16
  export type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from "./settings/settings-layout.js";
15
17
  export { useSettingsRoute, resolveSettingsLocation, settingsFromListState, SETTINGS_FROM_LIST_STATE, } from "./settings/use-settings-route.js";
package/dist/shell.js CHANGED
@@ -9,6 +9,7 @@ export * from "./shell/top-bar-brand.js";
9
9
  export * from "./shell/auth-layout.js";
10
10
  import { FillHeight, FILL_HEIGHT_CLASS } from "./shell/fill-height.js";
11
11
  import { adminMenuEntry, AdminMenuRow } from "./shell/admin-menu.js";
12
+ import { useAdminMenuEntry } from "./shell/admin-menu.js";
12
13
  import { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout.js";
13
14
  import {
14
15
  useSettingsRoute,
@@ -76,6 +77,7 @@ export {
76
77
  settingsHref,
77
78
  settingsSearchEntries,
78
79
  syncState,
80
+ useAdminMenuEntry,
79
81
  useLastVisitedPage,
80
82
  useSettingsFocus,
81
83
  useSettingsHeadingLevel,
package/dist/shell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\nexport { DevSwitchers, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;AACzE,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAE/C,SAAS,cAAc,6BAA6B;AAEpD,SAAS,8BAA8B,4BAA4B;","names":[]}
1
+ {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\nexport * from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/role-switcher\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport {\n useSettingsRoute,\n resolveSettingsLocation,\n settingsFromListState,\n SETTINGS_FROM_LIST_STATE,\n} from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev switchers, and their words.\nexport { TopBarActions, TOP_BAR_ACTION_ORDER } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator, syncState } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\nexport { DevSwitchers, DEV_ORIGINAL_ROLE_KEY } from \"./shell/dev-switchers\";\nexport type { DevSwitchersProps } from \"./shell/dev-switchers\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAGlC,SAAS,gBAAgB,iCAAiC;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP,SAAS,kBAAkB,qBAAqB,yBAAyB;AACzE,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,eAAe,4BAA4B;AAEpD,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,iBAAiB;AAE/C,SAAS,cAAc,6BAA6B;AAEpD,SAAS,8BAA8B,4BAA4B;","names":[]}
@@ -0,0 +1,44 @@
1
+ /** The `tourCatalogue` namespace. Counts arrive as numbers, dates already formatted. */
2
+ export interface TourCatalogueLabels {
3
+ /** The search box's name. */
4
+ searchLabel: string;
5
+ searchPlaceholder: string;
6
+ /** The group select's name (keksdose: "Topic"). */
7
+ groupLabel: string;
8
+ /** The select's first option, with the count the other filters leave. */
9
+ groupAll: (count: number) => string;
10
+ /** A group's option: its title and its count. */
11
+ groupOption: (group: string, count: number) => string;
12
+ /** The all / to do / done toggle's name. */
13
+ stateLabel: string;
14
+ stateAll: (count: number) => string;
15
+ stateTodo: (count: number) => string;
16
+ stateDone: (count: number) => string;
17
+ /** Under the filter bar while a filter is on. */
18
+ showing: (shown: number, total: number) => string;
19
+ noMatches: string;
20
+ noMatchesHint: string;
21
+ clearFilters: string;
22
+ /** The NEW pill. */
23
+ badgeNew: string;
24
+ /** The pill on a tour reset by an update (§8). */
25
+ badgeUpdated: string;
26
+ /** A reset tour's line: the update's day (formatted) and its reason (§8); `reason` is
27
+ * left out when the tour's update carries none. */
28
+ resetOn: (date: string, reason?: string) => string;
29
+ /** The points chip. */
30
+ points: (count: number) => string;
31
+ steps: (count: number) => string;
32
+ /** The duration, in minutes. */
33
+ duration: (minutes: number) => string;
34
+ /** Completed, with the day (formatted). */
35
+ completedOn: (date: string) => string;
36
+ /** Completed without a date (kastlan's old id list, §6.2). */
37
+ completed: string;
38
+ /** Not taken yet — the state icon, for a reader. */
39
+ notCompleted: string;
40
+ markUndone: string;
41
+ start: string;
42
+ replay: string;
43
+ }
44
+ export declare const DEFAULT_TOUR_CATALOGUE_LABELS: TourCatalogueLabels;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ const DEFAULT_TOUR_CATALOGUE_LABELS = {
3
+ searchLabel: "Search tours",
4
+ searchPlaceholder: "Search tours\u2026",
5
+ groupLabel: "Topic",
6
+ groupAll: (count) => `All topics (${count})`,
7
+ groupOption: (group, count) => `${group} (${count})`,
8
+ stateLabel: "Filter tours",
9
+ stateAll: (count) => `All (${count})`,
10
+ stateTodo: (count) => `To do (${count})`,
11
+ stateDone: (count) => `Completed (${count})`,
12
+ showing: (shown, total) => `Showing ${shown} of ${total} tours`,
13
+ noMatches: "No tour matches",
14
+ noMatchesHint: "Try another word, or clear the filters.",
15
+ clearFilters: "Clear filters",
16
+ badgeNew: "New",
17
+ badgeUpdated: "Updated",
18
+ resetOn: (date, reason) => reason ? `Reset on ${date}: ${reason}` : `Reset on ${date}`,
19
+ points: (count) => `+${count} pts`,
20
+ steps: (count) => count === 1 ? "1 step" : `${count} steps`,
21
+ duration: (minutes) => `About ${minutes} min`,
22
+ completedOn: (date) => `Completed on ${date}`,
23
+ completed: "Completed",
24
+ notCompleted: "Not completed",
25
+ markUndone: "Mark as not done",
26
+ start: "Start",
27
+ replay: "Replay"
28
+ };
29
+ export {
30
+ DEFAULT_TOUR_CATALOGUE_LABELS
31
+ };
32
+ //# sourceMappingURL=tour-catalogue-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tour/tour-catalogue-labels.ts"],"sourcesContent":["// The English words of `TourCatalogue` and `TourCatalogueCard` (docs/tours-harmonization.md\n// §3), in a module with no component code, like every `*-labels.ts` (0.34, KS U8).\n// keksdose's reviewed words (tour.* in its en.json) where it had them.\n\n/** The `tourCatalogue` namespace. Counts arrive as numbers, dates already formatted. */\nexport interface TourCatalogueLabels {\n /** The search box's name. */\n searchLabel: string;\n searchPlaceholder: string;\n /** The group select's name (keksdose: \"Topic\"). */\n groupLabel: string;\n /** The select's first option, with the count the other filters leave. */\n groupAll: (count: number) => string;\n /** A group's option: its title and its count. */\n groupOption: (group: string, count: number) => string;\n /** The all / to do / done toggle's name. */\n stateLabel: string;\n stateAll: (count: number) => string;\n stateTodo: (count: number) => string;\n stateDone: (count: number) => string;\n /** Under the filter bar while a filter is on. */\n showing: (shown: number, total: number) => string;\n noMatches: string;\n noMatchesHint: string;\n clearFilters: string;\n /** The NEW pill. */\n badgeNew: string;\n /** The pill on a tour reset by an update (§8). */\n badgeUpdated: string;\n /** A reset tour's line: the update's day (formatted) and its reason (§8); `reason` is\n * left out when the tour's update carries none. */\n resetOn: (date: string, reason?: string) => string;\n /** The points chip. */\n points: (count: number) => string;\n steps: (count: number) => string;\n /** The duration, in minutes. */\n duration: (minutes: number) => string;\n /** Completed, with the day (formatted). */\n completedOn: (date: string) => string;\n /** Completed without a date (kastlan's old id list, §6.2). */\n completed: string;\n /** Not taken yet — the state icon, for a reader. */\n notCompleted: string;\n markUndone: string;\n start: string;\n replay: string;\n}\n\nexport const DEFAULT_TOUR_CATALOGUE_LABELS: TourCatalogueLabels = {\n searchLabel: \"Search tours\",\n searchPlaceholder: \"Search tours…\",\n groupLabel: \"Topic\",\n groupAll: (count) => `All topics (${count})`,\n groupOption: (group, count) => `${group} (${count})`,\n stateLabel: \"Filter tours\",\n stateAll: (count) => `All (${count})`,\n stateTodo: (count) => `To do (${count})`,\n stateDone: (count) => `Completed (${count})`,\n showing: (shown, total) => `Showing ${shown} of ${total} tours`,\n noMatches: \"No tour matches\",\n noMatchesHint: \"Try another word, or clear the filters.\",\n clearFilters: \"Clear filters\",\n badgeNew: \"New\",\n badgeUpdated: \"Updated\",\n resetOn: (date, reason) => (reason ? `Reset on ${date}: ${reason}` : `Reset on ${date}`),\n points: (count) => `+${count} pts`,\n steps: (count) => (count === 1 ? \"1 step\" : `${count} steps`),\n duration: (minutes) => `About ${minutes} min`,\n completedOn: (date) => `Completed on ${date}`,\n completed: \"Completed\",\n notCompleted: \"Not completed\",\n markUndone: \"Mark as not done\",\n start: \"Start\",\n replay: \"Replay\",\n};\n"],"mappings":";AAgDO,MAAM,gCAAqD;AAAA,EAChE,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,eAAe,KAAK;AAAA,EACzC,aAAa,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,EACjD,YAAY;AAAA,EACZ,UAAU,CAAC,UAAU,QAAQ,KAAK;AAAA,EAClC,WAAW,CAAC,UAAU,UAAU,KAAK;AAAA,EACrC,WAAW,CAAC,UAAU,cAAc,KAAK;AAAA,EACzC,SAAS,CAAC,OAAO,UAAU,WAAW,KAAK,OAAO,KAAK;AAAA,EACvD,WAAW;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd,UAAU;AAAA,EACV,cAAc;AAAA,EACd,SAAS,CAAC,MAAM,WAAY,SAAS,YAAY,IAAI,KAAK,MAAM,KAAK,YAAY,IAAI;AAAA,EACrF,QAAQ,CAAC,UAAU,IAAI,KAAK;AAAA,EAC5B,OAAO,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAAA,EACpD,UAAU,CAAC,YAAY,SAAS,OAAO;AAAA,EACvC,aAAa,CAAC,SAAS,gBAAgB,IAAI;AAAA,EAC3C,WAAW;AAAA,EACX,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
@@ -0,0 +1,68 @@
1
+ import { type ReactNode } from "react";
2
+ import { type LabelOverride } from "../i18n/kit-labels.js";
3
+ import { DEFAULT_TOUR_CATALOGUE_LABELS, type TourCatalogueLabels } from "./tour-catalogue-labels.js";
4
+ import { type TourCompletions, type TourGroup, type TourInfo } from "./tour-info.js";
5
+ import type { TourCatalogueFilter } from "./use-tour-url-sync.js";
6
+ export { DEFAULT_TOUR_CATALOGUE_LABELS };
7
+ export type { TourCatalogueLabels };
8
+ export interface TourCatalogueCardProps {
9
+ tour: TourInfo;
10
+ /** The completion's ISO timestamp, `null` for one without a date; absent while untaken. */
11
+ completedAt?: string | null;
12
+ /** NEW — `isTourNew`. The catalogue works it out; a card on its own is told. */
13
+ isNew?: boolean;
14
+ onLaunch: (id: string) => void;
15
+ /** "Mark as not done". Without it, the link is not drawn. */
16
+ onReset?: (id: string) => void;
17
+ /** Formats the completion and update days. Default: the kit's medium date in the provider's locale. */
18
+ formatDate?: (iso: string) => string;
19
+ labels?: LabelOverride<TourCatalogueLabels>;
20
+ className?: string;
21
+ }
22
+ /**
23
+ * One tour's card — keksdose's (tours-page.tsx `TourCard`, dev#557), laid out top to
24
+ * bottom so the cards sit side by side: the state icon beside a column holding the tour's
25
+ * icon, title, NEW (or "Updated", §8) and the app's `badge`, the one-liner and, from `sm`,
26
+ * the longer account. The foot is pinned to the bottom edge, so every Start in a row of
27
+ * cards lands on one line: the points chip, the step count and the duration, the
28
+ * completion (or reset) line, "Mark as not done" and Start / Replay. Points, steps and
29
+ * duration are each left out when the tour has none — no gap, no "0 points" (KS).
30
+ */
31
+ export declare function TourCatalogueCard({ tour, completedAt, isNew, onLaunch, onReset, formatDate: formatDateProp, labels: labelsProp, className, }: TourCatalogueCardProps): import("react").JSX.Element;
32
+ export interface TourCatalogueProps extends TourCatalogueFilter {
33
+ /** The tours this person may take, already filtered by role and data (§6.3). */
34
+ tours: readonly TourInfo[];
35
+ /** The sections, in the app's order. A tour whose group is not here is not listed. */
36
+ groups: readonly TourGroup[];
37
+ completed: TourCompletions;
38
+ /** The account's `created_at`: a tour shipped after it, untaken, is NEW. Unknown → none is. */
39
+ joined?: string | null;
40
+ /** A filter changed — `{query}`, `{group}`, `{state}`, or all three for "Clear filters". */
41
+ onChange: (patch: Partial<TourCatalogueFilter>) => void;
42
+ onLaunch: (id: string) => void;
43
+ /** "Mark as not done" — `useTourProgress().reset`. */
44
+ onReset?: (id: string) => void;
45
+ /** Above the filter bar: the app's page header with its points tile (§6.1). */
46
+ header?: ReactNode;
47
+ formatDate?: (iso: string) => string;
48
+ /** The search's locale (case folding). Default: the provider's. */
49
+ locale?: string;
50
+ labels?: LabelOverride<TourCatalogueLabels>;
51
+ className?: string;
52
+ }
53
+ /**
54
+ * The tours page's body (docs/tours-harmonization.md §2.1, §3) — keksdose's /tours:
55
+ *
56
+ * - **The filter bar**: a search, a group select and an all / to do / done toggle,
57
+ * stacked below `lg` and side by side from it. Each count is what the OTHER filters
58
+ * leave: the toggle counts the search and the group, each group the search and the
59
+ * state. Under it, while a filter is on, "Showing X of Y tours".
60
+ * - **The sections**: one per group, in the app's order — an h2 with its icon and help
61
+ * line over a `sm:2 / xl:3` grid of `TourCatalogueCard`s. A group the filters empty is
62
+ * hidden; when nothing is left, an empty state with "Clear filters".
63
+ * - **Controlled**: `query`, `group`, `state` and `onChange`, so the app keeps them in its
64
+ * URL (`useTourCatalogueUrlState`).
65
+ *
66
+ * A tour reset by an update (§8) is "to do" for the toggle and the counts.
67
+ */
68
+ export declare function TourCatalogue({ tours, groups, completed, joined, query, group: groupProp, state: stateProp, onChange, onLaunch, onReset, header, formatDate, locale: localeProp, labels: labelsProp, className, }: TourCatalogueProps): import("react").JSX.Element;
@@ -0,0 +1,189 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { Circle, CircleCheck, Clock, Play } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useKitFormat } from "../lib/format.js";
7
+ import { foldForSearch } from "../lib/text-match.js";
8
+ import { useKitLabels } from "../i18n/kit-labels.js";
9
+ import { Button } from "../components/button.js";
10
+ import { Card } from "../components/card.js";
11
+ import { Chip } from "../components/chip.js";
12
+ import { Caption } from "../components/text.js";
13
+ import { EmptyState } from "../components/empty-state.js";
14
+ import { SearchField } from "../components/search-field.js";
15
+ import { Select } from "../components/select.js";
16
+ import { ToggleGroup } from "../components/toggle-group.js";
17
+ import { DEFAULT_TOUR_CATALOGUE_LABELS } from "./tour-catalogue-labels.js";
18
+ import { isTourDone, isTourNew, isTourReset } from "./tour-info.js";
19
+ function TourCatalogueCard({
20
+ tour,
21
+ completedAt,
22
+ isNew = false,
23
+ onLaunch,
24
+ onReset,
25
+ formatDate: formatDateProp,
26
+ labels: labelsProp,
27
+ className
28
+ }) {
29
+ const words = useKitLabels("tourCatalogue", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);
30
+ const { formatDate } = useKitFormat();
31
+ const day = formatDateProp ?? ((iso) => formatDate(iso, "medium"));
32
+ const titleId = useId();
33
+ const done = isTourDone(tour, completedAt);
34
+ const reset = isTourReset(tour, completedAt);
35
+ const hasFacts = tour.points != null || tour.steps != null || tour.duration != null;
36
+ const note = done ? completedAt ? words.completedOn(day(completedAt)) : words.completed : reset && tour.updated ? words.resetOn(day(tour.updated.date), tour.updated.reason) : null;
37
+ return /* @__PURE__ */ jsxs(Card, { className: cn("flex h-full flex-col p-4", className), children: [
38
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 items-start gap-3", children: [
39
+ /* @__PURE__ */ jsxs("div", { className: cn("mt-0.5 shrink-0", done ? "text-success" : "text-muted"), children: [
40
+ done ? /* @__PURE__ */ jsx(CircleCheck, { "aria-hidden": true, className: "size-5" }) : /* @__PURE__ */ jsx(Circle, { "aria-hidden": true, className: "size-5" }),
41
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: done ? words.completed : words.notCompleted })
42
+ ] }),
43
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
44
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
45
+ tour.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 text-brand [&_svg]:size-4", children: tour.icon }),
46
+ /* @__PURE__ */ jsx("h3", { className: "font-medium", children: /* @__PURE__ */ jsx("span", { id: titleId, children: tour.title }) }),
47
+ isNew && !reset && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "brand", caps: true, className: "px-1.5", children: words.badgeNew }),
48
+ reset && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "brand", caps: true, className: "px-1.5", children: words.badgeUpdated }),
49
+ tour.badge
50
+ ] }),
51
+ tour.description && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted", children: tour.description }),
52
+ tour.about && /* @__PURE__ */ jsx("p", { className: "mt-2 hidden text-sm leading-relaxed text-secondary sm:block", children: tour.about })
53
+ ] })
54
+ ] }),
55
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-end justify-between gap-2", children: [
56
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
57
+ hasFacts && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
58
+ tour.points != null && /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "brand", children: words.points(tour.points) }),
59
+ tour.steps != null && /* @__PURE__ */ jsx(Caption, { as: "span", children: words.steps(tour.steps) }),
60
+ tour.duration != null && /* @__PURE__ */ jsxs(Caption, { as: "span", className: "inline-flex items-center gap-1", children: [
61
+ /* @__PURE__ */ jsx(Clock, { "aria-hidden": true, className: "size-3" }),
62
+ words.duration(tour.duration)
63
+ ] })
64
+ ] }),
65
+ note && /* @__PURE__ */ jsx("p", { className: cn("text-xs text-muted", hasFacts && "mt-1"), children: note })
66
+ ] }),
67
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
68
+ done && onReset && /* @__PURE__ */ jsx(Button, { variant: "link", tone: "muted", size: "sm", "aria-describedby": titleId, onClick: () => onReset(tour.id), children: words.markUndone }),
69
+ /* @__PURE__ */ jsxs(Button, { "aria-describedby": titleId, onClick: () => onLaunch(tour.id), children: [
70
+ /* @__PURE__ */ jsx(Play, { "aria-hidden": true, className: "size-4" }),
71
+ " ",
72
+ done ? words.replay : words.start
73
+ ] })
74
+ ] })
75
+ ] })
76
+ ] });
77
+ }
78
+ const STATE_FILTERS = ["all", "todo", "done"];
79
+ function TourCatalogue({
80
+ tours,
81
+ groups,
82
+ completed,
83
+ joined,
84
+ query,
85
+ group: groupProp,
86
+ state: stateProp,
87
+ onChange,
88
+ onLaunch,
89
+ onReset,
90
+ header,
91
+ formatDate,
92
+ locale: localeProp,
93
+ labels: labelsProp,
94
+ className
95
+ }) {
96
+ const words = useKitLabels("tourCatalogue", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);
97
+ const uid = useId();
98
+ const { locale: kitLocale } = useKitFormat();
99
+ const locale = localeProp ?? kitLocale;
100
+ const group = groups.some((g) => g.id === groupProp) ? groupProp : null;
101
+ const state = STATE_FILTERS.includes(stateProp) ? stateProp : "all";
102
+ const listed = tours.filter((t) => groups.some((g) => g.id === t.group));
103
+ const done = (tour) => isTourDone(tour, completed[tour.id]);
104
+ const needle = foldForSearch(query.trim(), locale);
105
+ const matchesQuery = (tour) => !needle || [tour.title, tour.description, tour.about].some((text) => !!text && foldForSearch(text, locale).includes(needle));
106
+ const inState = (tour) => state === "all" ? true : state === "done" ? done(tour) : !done(tour);
107
+ const inGroup = (tour, id = group) => id === null || tour.group === id;
108
+ const found = listed.filter((t) => matchesQuery(t) && inGroup(t));
109
+ const foundDone = found.filter(done).length;
110
+ const visible = found.filter(inState);
111
+ const groupCount = (id) => listed.filter((t) => matchesQuery(t) && inState(t) && inGroup(t, id)).length;
112
+ const filtering = needle.length > 0 || state !== "all" || group !== null;
113
+ const sections = groups.map((g) => ({ ...g, tours: visible.filter((t) => t.group === g.id) })).filter((g) => g.tours.length > 0);
114
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
115
+ header,
116
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 lg:flex-row lg:items-center", children: [
117
+ /* @__PURE__ */ jsx(
118
+ SearchField,
119
+ {
120
+ className: "lg:max-w-xs lg:flex-1",
121
+ value: query,
122
+ onChange: (value) => onChange({ query: value }),
123
+ "aria-label": words.searchLabel,
124
+ placeholder: words.searchPlaceholder
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsxs(
128
+ Select,
129
+ {
130
+ className: "lg:w-60",
131
+ "aria-label": words.groupLabel,
132
+ value: group ?? "",
133
+ onChange: (event) => onChange({ group: event.target.value || null }),
134
+ children: [
135
+ /* @__PURE__ */ jsx("option", { value: "", children: words.groupAll(groupCount(null)) }),
136
+ groups.map((g) => /* @__PURE__ */ jsx("option", { value: g.id, children: words.groupOption(g.title, groupCount(g.id)) }, g.id))
137
+ ]
138
+ }
139
+ ),
140
+ /* @__PURE__ */ jsx(
141
+ ToggleGroup,
142
+ {
143
+ value: state,
144
+ onChange: (value) => onChange({ state: value }),
145
+ "aria-label": words.stateLabel,
146
+ className: "lg:w-auto",
147
+ options: [
148
+ { value: "all", label: words.stateAll(found.length) },
149
+ { value: "todo", label: words.stateTodo(found.length - foundDone) },
150
+ { value: "done", label: words.stateDone(foundDone) }
151
+ ]
152
+ }
153
+ )
154
+ ] }),
155
+ filtering && /* @__PURE__ */ jsx("p", { role: "status", className: "-mt-2 text-xs text-muted", children: words.showing(visible.length, listed.length) }),
156
+ visible.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
157
+ /* @__PURE__ */ jsx(EmptyState, { title: words.noMatches, hint: words.noMatchesHint }),
158
+ /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => onChange({ query: "", group: null, state: "all" }), children: words.clearFilters }) })
159
+ ] }) : (
160
+ // A GRID, never columns: a section holding one card keeps a cell's width.
161
+ sections.map((section) => /* @__PURE__ */ jsxs("section", { "aria-labelledby": `${uid}-${section.id}`, className: "space-y-3", children: [
162
+ /* @__PURE__ */ jsxs("h2", { id: `${uid}-${section.id}`, className: "flex items-center gap-1.5 text-sm font-semibold text-muted", children: [
163
+ section.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 [&_svg]:size-4", children: section.icon }),
164
+ section.title
165
+ ] }),
166
+ section.help && /* @__PURE__ */ jsx("p", { className: "-mt-2 text-xs text-muted", children: section.help }),
167
+ /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3", children: section.tours.map((tour) => /* @__PURE__ */ jsx(
168
+ TourCatalogueCard,
169
+ {
170
+ tour,
171
+ completedAt: completed[tour.id],
172
+ isNew: isTourNew({ since: tour.since, joined, completed: completed[tour.id] !== void 0 }),
173
+ onLaunch,
174
+ onReset,
175
+ formatDate,
176
+ labels: labelsProp
177
+ },
178
+ tour.id
179
+ )) })
180
+ ] }, section.id))
181
+ )
182
+ ] });
183
+ }
184
+ export {
185
+ DEFAULT_TOUR_CATALOGUE_LABELS,
186
+ TourCatalogue,
187
+ TourCatalogueCard
188
+ };
189
+ //# sourceMappingURL=tour-catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tour/tour-catalogue.tsx"],"sourcesContent":["import { useId, type ReactNode } from \"react\";\nimport { Circle, CircleCheck, Clock, Play } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitFormat } from \"../lib/format\";\nimport { foldForSearch } from \"../lib/text-match\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { Chip } from \"../components/chip\";\nimport { Caption } from \"../components/text\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { SearchField } from \"../components/search-field\";\nimport { Select } from \"../components/select\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { DEFAULT_TOUR_CATALOGUE_LABELS, type TourCatalogueLabels } from \"./tour-catalogue-labels\";\nimport { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourGroup, type TourInfo } from \"./tour-info\";\nimport type { TourCatalogueFilter, TourStateFilter } from \"./use-tour-url-sync\";\n\nexport { DEFAULT_TOUR_CATALOGUE_LABELS };\nexport type { TourCatalogueLabels };\n\n/* ── The card ───────────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueCardProps {\n tour: TourInfo;\n /** The completion's ISO timestamp, `null` for one without a date; absent while untaken. */\n completedAt?: string | null;\n /** NEW — `isTourNew`. The catalogue works it out; a card on its own is told. */\n isNew?: boolean;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\". Without it, the link is not drawn. */\n onReset?: (id: string) => void;\n /** Formats the completion and update days. Default: the kit's medium date in the provider's locale. */\n formatDate?: (iso: string) => string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\n/**\n * One tour's card — keksdose's (tours-page.tsx `TourCard`, dev#557), laid out top to\n * bottom so the cards sit side by side: the state icon beside a column holding the tour's\n * icon, title, NEW (or \"Updated\", §8) and the app's `badge`, the one-liner and, from `sm`,\n * the longer account. The foot is pinned to the bottom edge, so every Start in a row of\n * cards lands on one line: the points chip, the step count and the duration, the\n * completion (or reset) line, \"Mark as not done\" and Start / Replay. Points, steps and\n * duration are each left out when the tour has none — no gap, no \"0 points\" (KS).\n */\nexport function TourCatalogueCard({\n tour,\n completedAt,\n isNew = false,\n onLaunch,\n onReset,\n formatDate: formatDateProp,\n labels: labelsProp,\n className,\n}: TourCatalogueCardProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const { formatDate } = useKitFormat();\n const day = formatDateProp ?? ((iso: string) => formatDate(iso, \"medium\"));\n const titleId = useId();\n const done = isTourDone(tour, completedAt);\n const reset = isTourReset(tour, completedAt);\n const hasFacts = tour.points != null || tour.steps != null || tour.duration != null;\n const note = done\n ? completedAt\n ? words.completedOn(day(completedAt))\n : words.completed\n : reset && tour.updated\n ? words.resetOn(day(tour.updated.date), tour.updated.reason)\n : null;\n\n return (\n <Card className={cn(\"flex h-full flex-col p-4\", className)}>\n {/* Title, one-liner and the long body share ONE left edge, in the column beside the\n state icon. */}\n <div className=\"flex flex-1 items-start gap-3\">\n <div className={cn(\"mt-0.5 shrink-0\", done ? \"text-success\" : \"text-muted\")}>\n {done ? <CircleCheck aria-hidden className=\"size-5\" /> : <Circle aria-hidden className=\"size-5\" />}\n <span className=\"sr-only\">{done ? words.completed : words.notCompleted}</span>\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {tour.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 text-brand [&_svg]:size-4\">\n {tour.icon}\n </span>\n )}\n {/* The id on the words, not the heading: the Start button is described by them. */}\n <h3 className=\"font-medium\">\n <span id={titleId}>{tour.title}</span>\n </h3>\n {/* It retires itself the moment the tour is taken — nothing to dismiss. */}\n {isNew && !reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeNew}\n </Chip>\n )}\n {reset && (\n <Chip size=\"sm\" tone=\"brand\" caps className=\"px-1.5\">\n {words.badgeUpdated}\n </Chip>\n )}\n {tour.badge}\n </div>\n {tour.description && <p className=\"mt-1 text-sm text-muted\">{tour.description}</p>}\n {/* `hidden sm:block`: one card per row is the phone, where these paragraphs\n would turn the page into a scroll; the search still reads them. */}\n {tour.about && <p className=\"mt-2 hidden text-sm leading-relaxed text-secondary sm:block\">{tour.about}</p>}\n </div>\n </div>\n <div className=\"mt-3 flex flex-wrap items-end justify-between gap-2\">\n <div className=\"min-w-0\">\n {hasFacts && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {tour.points != null && (\n <Chip size=\"sm\" tone=\"brand\">\n {words.points(tour.points)}\n </Chip>\n )}\n {tour.steps != null && <Caption as=\"span\">{words.steps(tour.steps)}</Caption>}\n {tour.duration != null && (\n <Caption as=\"span\" className=\"inline-flex items-center gap-1\">\n <Clock aria-hidden className=\"size-3\" />\n {words.duration(tour.duration)}\n </Caption>\n )}\n </div>\n )}\n {note && <p className={cn(\"text-xs text-muted\", hasFacts && \"mt-1\")}>{note}</p>}\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done && onReset && (\n <Button variant=\"link\" tone=\"muted\" size=\"sm\" aria-describedby={titleId} onClick={() => onReset(tour.id)}>\n {words.markUndone}\n </Button>\n )}\n <Button aria-describedby={titleId} onClick={() => onLaunch(tour.id)}>\n <Play aria-hidden className=\"size-4\" /> {done ? words.replay : words.start}\n </Button>\n </div>\n </div>\n </Card>\n );\n}\n\n/* ── The page body ─────────────────────────────────────────────────────────── */\n\nexport interface TourCatalogueProps extends TourCatalogueFilter {\n /** The tours this person may take, already filtered by role and data (§6.3). */\n tours: readonly TourInfo[];\n /** The sections, in the app's order. A tour whose group is not here is not listed. */\n groups: readonly TourGroup[];\n completed: TourCompletions;\n /** The account's `created_at`: a tour shipped after it, untaken, is NEW. Unknown → none is. */\n joined?: string | null;\n /** A filter changed — `{query}`, `{group}`, `{state}`, or all three for \"Clear filters\". */\n onChange: (patch: Partial<TourCatalogueFilter>) => void;\n onLaunch: (id: string) => void;\n /** \"Mark as not done\" — `useTourProgress().reset`. */\n onReset?: (id: string) => void;\n /** Above the filter bar: the app's page header with its points tile (§6.1). */\n header?: ReactNode;\n formatDate?: (iso: string) => string;\n /** The search's locale (case folding). Default: the provider's. */\n locale?: string;\n labels?: LabelOverride<TourCatalogueLabels>;\n className?: string;\n}\n\nconst STATE_FILTERS: readonly TourStateFilter[] = [\"all\", \"todo\", \"done\"];\n\n/**\n * The tours page's body (docs/tours-harmonization.md §2.1, §3) — keksdose's /tours:\n *\n * - **The filter bar**: a search, a group select and an all / to do / done toggle,\n * stacked below `lg` and side by side from it. Each count is what the OTHER filters\n * leave: the toggle counts the search and the group, each group the search and the\n * state. Under it, while a filter is on, \"Showing X of Y tours\".\n * - **The sections**: one per group, in the app's order — an h2 with its icon and help\n * line over a `sm:2 / xl:3` grid of `TourCatalogueCard`s. A group the filters empty is\n * hidden; when nothing is left, an empty state with \"Clear filters\".\n * - **Controlled**: `query`, `group`, `state` and `onChange`, so the app keeps them in its\n * URL (`useTourCatalogueUrlState`).\n *\n * A tour reset by an update (§8) is \"to do\" for the toggle and the counts.\n */\nexport function TourCatalogue({\n tours,\n groups,\n completed,\n joined,\n query,\n group: groupProp,\n state: stateProp,\n onChange,\n onLaunch,\n onReset,\n header,\n formatDate,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: TourCatalogueProps) {\n const words = useKitLabels(\"tourCatalogue\", DEFAULT_TOUR_CATALOGUE_LABELS, labelsProp);\n const uid = useId();\n const { locale: kitLocale } = useKitFormat();\n const locale = localeProp ?? kitLocale;\n // A group or state the list does not have is no filter at all (an old link).\n const group = groups.some((g) => g.id === groupProp) ? groupProp : null;\n const state = STATE_FILTERS.includes(stateProp) ? stateProp : \"all\";\n\n const listed = tours.filter((t) => groups.some((g) => g.id === t.group));\n const done = (tour: TourInfo) => isTourDone(tour, completed[tour.id]);\n const needle = foldForSearch(query.trim(), locale);\n const matchesQuery = (tour: TourInfo) =>\n !needle || [tour.title, tour.description, tour.about].some((text) => !!text && foldForSearch(text, locale).includes(needle));\n const inState = (tour: TourInfo) => (state === \"all\" ? true : state === \"done\" ? done(tour) : !done(tour));\n const inGroup = (tour: TourInfo, id: string | null = group) => id === null || tour.group === id;\n\n const found = listed.filter((t) => matchesQuery(t) && inGroup(t));\n const foundDone = found.filter(done).length;\n const visible = found.filter(inState);\n const groupCount = (id: string | null) => listed.filter((t) => matchesQuery(t) && inState(t) && inGroup(t, id)).length;\n const filtering = needle.length > 0 || state !== \"all\" || group !== null;\n const sections = groups\n .map((g) => ({ ...g, tours: visible.filter((t) => t.group === g.id) }))\n .filter((g) => g.tours.length > 0);\n\n return (\n <div className={cn(\"space-y-4\", className)}>\n {header}\n <div className=\"flex flex-col gap-2 lg:flex-row lg:items-center\">\n <SearchField\n className=\"lg:max-w-xs lg:flex-1\"\n value={query}\n onChange={(value) => onChange({ query: value })}\n aria-label={words.searchLabel}\n placeholder={words.searchPlaceholder}\n />\n {/* A select, not a chip per group: on a phone those wrap into rows above the list\n they filter, and an app may have ten groups (ka F8). Named, not labelled: a\n floating label makes it taller than the search box beside it (dev #589). */}\n <Select\n className=\"lg:w-60\"\n aria-label={words.groupLabel}\n value={group ?? \"\"}\n onChange={(event) => onChange({ group: event.target.value || null })}\n >\n <option value=\"\">{words.groupAll(groupCount(null))}</option>\n {groups.map((g) => (\n <option key={g.id} value={g.id}>\n {words.groupOption(g.title, groupCount(g.id))}\n </option>\n ))}\n </Select>\n <ToggleGroup\n value={state}\n onChange={(value: TourStateFilter) => onChange({ state: value })}\n aria-label={words.stateLabel}\n className=\"lg:w-auto\"\n options={[\n { value: \"all\", label: words.stateAll(found.length) },\n { value: \"todo\", label: words.stateTodo(found.length - foundDone) },\n { value: \"done\", label: words.stateDone(foundDone) },\n ]}\n />\n </div>\n {filtering && (\n <p role=\"status\" className=\"-mt-2 text-xs text-muted\">\n {words.showing(visible.length, listed.length)}\n </p>\n )}\n\n {visible.length === 0 ? (\n <div className=\"space-y-3\">\n <EmptyState title={words.noMatches} hint={words.noMatchesHint} />\n <div className=\"flex justify-center\">\n <Button variant=\"secondary\" onClick={() => onChange({ query: \"\", group: null, state: \"all\" })}>\n {words.clearFilters}\n </Button>\n </div>\n </div>\n ) : (\n // A GRID, never columns: a section holding one card keeps a cell's width.\n sections.map((section) => (\n <section key={section.id} aria-labelledby={`${uid}-${section.id}`} className=\"space-y-3\">\n <h2 id={`${uid}-${section.id}`} className=\"flex items-center gap-1.5 text-sm font-semibold text-muted\">\n {section.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 [&_svg]:size-4\">\n {section.icon}\n </span>\n )}\n {section.title}\n </h2>\n {section.help && <p className=\"-mt-2 text-xs text-muted\">{section.help}</p>}\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {section.tours.map((tour) => (\n <TourCatalogueCard\n key={tour.id}\n tour={tour}\n completedAt={completed[tour.id]}\n isNew={isTourNew({ since: tour.since, joined, completed: completed[tour.id] !== undefined })}\n onLaunch={onLaunch}\n onReset={onReset}\n formatDate={formatDate}\n labels={labelsProp}\n />\n ))}\n </div>\n </section>\n ))\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}