@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
@@ -613,6 +613,34 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
613
613
  awaitClickHint: "Faccia clic sull’elemento evidenziato per continuare",
614
614
  step: (current, total) => `${n(current)} / ${n(total)}`,
615
615
  },
616
+ // 0.43.0 — docs/tours-harmonization.md §3, §8: the tours page. keksdose's reviewed words.
617
+ tourCatalogue: {
618
+ searchLabel: "Cerca tour",
619
+ searchPlaceholder: "Cerca tour…",
620
+ groupLabel: "Argomento",
621
+ groupAll: (count) => `Tutti gli argomenti (${n(count)})`,
622
+ groupOption: (group, count) => `${group} (${n(count)})`,
623
+ stateLabel: "Filtra i tour",
624
+ stateAll: (count) => `Tutti (${n(count)})`,
625
+ stateTodo: (count) => `Da fare (${n(count)})`,
626
+ stateDone: (count) => `Completati (${n(count)})`,
627
+ showing: (shown, total) => `${n(shown)} di ${n(total)} tour`,
628
+ noMatches: "Nessun tour corrisponde",
629
+ noMatchesHint: "Provi un’altra parola o cancelli i filtri.",
630
+ clearFilters: "Cancella filtri",
631
+ badgeNew: "Nuovo",
632
+ badgeUpdated: "Aggiornato",
633
+ resetOn: (date, reason) => (reason ? `Reimpostato il ${date}: ${reason}` : `Reimpostato il ${date}`),
634
+ points: (count) => `+${n(count)} pt`,
635
+ steps: (count) => (count === 1 ? `${n(count)} passaggio` : `${n(count)} passaggi`),
636
+ duration: (minutes) => `Circa ${n(minutes)} min`,
637
+ completedOn: (date) => `Completato il ${date}`,
638
+ completed: "Completato",
639
+ notCompleted: "Non completato",
640
+ markUndone: "Segna come non completato",
641
+ start: "Avvia",
642
+ replay: "Ripeti",
643
+ },
616
644
  commandPalette: {
617
645
  clear: "Cancella ricerca",
618
646
  submit: "Cerca",
@@ -1744,6 +1772,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1744
1772
  `Il Suo abbonamento termina il ${date}. Fino ad allora tutto funziona come prima.`,
1745
1773
  cancelConfirmAction: "Disdici ora",
1746
1774
  keepSubscription: "Mantieni l’abbonamento",
1775
+ portalOpensNewTab: (provider, app) =>
1776
+ `Pagamenti e fatture si aprono su ${provider} in una nuova scheda; ${app ? `${app} resta aperto qui` : "questa pagina resta aperta"}.`,
1747
1777
  grantWarningTitle: "Il Suo accesso gratuito continua",
1748
1778
  grantWarningBody:
1749
1779
  "Il Suo accesso gratuito non ha una data di fine. Finché dura, un acquisto non cambia il Suo piano: pagherebbe per un piano che non si applica.",
@@ -1831,7 +1861,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1831
1861
  sections: {
1832
1862
  overview: { title: "Panoramica", help: "Ciò che è in attesa, a colpo d’occhio." },
1833
1863
  users: { title: "Utenti", help: "Chi ha un account e cosa può fare ciascuno." },
1834
- invitations: { title: "Inviti", help: "I link con cui entra una persona nuova." },
1864
+ invitations: { title: "Inviti", help: "Inviti persone via e-mail nella loro lingua; reinvii o revochi gli inviti ancora aperti." },
1835
1865
  billing: { title: "Fatturazione", help: "Il piano, il pagamento e le fatture." },
1836
1866
  activity: { title: "Attività", help: "Ciò che hanno fatto gli amministratori, dal più recente." },
1837
1867
  },
@@ -1842,6 +1872,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1842
1872
  users: "Utenti",
1843
1873
  usersNew: (count) => `${n(count)} ${plural(count, "nuovo", "nuovi")} in 7 giorni`,
1844
1874
  },
1875
+ overviewError: "Non è stato possibile caricare la panoramica.",
1876
+ retry: "Riprova",
1845
1877
  tools: "Strumenti",
1846
1878
  },
1847
1879
  // 0.42.0 — docs/top-bar-harmonization.md §3.1: the top bar's shared parts, keksdose's
@@ -1855,6 +1887,9 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
1855
1887
  allTours: "Tutti i tour…",
1856
1888
  badgeNew: "Nuovo",
1857
1889
  completed: "Completato",
1890
+ badgeUpdated: "Aggiornato",
1891
+ updatedDot: "Tour aggiornati",
1892
+ newAndUpdated: "Nuovi e aggiornati",
1858
1893
  },
1859
1894
  sync: {
1860
1895
  blocked: (count) =>
@@ -592,6 +592,34 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
592
592
  awaitClickHint: "点击高亮的元素以继续",
593
593
  step: (current, total) => `${n(current)} / ${n(total)}`,
594
594
  },
595
+ // 0.43.0 — docs/tours-harmonization.md §3, §8: the tours page.
596
+ tourCatalogue: {
597
+ searchLabel: "搜索导览",
598
+ searchPlaceholder: "搜索导览…",
599
+ groupLabel: "主题",
600
+ groupAll: (count) => `全部主题(${n(count)})`,
601
+ groupOption: (group, count) => `${group}(${n(count)})`,
602
+ stateLabel: "筛选导览",
603
+ stateAll: (count) => `全部(${n(count)})`,
604
+ stateTodo: (count) => `待完成(${n(count)})`,
605
+ stateDone: (count) => `已完成(${n(count)})`,
606
+ showing: (shown, total) => `显示 ${n(shown)} / ${n(total)} 个导览`,
607
+ noMatches: "没有匹配的导览",
608
+ noMatchesHint: "请换个关键词,或清除筛选条件。",
609
+ clearFilters: "清除筛选",
610
+ badgeNew: "新",
611
+ badgeUpdated: "已更新",
612
+ resetOn: (date, reason) => (reason ? `已于 ${date} 重置:${reason}` : `已于 ${date} 重置`),
613
+ points: (count) => `+${n(count)} 分`,
614
+ steps: (count) => `${n(count)} 步`,
615
+ duration: (minutes) => `约 ${n(minutes)} 分钟`,
616
+ completedOn: (date) => `完成于 ${date}`,
617
+ completed: "已完成",
618
+ notCompleted: "未完成",
619
+ markUndone: "标记为未完成",
620
+ start: "开始",
621
+ replay: "再来一次",
622
+ },
595
623
  commandPalette: {
596
624
  clear: "清除搜索",
597
625
  submit: "搜索",
@@ -1655,6 +1683,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1655
1683
  cancelConfirmBody: (date) => `您的订阅将于 ${date} 结束。在此之前,一切照常使用。`,
1656
1684
  cancelConfirmAction: "立即取消订阅",
1657
1685
  keepSubscription: "保留订阅",
1686
+ portalOpensNewTab: (provider, app) =>
1687
+ `付款和发票将在新标签页中于 ${provider} 打开;${app ? `${app} 仍在此保持打开` : "本页面仍保持打开"}。`,
1658
1688
  grantWarningTitle: "您的免费使用权仍然有效",
1659
1689
  grantWarningBody: "您的免费使用权没有结束日期。在此期间,购买不会改变您的套餐:您将为一个不生效的套餐付费。",
1660
1690
  grantWarningAction: "仍然购买",
@@ -1734,7 +1764,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1734
1764
  sections: {
1735
1765
  overview: { title: "概览", help: "一眼看清待处理的事项。" },
1736
1766
  users: { title: "用户", help: "谁拥有账户,以及每个人可以做什么。" },
1737
- invitations: { title: "邀请", help: "让新成员加入的链接。" },
1767
+ invitations: { title: "邀请", help: "通过电子邮件以对方的语言邀请他人;可重新发送或撤销尚未处理的邀请。" },
1738
1768
  billing: { title: "账单", help: "套餐、付款和发票。" },
1739
1769
  activity: { title: "活动", help: "管理员的操作记录,最新的在前。" },
1740
1770
  },
@@ -1745,6 +1775,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1745
1775
  users: "用户",
1746
1776
  usersNew: (count) => `近 7 天新增 ${n(count)} 人`,
1747
1777
  },
1778
+ overviewError: "无法加载概览。",
1779
+ retry: "重试",
1748
1780
  tools: "工具",
1749
1781
  },
1750
1782
  // 0.42.0 — docs/top-bar-harmonization.md §3.1: the top bar's shared parts; "套餐" as
@@ -1758,6 +1790,9 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
1758
1790
  allTours: "全部导览…",
1759
1791
  badgeNew: "新",
1760
1792
  completed: "已完成",
1793
+ badgeUpdated: "已更新",
1794
+ updatedDot: "有更新的导览",
1795
+ newAndUpdated: "新增和更新",
1761
1796
  },
1762
1797
  sync: {
1763
1798
  blocked: (count) => `${n(count)} 项修改无法从此设备发送`,
@@ -397,6 +397,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
397
397
  "billing.buyerHolds": [["{{buyer}}"], [undefined]],
398
398
  "billing.paymentFailedAskBuyer": [["{{buyer}}"], [undefined]],
399
399
  "billing.cancelConfirmBody": [["{{date}}"]],
400
+ "billing.portalOpensNewTab": [["{{provider}}", "{{app}}"], ["{{provider}}", undefined]],
400
401
  "planChange.current": [["{{plan}}"]],
401
402
  "planChange.changed": [["{{from}}", "{{to}}"]],
402
403
  "planChange.set": [["{{to}}"]],
@@ -417,4 +418,17 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
417
418
  "topBarParts.sync.waitingShort": one,
418
419
  "topBarParts.dev.plan": [["{{plan}}"]],
419
420
  "topBarParts.dev.backTo": [["{{role}}"]],
421
+ // 0.43.0 (docs/tours-harmonization.md §3, §8): the tours page's counts, its group
422
+ // option, the completion day and a reset's day and reason.
423
+ "tourCatalogue.groupAll": one,
424
+ "tourCatalogue.groupOption": [["{{group}}", 1], ["{{group}}", 3]],
425
+ "tourCatalogue.stateAll": one,
426
+ "tourCatalogue.stateTodo": one,
427
+ "tourCatalogue.stateDone": one,
428
+ "tourCatalogue.showing": [[3, 16]],
429
+ "tourCatalogue.resetOn": [["{{date}}", "{{reason}}"]],
430
+ "tourCatalogue.points": one,
431
+ "tourCatalogue.steps": one,
432
+ "tourCatalogue.duration": one,
433
+ "tourCatalogue.completedOn": [["{{date}}"]],
420
434
  };
package/src/index.ts CHANGED
@@ -796,6 +796,9 @@ export { DEFAULT_ADMIN_AREA_LABELS, useAdminAreaLabels } from "./admin/admin-are
796
796
  export type { AdminAreaLabels, AdminTileLabels, AdminSharedSection } from "./admin/admin-area-labels";
797
797
  export { ADMIN_SECTIONS, adminSections } from "./admin/admin-sections";
798
798
  export type { AdminSectionDefinition, AdminOwnSection, AdminSectionsOptions } from "./admin/admin-sections";
799
+ // 0.43.0 (keksdose): the section order as ids, and the narrowed `leaveOut`.
800
+ export { adminSectionOrder } from "./admin/admin-sections";
801
+ export type { AdminSectionPlace, AdminSectionOrderOptions, AdminKeptSection } from "./admin/admin-sections";
799
802
  export { AdminForbidden } from "./admin/admin-forbidden";
800
803
  export type { AdminForbiddenProps } from "./admin/admin-forbidden";
801
804
  export { AdminOverview, AttentionTile, adminWaitingCount, DEFAULT_ADMIN_ATTENTION_HREFS } from "./admin/admin-overview";
@@ -883,6 +886,11 @@ export { isPermanentGrant } from "./billing/billing-overview";
883
886
  export type { BillingPortalAccess } from "./billing/billing-overview";
884
887
  export { CancelSubscriptionConfirm } from "./billing/cancel-subscription";
885
888
  export type { CancelSubscriptionConfirmProps } from "./billing/cancel-subscription";
889
+ // 0.43 (§17): the provider's portal in a new tab; the in-app cancel's follow-up.
890
+ export { openProviderPage } from "./billing/provider-page";
891
+ export type { OpenProviderPageOptions, ProviderPageWay } from "./billing/provider-page";
892
+ export { useCancelRequest } from "./billing/cancel-request";
893
+ export type { CancelRequest, CancelRequested, CancelRequestView } from "./billing/cancel-request";
886
894
  // The operator's plan parts (§14.12) are admin/plan-change, in the admin section.
887
895
 
888
896
  // ── settings ─────────────────────────────────────────────────────────────────
@@ -188,6 +188,9 @@ export interface GlobalSearchProps {
188
188
  labels?: LabelOverride<GlobalSearchLabels>;
189
189
  /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */
190
190
  paletteLabels?: Partial<CommandPaletteLabels>;
191
+ /** 0.43: `data-*` attributes go on the kit's trigger button (`trigger="button"`) — a
192
+ * tour's anchor (`data-tour="search"`), a test hook. A function trigger is the app's. */
193
+ [key: `data-${string}`]: string | number | boolean | undefined;
191
194
  }
192
195
 
193
196
  function isApplePlatform(): boolean {
@@ -290,6 +293,7 @@ function GlobalSearchImpl({
290
293
  density,
291
294
  labels,
292
295
  paletteLabels,
296
+ ...attributes
293
297
  }: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {
294
298
  const l = useKitLabels("globalSearch", DEFAULT_GLOBAL_SEARCH_LABELS, labels);
295
299
  const pl = useKitLabels("commandPalette", DEFAULT_COMMAND_PALETTE_LABELS, paletteLabels);
@@ -563,6 +567,7 @@ function GlobalSearchImpl({
563
567
  className={hideTriggerOnPhone ? "hidden sm:block" : undefined}
564
568
  >
565
569
  <button
570
+ {...Object.fromEntries(Object.entries(attributes).filter(([k]) => k.startsWith("data-")))}
566
571
  type="button"
567
572
  onClick={openSearch}
568
573
  aria-label={triggerLabel}
@@ -29,6 +29,13 @@ export interface SettingsGroup<G extends string = string> {
29
29
  * kastlan's `/admin/billing` while billing is off. Default: visible.
30
30
  */
31
31
  visible?: boolean;
32
+ /**
33
+ * 0.43: the group shows with no visible entry — a page of its own rather than of setting
34
+ * cards (the admin area's Overview, whose tiles are no settings; `adminSections` sets
35
+ * it). `visible: false` still hides it. Default: a group shows only with an entry once
36
+ * `entries` are given (§4.1).
37
+ */
38
+ standalone?: boolean;
32
39
  }
33
40
 
34
41
  /** One setting, and the card that holds it (§4.1). */
@@ -57,7 +64,8 @@ export interface SettingsEntry<G extends string = string> {
57
64
  /**
58
65
  * The groups that show, in their order: not `visible: false`, and — when `entries` is
59
66
  * given — holding at least one visible entry (§4.1: Kurvenschmiede's notifications group
60
- * stays hidden until it has a card). Without `entries` only the groups' own flag counts.
67
+ * stays hidden until it has a card) or `standalone` (0.43). Without `entries` only the
68
+ * groups' own flag counts.
61
69
  */
62
70
  export function visibleSettingsGroups<G extends string>(
63
71
  groups: readonly SettingsGroup<G>[],
@@ -66,7 +74,7 @@ export function visibleSettingsGroups<G extends string>(
66
74
  if (!entries) return groups.filter((g) => g.visible !== false);
67
75
  const filled = new Set<G>();
68
76
  for (const e of entries) if (e.visible !== false) filled.add(e.group);
69
- return groups.filter((g) => g.visible !== false && filled.has(g.id));
77
+ return groups.filter((g) => g.visible !== false && (g.standalone === true || filled.has(g.id)));
70
78
  }
71
79
 
72
80
  /** The visible entries of the visible groups — what search may find. */
@@ -2,8 +2,8 @@ import { Link } from "react-router";
2
2
  import { ShieldCheck } from "lucide-react";
3
3
  import { Chip } from "../components/chip";
4
4
  import { MenuItem } from "../components/menu-item";
5
- import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
6
- import { DEFAULT_ADMIN_AREA_LABELS } from "../admin/admin-area-labels";
5
+ import type { LabelOverride } from "../i18n/kit-labels";
6
+ import { DEFAULT_ADMIN_AREA_LABELS, useAdminAreaLabels } from "../admin/admin-area-labels";
7
7
  import type { AdminAreaLabels } from "../admin/admin-area-labels";
8
8
  import type { TopBarMenuEntry } from "./topbar-action-menu";
9
9
 
@@ -45,8 +45,12 @@ export interface AdminMenuEntryOptions {
45
45
  * `adminWaitingCount(attention, […])`. `0`, `null` or none shows no badge.
46
46
  */
47
47
  badge?: number | null;
48
- /** The resolved `adminArea` words (`useAdminAreaLabels()`); default English. Builds no
49
- * hook, so the entry can be made in the list of entries. */
48
+ /**
49
+ * The resolved `adminArea` words — pass `useAdminAreaLabels()`, or the row is ENGLISH
50
+ * in every language: a plain function reads no provider (kastlan, 0.42 adoption). Builds
51
+ * no hook, so the entry can be made in the list of entries. {@link useAdminMenuEntry}
52
+ * (0.43) is the same entry with the provider's words read for you.
53
+ */
50
54
  labels?: AdminAreaLabels;
51
55
  /** The entry's `key`. Default `"administration"`. */
52
56
  key?: string;
@@ -59,7 +63,12 @@ export interface AdminMenuEntryOptions {
59
63
  * the kit's word and the waiting count as its badge. Put it in the entries for an admin
60
64
  * only — the menu closes on the way, as any link row:
61
65
  *
66
+ * const labels = useAdminAreaLabels();
62
67
  * entries={[…, ...(isAdmin ? [adminMenuEntry({ badge: waiting, labels })] : []), …]}
68
+ *
69
+ * A function, not a hook: it reads NO provider, so pass `labels` (`useAdminAreaLabels()`)
70
+ * or the row says "Administration" in every language — or call {@link useAdminMenuEntry}
71
+ * at the component's top.
63
72
  */
64
73
  export function adminMenuEntry({
65
74
  href = "/admin",
@@ -79,6 +88,25 @@ export function adminMenuEntry({
79
88
  };
80
89
  }
81
90
 
91
+ export interface UseAdminMenuEntryOptions extends Omit<AdminMenuEntryOptions, "labels"> {
92
+ /** Over the provider's `adminArea` words: prop > `<UiKitProvider labels={{ adminArea }}>`
93
+ * > English. */
94
+ labels?: LabelOverride<AdminAreaLabels>;
95
+ }
96
+
97
+ /**
98
+ * {@link adminMenuEntry} with the provider's words (0.43, kastlan): the hook reads
99
+ * `useAdminAreaLabels()` itself, so the row is in the app's language without a `labels`
100
+ * to remember. Call it at the component's top, then put it in the entries:
101
+ *
102
+ * const admin = useAdminMenuEntry({ badge: waiting });
103
+ * entries={[…, ...(isAdmin ? [admin] : []), …]}
104
+ */
105
+ export function useAdminMenuEntry({ labels: labelsProp, ...options }: UseAdminMenuEntryOptions = {}): TopBarMenuEntry {
106
+ const labels = useAdminAreaLabels(labelsProp);
107
+ return adminMenuEntry({ ...options, labels });
108
+ }
109
+
82
110
  export interface AdminMenuRowProps {
83
111
  /** The admin area. Default `/admin`. */
84
112
  href?: string;
@@ -99,7 +127,8 @@ export interface AdminMenuRowProps {
99
127
  * {(close) => <ul>… {isAdmin && <AdminMenuRow badge={waiting} onNavigate={close} />} …</ul>}
100
128
  */
101
129
  export function AdminMenuRow({ href = "/admin", badge, onNavigate, current, labels: labelsProp }: AdminMenuRowProps) {
102
- const labels = useKitLabels("adminArea", DEFAULT_ADMIN_AREA_LABELS, labelsProp);
130
+ // The provider's words, as every kit part (0.43: through the namespace's own hook).
131
+ const labels = useAdminAreaLabels(labelsProp);
103
132
  return (
104
133
  <li>
105
134
  <MenuItem
@@ -97,6 +97,14 @@ export function DevSwitchers<R extends string, P extends string>({
97
97
  const bar = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS);
98
98
  const words = useTopBarPartsLabels(labelsProp ? { dev: labelsProp } : undefined).dev;
99
99
  const [kept, setKept] = useState<string | null>(() => (enabled ? readKey(storageKey) : null));
100
+ // 0.43 (keksdose): `enabled` may start false and turn true — an app's GET probe of the
101
+ // dev route — so the kept role is read again then, or "Back to {role}" is lost after a
102
+ // reload. Adjusted during render, React's pattern for state that follows a prop.
103
+ const [wasEnabled, setWasEnabled] = useState(enabled);
104
+ if (wasEnabled !== enabled) {
105
+ setWasEnabled(enabled);
106
+ if (enabled) setKept(readKey(storageKey));
107
+ }
100
108
  if (!enabled) return null;
101
109
 
102
110
  const original = (originalRole ?? kept ?? undefined) as R | undefined;
@@ -24,6 +24,12 @@ export interface TourMenuLabels {
24
24
  badgeNew: string;
25
25
  /** What the ✓ on a taken tour's row says to a reader. */
26
26
  completed: string;
27
+ /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */
28
+ badgeUpdated: string;
29
+ /** The dot on the compass while only updated tours light it, for a reader. */
30
+ updatedDot: string;
31
+ /** The heading over the untaken new tours when updated ones are among them. */
32
+ newAndUpdated: string;
27
33
  }
28
34
 
29
35
  /** `SyncStatusIndicator`'s words (§2.6). The "waiting for a plan" state is
@@ -94,6 +100,9 @@ export const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {
94
100
  allTours: "All tours…",
95
101
  badgeNew: "New",
96
102
  completed: "Completed",
103
+ badgeUpdated: "Updated",
104
+ updatedDot: "Updated tours",
105
+ newAndUpdated: "New and updated",
97
106
  },
98
107
  sync: {
99
108
  blocked: (count) =>
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
2
2
  import { useLocation } from "react-router";
3
3
  import { Check, Compass } from "lucide-react";
4
4
  import { Chip } from "../components/chip";
5
+ import { isTourDone, isTourNew, isTourReset, type TourCompletions, type TourInfo } from "../tour/tour-info";
5
6
  import type { LabelOverride } from "../i18n/kit-labels";
6
7
  import { TopBarActionMenu, type TopBarMenuEntry } from "./topbar-action-menu";
7
8
  import { useTopBarPartsLabels, type TourMenuLabels } from "./top-bar-parts-labels";
@@ -11,28 +12,36 @@ import { useTopBarPartsLabels, type TourMenuLabels } from "./top-bar-parts-label
11
12
  * The kit's tour engine (`TourProvider`) runs steps and keeps none; which tours exist,
12
13
  * which are new and which are taken is the app's (keksdose: `useTours`, `useIsNewTour`,
13
14
  * its progress store).
15
+ *
16
+ * Since 0.43 it is a subset of the catalogue's `TourInfo` (docs/tours-harmonization.md
17
+ * §3), so an app keeps ONE list: hand the menu the catalogue's tours with `completed`
18
+ * and `joined`, and it works out ✓, NEW and "Updated" itself. Given the record, the kit's
19
+ * ✓ and "Updated" win over a row's `completed` and `reset`; given `joined`, its NEW wins
20
+ * over `isNew` — so a row built from an app's own booleans cannot hide an update (§8,
21
+ * build review kastlan A3, keksdose K-S6). The booleans are for a menu with neither.
14
22
  */
15
- export interface TourMenuTour {
23
+ export type TourMenuTour = Partial<Pick<TourInfo, "route" | "since" | "updated">> & {
16
24
  id: string;
17
25
  title: ReactNode;
18
- /** The page the tour starts on. Its first segment decides "On this page":
19
- * "/reports/spending" belongs to "/reports". */
20
- route?: string;
26
+ /** The row's icon, drawn at 16px. */
27
+ icon?: ReactNode;
21
28
  /** Shipped after this account was opened and not yet taken: a NEW chip on the row,
22
29
  * and the dot on the compass. */
23
30
  isNew?: boolean;
24
31
  /** Taken: a ✓ on the row. */
25
32
  completed?: boolean;
26
- /** The row's icon, drawn at 16px. */
27
- icon?: ReactNode;
28
- /** When the tour shipped, as an ISO date: the newest of the new come first. */
29
- since?: string;
30
- }
33
+ /** Taken before its last update (§8): an "Updated" chip, and the dot, as for NEW. */
34
+ reset?: boolean;
35
+ };
31
36
 
32
37
  export interface TourMenuProps {
33
38
  tours: readonly TourMenuTour[];
34
39
  /** Start a tour — `useTour().start(steps, { id })` with the app's steps. */
35
40
  onLaunch: (id: string) => void;
41
+ /** The device's completions (`useTourProgress().completed`): ✓ and "Updated" from them. */
42
+ completed?: TourCompletions;
43
+ /** The account's `created_at`: NEW from it, by `isTourNew`. */
44
+ joined?: string | null;
36
45
  /**
37
46
  * The app's tours page (keksdose `/tours`). With it the compass is a LINK there plus a
38
47
  * hover menu, and the menu ends in "All tours…". Without it (§6.7, Kurvenschmiede) the
@@ -59,7 +68,9 @@ export interface TourMenuProps {
59
68
  /** A path's first segment — "/reports/spending?g=month" → "/reports". */
60
69
  const pageOf = (path: string) => `/${path.split(/[?#]/)[0].split("/")[1] ?? ""}`;
61
70
 
62
- const isFresh = (t: TourMenuTour) => !!t.isNew && !t.completed;
71
+ const isFresh = (t: TourMenuTour) => (!!t.isNew && !t.completed) || !!t.reset;
72
+ /** When it last became news: its update, else its ship date. */
73
+ const newsOf = (t: TourMenuTour) => (t.reset ? t.updated?.date : undefined) ?? t.since ?? "";
63
74
 
64
75
  /**
65
76
  * The top bar's guided-tours control (docs/top-bar-harmonization.md §2.4, §6.7), lifted
@@ -68,8 +79,10 @@ const isFresh = (t: TourMenuTour) => !!t.isNew && !t.completed;
68
79
  * and "All tours…". A taken tour's row carries a ✓, a new one a NEW chip.
69
80
  */
70
81
  export function TourMenu({
71
- tours,
82
+ tours: toursProp,
72
83
  onLaunch,
84
+ completed: completions,
85
+ joined,
73
86
  href,
74
87
  pathname: pathnameProp,
75
88
  maxNew = 3,
@@ -80,13 +93,27 @@ export function TourMenu({
80
93
  const words = useTopBarPartsLabels(labelsProp ? { tours: labelsProp } : undefined).tours;
81
94
  const location = useLocation();
82
95
  const here = pageOf(pathnameProp ?? location.pathname);
96
+ const tours =
97
+ completions === undefined && joined === undefined
98
+ ? toursProp
99
+ : toursProp.map((t) => {
100
+ const at = completions?.[t.id];
101
+ // Ever completed: the record when there is one, else the row's own word.
102
+ const ever = completions ? at !== undefined : !!t.completed;
103
+ return {
104
+ ...t,
105
+ completed: completions ? isTourDone(t, at) : t.completed,
106
+ reset: completions ? isTourReset(t, at) : t.reset,
107
+ isNew: joined !== undefined ? isTourNew({ since: t.since, joined, completed: ever }) : t.isNew,
108
+ };
109
+ });
83
110
 
84
111
  const forThisPage = tours.filter((t) => t.route !== undefined && pageOf(t.route) === here);
85
112
  const fresh = tours.filter(isFresh);
86
113
  // ISO dates sort as text; a stable sort keeps one day's tours in the app's order.
87
114
  const newRows = fresh
88
115
  .filter((t) => !forThisPage.includes(t))
89
- .sort((a, b) => (b.since ?? "").localeCompare(a.since ?? ""))
116
+ .sort((a, b) => newsOf(b).localeCompare(newsOf(a)))
90
117
  .slice(0, maxNew);
91
118
  const rest = href && list !== "all" ? [] : tours.filter((t) => !forThisPage.includes(t) && !newRows.includes(t));
92
119
 
@@ -96,7 +123,11 @@ export function TourMenu({
96
123
  key: tour.id,
97
124
  icon: tour.icon,
98
125
  label: tour.title,
99
- trailing: tour.completed ? (
126
+ trailing: tour.reset ? (
127
+ <Chip size="sm" tone="brand" caps className="px-1.5">
128
+ {words.badgeUpdated}
129
+ </Chip>
130
+ ) : tour.completed ? (
100
131
  <>
101
132
  <Check aria-hidden className="size-4 text-success" />
102
133
  <span className="sr-only">{words.completed}</span>
@@ -112,7 +143,7 @@ export function TourMenu({
112
143
 
113
144
  const blocks = [
114
145
  { key: "this-page", label: words.thisPage, tours: forThisPage },
115
- { key: "new", label: words.newTours, tours: newRows },
146
+ { key: "new", label: newRows.some((t) => t.reset) ? words.newAndUpdated : words.newTours, tours: newRows },
116
147
  ].filter((b) => b.tours.length > 0);
117
148
  const tail: TopBarMenuEntry[] = [
118
149
  ...rest.map(entryFor),
@@ -133,7 +164,11 @@ export function TourMenu({
133
164
  return (
134
165
  <TopBarActionMenu
135
166
  icon={<Compass className="size-5" />}
136
- iconBadge={fresh.length > 0 ? { label: words.newDot, tone: "brand" } : null}
167
+ iconBadge={
168
+ fresh.length > 0
169
+ ? { label: fresh.some((t) => !t.reset) ? words.newDot : words.updatedDot, tone: "brand" }
170
+ : null
171
+ }
137
172
  aria-label={words.menu}
138
173
  href={href}
139
174
  panelClassName="w-72 max-h-[70vh] overflow-y-auto"
package/src/shell.ts CHANGED
@@ -17,6 +17,9 @@ export type { FillHeightProps } from "./shell/fill-height";
17
17
  // (docs/admin-harmonization.md §2.1, §6.6).
18
18
  export { adminMenuEntry, AdminMenuRow } from "./shell/admin-menu";
19
19
  export type { AdminMenuEntryOptions, AdminMenuRowProps } from "./shell/admin-menu";
20
+ // 0.43.0 (kastlan): the entry with the provider's words.
21
+ export { useAdminMenuEntry } from "./shell/admin-menu";
22
+ export type { UseAdminMenuEntryOptions } from "./shell/admin-menu";
20
23
  // 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).
21
24
  export { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout";
22
25
  export type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from "./settings/settings-layout";
@@ -0,0 +1,75 @@
1
+ // The English words of `TourCatalogue` and `TourCatalogueCard` (docs/tours-harmonization.md
2
+ // §3), in a module with no component code, like every `*-labels.ts` (0.34, KS U8).
3
+ // keksdose's reviewed words (tour.* in its en.json) where it had them.
4
+
5
+ /** The `tourCatalogue` namespace. Counts arrive as numbers, dates already formatted. */
6
+ export interface TourCatalogueLabels {
7
+ /** The search box's name. */
8
+ searchLabel: string;
9
+ searchPlaceholder: string;
10
+ /** The group select's name (keksdose: "Topic"). */
11
+ groupLabel: string;
12
+ /** The select's first option, with the count the other filters leave. */
13
+ groupAll: (count: number) => string;
14
+ /** A group's option: its title and its count. */
15
+ groupOption: (group: string, count: number) => string;
16
+ /** The all / to do / done toggle's name. */
17
+ stateLabel: string;
18
+ stateAll: (count: number) => string;
19
+ stateTodo: (count: number) => string;
20
+ stateDone: (count: number) => string;
21
+ /** Under the filter bar while a filter is on. */
22
+ showing: (shown: number, total: number) => string;
23
+ noMatches: string;
24
+ noMatchesHint: string;
25
+ clearFilters: string;
26
+ /** The NEW pill. */
27
+ badgeNew: string;
28
+ /** The pill on a tour reset by an update (§8). */
29
+ badgeUpdated: string;
30
+ /** A reset tour's line: the update's day (formatted) and its reason (§8); `reason` is
31
+ * left out when the tour's update carries none. */
32
+ resetOn: (date: string, reason?: string) => string;
33
+ /** The points chip. */
34
+ points: (count: number) => string;
35
+ steps: (count: number) => string;
36
+ /** The duration, in minutes. */
37
+ duration: (minutes: number) => string;
38
+ /** Completed, with the day (formatted). */
39
+ completedOn: (date: string) => string;
40
+ /** Completed without a date (kastlan's old id list, §6.2). */
41
+ completed: string;
42
+ /** Not taken yet — the state icon, for a reader. */
43
+ notCompleted: string;
44
+ markUndone: string;
45
+ start: string;
46
+ replay: string;
47
+ }
48
+
49
+ export const DEFAULT_TOUR_CATALOGUE_LABELS: TourCatalogueLabels = {
50
+ searchLabel: "Search tours",
51
+ searchPlaceholder: "Search tours…",
52
+ groupLabel: "Topic",
53
+ groupAll: (count) => `All topics (${count})`,
54
+ groupOption: (group, count) => `${group} (${count})`,
55
+ stateLabel: "Filter tours",
56
+ stateAll: (count) => `All (${count})`,
57
+ stateTodo: (count) => `To do (${count})`,
58
+ stateDone: (count) => `Completed (${count})`,
59
+ showing: (shown, total) => `Showing ${shown} of ${total} tours`,
60
+ noMatches: "No tour matches",
61
+ noMatchesHint: "Try another word, or clear the filters.",
62
+ clearFilters: "Clear filters",
63
+ badgeNew: "New",
64
+ badgeUpdated: "Updated",
65
+ resetOn: (date, reason) => (reason ? `Reset on ${date}: ${reason}` : `Reset on ${date}`),
66
+ points: (count) => `+${count} pts`,
67
+ steps: (count) => (count === 1 ? "1 step" : `${count} steps`),
68
+ duration: (minutes) => `About ${minutes} min`,
69
+ completedOn: (date) => `Completed on ${date}`,
70
+ completed: "Completed",
71
+ notCompleted: "Not completed",
72
+ markUndone: "Mark as not done",
73
+ start: "Start",
74
+ replay: "Replay",
75
+ };