@eifi1/ui-kit 0.16.1 → 0.18.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 (186) hide show
  1. package/README.md +34 -9
  2. package/dist/chart.d.ts +10 -1
  3. package/dist/components/account-chips.d.ts +153 -0
  4. package/dist/components/account-chips.js +135 -0
  5. package/dist/components/account-chips.js.map +1 -0
  6. package/dist/components/account-settings-labels.d.ts +10 -2
  7. package/dist/components/account-settings-labels.js +3 -0
  8. package/dist/components/account-settings-labels.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +18 -1
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/button-group.d.ts +10 -1
  12. package/dist/components/calculator.d.ts +10 -1
  13. package/dist/components/chip.d.ts +25 -1
  14. package/dist/components/chip.js +143 -6
  15. package/dist/components/chip.js.map +1 -1
  16. package/dist/components/confirm-dialog.d.ts +42 -1
  17. package/dist/components/confirm-dialog.js +36 -5
  18. package/dist/components/confirm-dialog.js.map +1 -1
  19. package/dist/components/copy-button.d.ts +10 -1
  20. package/dist/components/danger-confirm.d.ts +55 -4
  21. package/dist/components/danger-confirm.js +67 -30
  22. package/dist/components/danger-confirm.js.map +1 -1
  23. package/dist/components/data-table-cells.d.ts +1 -1
  24. package/dist/components/data-table-filter-popover.d.ts +1 -1
  25. package/dist/components/data-table-filters.d.ts +1 -1
  26. package/dist/components/data-table.d.ts +1 -1
  27. package/dist/components/data-table.js +93 -3
  28. package/dist/components/data-table.js.map +1 -1
  29. package/dist/components/facing-pair.d.ts +10 -1
  30. package/dist/components/file-button.d.ts +10 -1
  31. package/dist/components/file-dropzone.d.ts +10 -1
  32. package/dist/components/form-actions.d.ts +10 -1
  33. package/dist/components/form-actions.js +35 -1
  34. package/dist/components/form-actions.js.map +1 -1
  35. package/dist/components/line-items.d.ts +30 -6
  36. package/dist/components/line-items.js +77 -33
  37. package/dist/components/line-items.js.map +1 -1
  38. package/dist/components/list.d.ts +13 -1
  39. package/dist/components/list.js.map +1 -1
  40. package/dist/components/money-field.d.ts +10 -1
  41. package/dist/components/number-field.d.ts +11 -2
  42. package/dist/components/number-field.js +23 -22
  43. package/dist/components/number-field.js.map +1 -1
  44. package/dist/components/number-input.d.ts +10 -1
  45. package/dist/components/numpad-sheet.d.ts +10 -1
  46. package/dist/components/passkeys-setting.d.ts +27 -1
  47. package/dist/components/passkeys-setting.js +31 -5
  48. package/dist/components/passkeys-setting.js.map +1 -1
  49. package/dist/components/progress-bar.d.ts +25 -1
  50. package/dist/components/progress-bar.js +21 -4
  51. package/dist/components/progress-bar.js.map +1 -1
  52. package/dist/components/reauth-dialog.d.ts +90 -0
  53. package/dist/components/reauth-dialog.js +158 -0
  54. package/dist/components/reauth-dialog.js.map +1 -0
  55. package/dist/components/series-chart.d.ts +10 -1
  56. package/dist/components/server-wake.d.ts +58 -0
  57. package/dist/components/server-wake.js +58 -0
  58. package/dist/components/server-wake.js.map +1 -0
  59. package/dist/components/settings-fields.d.ts +10 -1
  60. package/dist/components/share-card.d.ts +61 -0
  61. package/dist/components/share-card.js +350 -0
  62. package/dist/components/share-card.js.map +1 -0
  63. package/dist/components/signed-amount.d.ts +12 -3
  64. package/dist/components/signed-amount.js +6 -4
  65. package/dist/components/signed-amount.js.map +1 -1
  66. package/dist/components/status-dot.d.ts +2 -0
  67. package/dist/components/text-link.d.ts +10 -1
  68. package/dist/components/time-input.d.ts +10 -1
  69. package/dist/components/toast.d.ts +10 -1
  70. package/dist/components/toast.js +42 -2
  71. package/dist/components/toast.js.map +1 -1
  72. package/dist/components/toggle-group.js +5 -2
  73. package/dist/components/toggle-group.js.map +1 -1
  74. package/dist/components/ui.d.ts +10 -1
  75. package/dist/components/ui.js +67 -30
  76. package/dist/components/ui.js.map +1 -1
  77. package/dist/components/use-table-state.d.ts +1 -1
  78. package/dist/components/user-avatar.d.ts +2 -0
  79. package/dist/components/write-lock.d.ts +68 -0
  80. package/dist/components/write-lock.js +33 -0
  81. package/dist/components/write-lock.js.map +1 -0
  82. package/dist/{data-table-CfVXtCeG.d.ts → data-table-3mi2R3so.d.ts} +59 -1
  83. package/dist/data-table.d.ts +1 -1
  84. package/dist/hooks/use-file-drop.d.ts +10 -1
  85. package/dist/hooks/use-overlay-history.js +16 -0
  86. package/dist/hooks/use-overlay-history.js.map +1 -1
  87. package/dist/i18n/defaults.d.ts +10 -1
  88. package/dist/i18n/defaults.js +11 -1
  89. package/dist/i18n/defaults.js.map +1 -1
  90. package/dist/i18n/german.d.ts +78 -0
  91. package/dist/i18n/{locales/de.js → german.js} +59 -9
  92. package/dist/i18n/german.js.map +1 -0
  93. package/dist/i18n/kit-labels.d.ts +10 -1
  94. package/dist/i18n/kit-labels.js +5 -0
  95. package/dist/i18n/kit-labels.js.map +1 -1
  96. package/dist/i18n/locales/de-CH.d.ts +23 -8
  97. package/dist/i18n/locales/de-CH.js +7 -3
  98. package/dist/i18n/locales/de-CH.js.map +1 -1
  99. package/dist/i18n/locales/{de.d.ts → en.d.ts} +24 -10
  100. package/dist/i18n/locales/en.js +95 -0
  101. package/dist/i18n/locales/en.js.map +1 -0
  102. package/dist/i18n/locales/es.d.ts +10 -1
  103. package/dist/i18n/locales/es.js +51 -1
  104. package/dist/i18n/locales/es.js.map +1 -1
  105. package/dist/i18n/locales/fr.d.ts +10 -1
  106. package/dist/i18n/locales/fr.js +51 -1
  107. package/dist/i18n/locales/fr.js.map +1 -1
  108. package/dist/i18n/locales/hu.d.ts +10 -1
  109. package/dist/i18n/locales/hu.js +54 -1
  110. package/dist/i18n/locales/hu.js.map +1 -1
  111. package/dist/i18n/locales/it.d.ts +10 -1
  112. package/dist/i18n/locales/it.js +51 -1
  113. package/dist/i18n/locales/it.js.map +1 -1
  114. package/dist/i18n/locales/zh.d.ts +10 -1
  115. package/dist/i18n/locales/zh.js +50 -1
  116. package/dist/i18n/locales/zh.js.map +1 -1
  117. package/dist/i18n/swiss.d.ts +3 -3
  118. package/dist/i18n/swiss.js.map +1 -1
  119. package/dist/index.d.ts +11 -6
  120. package/dist/index.js +33 -1
  121. package/dist/index.js.map +1 -1
  122. package/dist/{kit-labels-DE--LmCk.d.ts → kit-labels-CPbsjlXr.d.ts} +282 -10
  123. package/dist/lib/server-wake.d.ts +130 -0
  124. package/dist/lib/server-wake.js +121 -0
  125. package/dist/lib/server-wake.js.map +1 -0
  126. package/dist/rhf/fields.d.ts +10 -1
  127. package/dist/rhf/form.d.ts +10 -1
  128. package/dist/rhf/line-items.js.map +1 -1
  129. package/dist/rhf.d.ts +10 -1
  130. package/dist/search/global-search.d.ts +24 -4
  131. package/dist/search/global-search.js +31 -3
  132. package/dist/search/global-search.js.map +1 -1
  133. package/dist/search.d.ts +1 -1
  134. package/dist/shell/app-shell.d.ts +10 -1
  135. package/dist/shell/top-bar-brand.d.ts +10 -1
  136. package/dist/shell/topbar-action-menu.d.ts +2 -0
  137. package/dist/shell.d.ts +10 -1
  138. package/dist/wizard/stepper-nav.d.ts +10 -1
  139. package/dist/wizard.d.ts +10 -1
  140. package/package.json +1 -1
  141. package/src/components/account-chips.tsx +302 -0
  142. package/src/components/account-settings-labels.ts +16 -2
  143. package/src/components/amount-input.tsx +8 -0
  144. package/src/components/chip.tsx +239 -6
  145. package/src/components/confirm-dialog.tsx +82 -7
  146. package/src/components/danger-confirm.tsx +118 -33
  147. package/src/components/data-table.tsx +185 -3
  148. package/src/components/form-actions.tsx +93 -4
  149. package/src/components/line-items.tsx +116 -40
  150. package/src/components/list.tsx +13 -1
  151. package/src/components/number-field.tsx +32 -26
  152. package/src/components/passkeys-setting.tsx +68 -7
  153. package/src/components/progress-bar.tsx +40 -4
  154. package/src/components/reauth-dialog.tsx +273 -0
  155. package/src/components/server-wake.tsx +124 -0
  156. package/src/components/share-card.tsx +615 -0
  157. package/src/components/signed-amount.tsx +16 -5
  158. package/src/components/toast.tsx +72 -2
  159. package/src/components/toggle-group.tsx +5 -2
  160. package/src/components/ui.tsx +145 -34
  161. package/src/components/write-lock.tsx +96 -0
  162. package/src/hooks/use-overlay-history.ts +51 -0
  163. package/src/i18n/defaults.ts +10 -0
  164. package/src/i18n/{locales/de.ts → german.ts} +68 -13
  165. package/src/i18n/kit-labels.tsx +22 -2
  166. package/src/i18n/locales/de-CH.ts +17 -8
  167. package/src/i18n/locales/en.ts +116 -0
  168. package/src/i18n/locales/es.ts +51 -0
  169. package/src/i18n/locales/fr.ts +52 -0
  170. package/src/i18n/locales/hu.ts +55 -0
  171. package/src/i18n/locales/it.ts +51 -0
  172. package/src/i18n/locales/zh.ts +50 -0
  173. package/src/i18n/swiss.ts +3 -3
  174. package/src/index.ts +23 -1
  175. package/src/lib/server-wake.ts +270 -0
  176. package/src/rhf/line-items.tsx +2 -1
  177. package/src/search/global-search.tsx +59 -6
  178. package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
  179. package/dist/i18n/locales/de-CH-informal.js +0 -8
  180. package/dist/i18n/locales/de-CH-informal.js.map +0 -1
  181. package/dist/i18n/locales/de-informal.d.ts +0 -82
  182. package/dist/i18n/locales/de-informal.js +0 -37
  183. package/dist/i18n/locales/de-informal.js.map +0 -1
  184. package/dist/i18n/locales/de.js.map +0 -1
  185. package/src/i18n/locales/de-CH-informal.ts +0 -10
  186. package/src/i18n/locales/de-informal.ts +0 -61
@@ -89,6 +89,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
89
89
  passkeys: {
90
90
  title: "Passkey",
91
91
  description: "Accedi con impronta, volto o PIN del dispositivo invece che con una password.",
92
+ descriptionAlongside: "Accedi con impronta, volto o PIN del dispositivo. La tua password continua a funzionare.",
92
93
  empty: "Ancora nessuna passkey",
93
94
  loading: "Caricamento delle passkey…",
94
95
  list: "Le tue passkey",
@@ -498,6 +499,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
498
499
  confirmDialog: {
499
500
  confirm: "Conferma",
500
501
  cancel: "Annulla",
502
+ typed: (text) => `Digita «${text}» per confermare`,
501
503
  },
502
504
  floatingPanel: {
503
505
  close: "Chiudi",
@@ -583,6 +585,55 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
583
585
  noPreview: "Nessuna anteprima disponibile per questo file",
584
586
  openInNewTab: "Apri in una nuova scheda",
585
587
  },
588
+ writeLock: {
589
+ reason: "Puoi visualizzarlo, ma non modificarlo.",
590
+ },
591
+ accountState: {
592
+ // Agreeing with "account" (masculine): the chip is the state of an account.
593
+ active: "Attivo",
594
+ inactive: "Non attivo",
595
+ invited: "Invitato",
596
+ registered: "Registrato",
597
+ unverified: "Non verificato",
598
+ passwordChange: "Deve cambiare la password",
599
+ },
600
+ shareCard: {
601
+ dialogTitle: "Condividi",
602
+ close: "Chiudi",
603
+ email: "Indirizzo email",
604
+ emailOptional: "Indirizzo email (facoltativo)",
605
+ // example.com is reserved for examples (RFC 2606); a localised domain is real.
606
+ emailPlaceholder: "nome@example.com",
607
+ invalidEmail: "Inserisci un indirizzo email completo.",
608
+ role: "Ruolo",
609
+ roleOf: (name) => `Ruolo di ${name}`,
610
+ add: "Condividi",
611
+ whoHasAccess: "Chi ha accesso",
612
+ nobodyYet: "Nessun altro ha ancora accesso.",
613
+ pending: "In attesa",
614
+ openInvite: "Link di invito aperto",
615
+ copyLink: "Copia link",
616
+ team: "Team",
617
+ teamHint: (name) => `Tutti i membri di ${name} avranno accesso.`,
618
+ remove: "Rimuovi accesso",
619
+ removeConfirm: (name) => `Rimuovere l’accesso di ${name}?`,
620
+ revokePending: "Ritira invito",
621
+ revokePendingConfirm: (name) => `Ritirare l’invito per ${name}?`,
622
+ failed: "Non ha funzionato. Riprova.",
623
+ loading: "Caricamento…",
624
+ },
625
+ reauthDialog: {
626
+ title: "Conferma la tua identità",
627
+ description: "Inserisci la password attuale per continuare.",
628
+ password: "Password attuale",
629
+ submit: "Continua",
630
+ cancel: "Annulla",
631
+ },
632
+ serverWake: {
633
+ slow: "Caricamento in corso: ci vuole più del solito.",
634
+ waking: (appName) =>
635
+ `Il server va in sospensione quando nessuno usa ${appName ?? "l’app"}, quindi la prima richiesta dopo una pausa deve riavviarlo. Può volerci un momento: non si perde nulla, la pagina si completa da sola.`,
636
+ },
586
637
  };
587
638
  }
588
639
 
@@ -87,6 +87,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
87
87
  passkeys: {
88
88
  title: "通行密钥",
89
89
  description: "使用指纹、面容或设备 PIN 登录,无需密码。",
90
+ descriptionAlongside: "使用指纹、面容或设备 PIN 登录,密码仍可照常使用。",
90
91
  empty: "暂无通行密钥",
91
92
  loading: "正在加载通行密钥…",
92
93
  list: "已注册的通行密钥",
@@ -485,6 +486,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
485
486
  confirmDialog: {
486
487
  confirm: "确认",
487
488
  cancel: "取消",
489
+ typed: (text) => `输入“${text}”以确认`,
488
490
  },
489
491
  floatingPanel: {
490
492
  close: "关闭",
@@ -570,6 +572,54 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
570
572
  noPreview: "此文件无法预览",
571
573
  openInNewTab: "在新标签页中打开",
572
574
  },
575
+ writeLock: {
576
+ reason: "仅可查看,无法修改。",
577
+ },
578
+ accountState: {
579
+ active: "已启用",
580
+ inactive: "已停用",
581
+ invited: "已邀请",
582
+ registered: "已注册",
583
+ unverified: "未验证",
584
+ passwordChange: "需修改密码",
585
+ },
586
+ shareCard: {
587
+ dialogTitle: "共享",
588
+ close: "关闭",
589
+ email: "电子邮箱地址",
590
+ emailOptional: "电子邮箱地址(可选)",
591
+ emailPlaceholder: "name@example.com",
592
+ invalidEmail: "请输入完整的电子邮箱地址。",
593
+ role: "角色",
594
+ roleOf: (name) => `${name} 的角色`,
595
+ add: "共享",
596
+ whoHasAccess: "有权访问的人",
597
+ nobodyYet: "目前还没有其他人有权访问。",
598
+ pending: "待接受",
599
+ openInvite: "公开邀请链接",
600
+ copyLink: "复制链接",
601
+ team: "团队",
602
+ teamHint: (name) => `${name} 的所有成员都将获得访问权限。`,
603
+ remove: "移除访问权限",
604
+ removeConfirm: (name) => `移除 ${name} 的访问权限?`,
605
+ revokePending: "撤回邀请",
606
+ revokePendingConfirm: (name) => `撤回对 ${name} 的邀请?`,
607
+ failed: "操作未成功,请重试。",
608
+ loading: "加载中…",
609
+ },
610
+ reauthDialog: {
611
+ title: "验证身份",
612
+ description: "请输入当前密码以继续。",
613
+ password: "当前密码",
614
+ submit: "继续",
615
+ cancel: "取消",
616
+ },
617
+ serverWake: {
618
+ slow: "仍在加载,比平时耗时更长。",
619
+ // A Latin product name gets a space either side, as everywhere in this catalogue.
620
+ waking: (appName) =>
621
+ `无人使用${appName ? ` ${appName} ` : "本应用"}时,服务器会进入休眠,因此暂停后的第一个请求需要重新启动它。这可能需要一点时间——不会丢失任何内容,页面会自动加载完成。`,
622
+ },
573
623
  };
574
624
  }
575
625
 
package/src/i18n/swiss.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Swiss Standard German respelling, shared by the `de-CH` and `de-CH-informal`
3
- * translations. Internal: it lives outside `src/i18n/locales/` because every file there
4
- * becomes a public `@eifi1/ui-kit/i18n/<code>` subpath.
2
+ * Swiss Standard German respelling, applied to the internal German source (`german.ts`)
3
+ * to make the `de-CH` catalogue. Internal: it lives outside `src/i18n/locales/` because
4
+ * every file there becomes a public `@eifi1/ui-kit/i18n/<code>` subpath.
5
5
  *
6
6
  * "ss" for every "ß" (Schliessen, Grösse) is the one systematic difference in UI text.
7
7
  * Function labels are wrapped: their RESULT is respelled, so an argument the app passes
package/src/index.ts CHANGED
@@ -109,7 +109,13 @@ export * from "./components/password-strength";
109
109
  export * from "./components/page-contents";
110
110
  export * from "./components/disclosure";
111
111
  export * from "./components/dialog-frame";
112
- export * from "./components/danger-confirm";
112
+ // Named, not `*`: danger-confirm also holds the password field and pending hook that
113
+ // ReauthDialog shares with it, which are kit-internal.
114
+ export { DangerConfirm, DEFAULT_DANGER_CONFIRM_LABELS, typedMatches } from "./components/danger-confirm";
115
+ export type { DangerConfirmLabels, DangerConfirmProps, TypedMatch } from "./components/danger-confirm";
116
+ // 0.18: re-authentication before a sensitive action (Kurvenschmiede 4).
117
+ export { ReauthDialog, DEFAULT_REAUTH_DIALOG_LABELS } from "./components/reauth-dialog";
118
+ export type { ReauthDialogProps, ReauthDialogLabels } from "./components/reauth-dialog";
113
119
  // `useConfirm()` — the promise-based replacement for `window.confirm`, one host per app.
114
120
  export * from "./components/confirm-dialog";
115
121
  // A non-modal corner panel and its round trigger (keksdose's assistant launcher).
@@ -174,6 +180,22 @@ export * from "./components/description-list";
174
180
  // The Save / Cancel row forms and dialogs end in, and the editable line-item repeater
175
181
  // (journal lines, lease components, invoice positions). 0.12.0.
176
182
  export * from "./components/form-actions";
183
+ // 0.18: lock every opted-in commit under one provider, the reason in its tooltip
184
+ // (Kurvenschmiede 2). `useCommitReason` stays internal.
185
+ export { WriteLockProvider, useWriteLock, DEFAULT_WRITE_LOCK_LABELS } from "./components/write-lock";
186
+ export type { WriteLock, WriteLockLabels, WriteLockProviderProps } from "./components/write-lock";
187
+ // 0.18: the parts both apps' admin rosters repeat (Kurvenschmiede 6).
188
+ export { RoleChip, AccountStateChip, DateMark, dateColumn, DEFAULT_ACCOUNT_STATE_LABELS, ACCOUNT_STATE_TONES } from "./components/account-chips";
189
+ export type { RoleChipProps, RoleDefinition, RoleVocabulary, AccountState, AccountStateLabels, AccountStateChipProps, DateMarkProps, DateColumnOptions } from "./components/account-chips";
190
+ // 0.18: the cold-start notice keksdose built (#199), for every app: a framework-free
191
+ // watchdog over the app's requests and the corner notice that explains the wait.
192
+ export { createServerWake, serverWake, watchReadsAnd, attachServerWake, wrapFetch } from "./lib/server-wake";
193
+ export type { ServerWakeStage, ServerWakeFilter, ServerWakeOptions, ServerWakeRequest, ServerWakeWatcher, ServerWakeAxiosConfig, AxiosLikeInstance } from "./lib/server-wake";
194
+ export { ServerWakeNotice, useServerWakeStage, DEFAULT_SERVER_WAKE_LABELS } from "./components/server-wake";
195
+ export type { ServerWakeLabels, ServerWakeNoticeProps } from "./components/server-wake";
196
+ // 0.18: sharing — grantees, roles, pending grants, candidates (Kurvenschmiede 1).
197
+ export { ShareCard, ShareDialog, SharePanel, DEFAULT_SHARE_CARD_LABELS } from "./components/share-card";
198
+ export type { ShareCardProps, ShareDialogProps, SharePanelProps, ShareCardLabels, ShareRole, ShareGrantee, SharePendingGrant, ShareCandidate, ShareAddRequest } from "./components/share-card";
177
199
  export * from "./components/line-items";
178
200
  export * from "./components/progress-bar";
179
201
  // Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * "Is the server asleep, or is the app broken?" — the cold-start watchdog, lifted
3
+ * from keksdose (feedback #199) so kastlan and Kurvenschmiede get it too.
4
+ *
5
+ * All three backends run on Cloud Run with `--min-instances=0`: after an idle
6
+ * period the container is gone and the first request has to start it, which takes
7
+ * several seconds and occasionally more. Nothing in the UI said so, and none of the
8
+ * axios instances has a timeout, so the honest experience was a spinner that looked
9
+ * stuck. This counts the in-flight requests a cold start would actually delay and
10
+ * escalates in two steps, so a merely slow response says "still loading" and only a
11
+ * genuinely long wait blames the sleeping server.
12
+ *
13
+ * Framework-free on purpose (no zustand, no axios, no React): a counter, two timers
14
+ * and a listener set, read by `useServerWakeStage` through `useSyncExternalStore`.
15
+ * The adapters below take the app's axios instance / fetch as an ARGUMENT, so the
16
+ * kit never imports either.
17
+ *
18
+ * WHAT ARMS IT, and why the default is narrow:
19
+ * - reads (GET) — what every page does on open — always;
20
+ * - writes only when the app says so (`watchReadsAnd(/\/auth\/login\b/)`): the
21
+ * auth POSTs that gate the app are the only writes a user waits on before the
22
+ * app exists at all, so a cold start there looks exactly like a broken login.
23
+ * An UPLOAD must never be watched — a statement PDF or a photo takes seconds on a
24
+ * perfectly warm server, and "the server is waking up" during a 4 s upload would
25
+ * simply be false. With the default that holds by itself (an upload is a POST);
26
+ * an app widening `shouldWatch` keeps its upload routes out of it.
27
+ *
28
+ * It never arms while the browser is offline, whatever `shouldWatch` says: that state
29
+ * has its own, truthful indicator, and a request that never left the device is not
30
+ * evidence of anything about the server.
31
+ */
32
+
33
+ /** `slow` = longer than a warm server ever takes; `waking` = long enough that a cold
34
+ * start is the only sensible explanation. */
35
+ export type ServerWakeStage = "idle" | "slow" | "waking";
36
+
37
+ /** Decides which requests count. `method` arrives lower-cased (`"get"` when the
38
+ * request named none — axios's and fetch's default); `url` as the client had it. */
39
+ export type ServerWakeFilter = (method: string, url: string) => boolean;
40
+
41
+ export interface ServerWakeOptions {
42
+ /** Past this, a warm round trip is out of the question (a cold database query on a
43
+ * free tier can still take a second, so it is not tighter). Default 2000 ms. */
44
+ slowMs?: number;
45
+ /** Past this, the container was almost certainly scaled to zero. Default 7000 ms. */
46
+ wakingMs?: number;
47
+ /** Which requests count. Default {@link watchReadsAnd}`()` — GETs only. The offline
48
+ * guard runs before it and cannot be overridden. */
49
+ shouldWatch?: ServerWakeFilter;
50
+ }
51
+
52
+ /** What a request is, for {@link ServerWakeWatcher.track}. */
53
+ export interface ServerWakeRequest {
54
+ method?: string;
55
+ url?: string;
56
+ }
57
+
58
+ export interface ServerWakeWatcher {
59
+ /**
60
+ * Count a request that is going out, and arm the escalation if it is the first.
61
+ * Returns whether it was counted — the caller MUST call {@link end} exactly once
62
+ * for every `true`, or the counter never returns to zero and the notice sticks.
63
+ * (The axios adapter stashes it on the request config for that reason.)
64
+ */
65
+ start(method?: string, url?: string): boolean;
66
+ /** A counted request settled — either way: an error still proves the server
67
+ * answered or gave up. When the last one does, the notice goes away. Never takes
68
+ * the counter below zero. */
69
+ end(): void;
70
+ /** `start` + `end` around a promise (or a function returning one), for a request
71
+ * that does not go through an adapter. Resolves and rejects as the work does. */
72
+ track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request?: ServerWakeRequest): Promise<T>;
73
+ /** Back to a pristine watchdog — for tests, which share an instance across cases. */
74
+ reset(): void;
75
+ /** `useSyncExternalStore`'s pair. Listeners hear stage changes only. */
76
+ subscribe(listener: () => void): () => void;
77
+ getStage(): ServerWakeStage;
78
+ /** Counted requests currently outstanding. */
79
+ getInFlight(): number;
80
+ }
81
+
82
+ /**
83
+ * The usual filter: every GET, plus any other request whose URL matches `writes` —
84
+ * keksdose's `/\/auth\/(login|register|demo-session)\b/`. Keep upload routes out of
85
+ * the pattern (see the module comment).
86
+ */
87
+ export function watchReadsAnd(writes?: RegExp): ServerWakeFilter {
88
+ return (method, url) => method === "get" || (writes !== undefined && writes.test(url));
89
+ }
90
+
91
+ function isOffline(): boolean {
92
+ return typeof navigator !== "undefined" && navigator.onLine === false;
93
+ }
94
+
95
+ export function createServerWake({
96
+ slowMs = 2000,
97
+ wakingMs = 7000,
98
+ shouldWatch = watchReadsAnd(),
99
+ }: ServerWakeOptions = {}): ServerWakeWatcher {
100
+ let inFlight = 0;
101
+ let stage: ServerWakeStage = "idle";
102
+ let slowTimer: ReturnType<typeof setTimeout> | null = null;
103
+ let wakingTimer: ReturnType<typeof setTimeout> | null = null;
104
+ const listeners = new Set<() => void>();
105
+
106
+ const setStage = (next: ServerWakeStage) => {
107
+ if (next === stage) return;
108
+ stage = next;
109
+ for (const listener of listeners) listener();
110
+ };
111
+
112
+ const clearTimers = () => {
113
+ if (slowTimer !== null) clearTimeout(slowTimer);
114
+ if (wakingTimer !== null) clearTimeout(wakingTimer);
115
+ slowTimer = null;
116
+ wakingTimer = null;
117
+ };
118
+
119
+ const start = (method?: string, url?: string): boolean => {
120
+ if (isOffline()) return false;
121
+ if (!shouldWatch((method ?? "get").toLowerCase(), url ?? "")) return false;
122
+ inFlight += 1;
123
+ // The clock runs from the FIRST outstanding request: a page firing a second read
124
+ // at 1.9 s is still waiting on the same sleeping server, not starting afresh.
125
+ if (inFlight === 1) {
126
+ clearTimers();
127
+ slowTimer = setTimeout(() => setStage("slow"), slowMs);
128
+ wakingTimer = setTimeout(() => setStage("waking"), wakingMs);
129
+ }
130
+ return true;
131
+ };
132
+
133
+ const end = () => {
134
+ inFlight = Math.max(0, inFlight - 1);
135
+ if (inFlight > 0) return;
136
+ clearTimers();
137
+ setStage("idle");
138
+ };
139
+
140
+ return {
141
+ start,
142
+ end,
143
+ track<T>(work: PromiseLike<T> | (() => PromiseLike<T>), request: ServerWakeRequest = {}) {
144
+ const counted = start(request.method, request.url);
145
+ let promise: Promise<T>;
146
+ try {
147
+ promise = Promise.resolve(typeof work === "function" ? work() : work);
148
+ } catch (error) {
149
+ // A thunk that throws before it returns a promise settled too.
150
+ promise = Promise.reject(error);
151
+ }
152
+ return counted ? promise.finally(end) : promise;
153
+ },
154
+ reset() {
155
+ inFlight = 0;
156
+ clearTimers();
157
+ setStage("idle");
158
+ },
159
+ subscribe(listener) {
160
+ listeners.add(listener);
161
+ return () => {
162
+ listeners.delete(listener);
163
+ };
164
+ },
165
+ getStage: () => stage,
166
+ getInFlight: () => inFlight,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * The shared instance: what `<ServerWakeNotice />`, `useServerWakeStage()`,
172
+ * {@link attachServerWake} and {@link wrapFetch} use when no watcher is passed. GETs
173
+ * only, default timings — an app that also watches its login POST creates its own
174
+ * with {@link createServerWake} and passes it to both ends.
175
+ */
176
+ export const serverWake: ServerWakeWatcher = createServerWake();
177
+
178
+ // ── axios ─────────────────────────────────────────────────────────────────────
179
+
180
+ /** The two config fields the watchdog reads — what axios's request config has. */
181
+ export interface ServerWakeAxiosConfig {
182
+ method?: string;
183
+ url?: string;
184
+ }
185
+
186
+ /** The slice of an interceptor manager the adapter uses. A method (not a function
187
+ * property) so axios's wider `use(onFulfilled?, onRejected?, options?)` fits it. */
188
+ interface InterceptorManagerLike<V> {
189
+ use(onFulfilled: (value: V) => V | Promise<V>, onRejected: (error: unknown) => unknown): number;
190
+ eject(id: number): void;
191
+ }
192
+
193
+ /** An axios instance, structurally — so the kit types the adapter without importing
194
+ * axios. `axios.create()`'s result fits it as is. */
195
+ export interface AxiosLikeInstance<C extends ServerWakeAxiosConfig, R extends { config: C }> {
196
+ interceptors: {
197
+ request: InterceptorManagerLike<C>;
198
+ response: InterceptorManagerLike<R>;
199
+ };
200
+ }
201
+
202
+ /** Whether the watchdog counted this request — the response side owes a matching
203
+ * `end()` only then. On the config, as keksdose's `_wake` was: it travels with the
204
+ * request onto the response and onto an error. Cleared once settled, so a 401 replay
205
+ * of the same config re-enters the request interceptor and is counted afresh — the
206
+ * pairing stays balanced. */
207
+ const COUNTED = "_serverWake";
208
+ type Marked = { [COUNTED]?: boolean };
209
+
210
+ /**
211
+ * Drive a watcher from an axios instance's interceptors: `attachServerWake(api)`.
212
+ * Returns a function that ejects both interceptors again.
213
+ *
214
+ * Attach it BEFORE the app's own response interceptors: axios runs those in the order
215
+ * they were added, so the wake one ends the wait before a 401 handler awaits a token
216
+ * refresh — otherwise the notice would keep counting a request that has already been
217
+ * answered. (Request-side order does not matter.)
218
+ */
219
+ export function attachServerWake<C extends ServerWakeAxiosConfig, R extends { config: C }>(
220
+ instance: AxiosLikeInstance<C, R>,
221
+ watcher: ServerWakeWatcher = serverWake,
222
+ ): () => void {
223
+ const settle = (config: unknown) => {
224
+ const marked = config as Marked | undefined;
225
+ if (marked?.[COUNTED]) {
226
+ marked[COUNTED] = false;
227
+ watcher.end();
228
+ }
229
+ };
230
+ const request = instance.interceptors.request.use(
231
+ (config) => {
232
+ (config as C & Marked)[COUNTED] = watcher.start(config.method, config.url);
233
+ return config;
234
+ },
235
+ (error) => Promise.reject(error),
236
+ );
237
+ const response = instance.interceptors.response.use(
238
+ (res) => {
239
+ settle(res.config);
240
+ return res;
241
+ },
242
+ (error) => {
243
+ settle((error as { config?: unknown } | null)?.config);
244
+ return Promise.reject(error);
245
+ },
246
+ );
247
+ return () => {
248
+ instance.interceptors.request.eject(request);
249
+ instance.interceptors.response.eject(response);
250
+ };
251
+ }
252
+
253
+ // ── fetch ─────────────────────────────────────────────────────────────────────
254
+
255
+ /**
256
+ * A `fetch` that drives a watcher: `const apiFetch = wrapFetch(fetch)`. The wait ends
257
+ * when the response's HEADERS arrive (or the call fails) — the server is awake by then,
258
+ * however long the body takes.
259
+ */
260
+ export function wrapFetch(
261
+ fetchImpl: typeof fetch = globalThis.fetch,
262
+ watcher: ServerWakeWatcher = serverWake,
263
+ ): typeof fetch {
264
+ return (input, init) => {
265
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : undefined;
266
+ const method = init?.method ?? request?.method;
267
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : (request?.url ?? "");
268
+ return watcher.track(() => fetchImpl(input, init), { method, url });
269
+ };
270
+ }
@@ -24,7 +24,8 @@
24
24
  * list's `error`. Each cell's `render` gets `name`, the row's path
25
25
  * (`"components.2"`), to bind its field to.
26
26
  *
27
- * `summary`, `fieldLabels` and `narrowColumns` pass through to {@link LineItems}. With
27
+ * `summary`, `fieldLabels`, `narrowColumns`, `removePlacement` and `removeAlign` pass
28
+ * through to {@link LineItems}. With
28
29
  * `fieldLabels="floating"` a cell's `fieldLabel` is the column's name; the bound
29
30
  * fields' own `label` is a `FormLabel` ABOVE the control, so for the floating
30
31
  * one render the kit field through `RhfField`:
@@ -42,8 +42,22 @@ export const DEFAULT_GLOBAL_SEARCH_LABELS: GlobalSearchLabels = {
42
42
  export interface GlobalSearchSource {
43
43
  /** Stable key — the effect that runs the source is keyed by it, not by identity. */
44
44
  id: string;
45
- /** The group its results are shown under (an entry's own `group` is ignored). */
45
+ /** The group its results are shown under (an entry's own `group` is ignored) — unless
46
+ * `groups` is set, where it is only the home of entries that name no declared group.
47
+ * If every entry names a declared group, it is never shown: a catch-all heading such
48
+ * as "Other" is the honest value, so a stray type would still surface, labelled. */
46
49
  group: string;
50
+ /**
51
+ * Split ONE request into several groups — kastlan's record search answers with
52
+ * properties, units, contacts, leases and invoices at once, and one source per group
53
+ * meant a dedupe layer so the network still saw one request. Each entry is then shown
54
+ * under the group its own `group` names: a declared `key` (shown by its `label`) or a
55
+ * plain string (key and heading at once). The groups appear in THIS order whatever the
56
+ * answer's order, each kept to `limit` rows; an entry naming no declared group falls
57
+ * under `group`. While the request runs, or when it fails, the source shows one status
58
+ * line under its first group, not one per group. Unset: one group, as before.
59
+ */
60
+ groups?: readonly (string | GlobalSearchSourceGroup)[];
47
61
  /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked
48
62
  * them — and never re-filtered by the index's matcher. `signal` aborts when the query
49
63
  * moves on or the palette closes; a stale answer is dropped either way. */
@@ -52,7 +66,7 @@ export interface GlobalSearchSource {
52
66
  minChars?: number;
53
67
  /** Quiet period after the last keystroke before it is asked. Default 200ms. */
54
68
  debounceMs?: number;
55
- /** Rows kept. Default: `indexOptions.groupLimit` (8). */
69
+ /** Rows kept — per group, when `groups` is set. Default: `indexOptions.groupLimit` (8). */
56
70
  limit?: number;
57
71
  /** Mask every row of this source for session replay, hints (amounts) included. An
58
72
  * entry's own `redact` still wins. */
@@ -62,6 +76,13 @@ export interface GlobalSearchSource {
62
76
  onError?: (error: unknown) => void;
63
77
  }
64
78
 
79
+ /** One of a multi-group source's groups: the `key` its entries name in `group`, and the
80
+ * heading shown — so a server's type ("unit") need not be translated per entry. */
81
+ export interface GlobalSearchSourceGroup {
82
+ key: string;
83
+ label: string;
84
+ }
85
+
65
86
  /** What a custom trigger gets: everything the default button uses. */
66
87
  export interface GlobalSearchTriggerProps {
67
88
  open: () => void;
@@ -90,7 +111,7 @@ export interface GlobalSearchProps {
90
111
  * neither re-indexes nor loops, and the icons and handlers used are always the latest.
91
112
  */
92
113
  entries?: readonly SearchEntry[];
93
- /** The async sources, each streamed into its own group. */
114
+ /** The async sources, each streamed into its own group — or several (`groups`). */
94
115
  sources?: readonly GlobalSearchSource[];
95
116
  /** What the empty query shows, under the `suggestions` heading. */
96
117
  suggestions?: readonly GlobalSearchSuggestion[];
@@ -311,7 +332,17 @@ function GlobalSearchImpl({
311
332
  const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);
312
333
 
313
334
  const sourceKey = (sources ?? [])
314
- .map((s) => [s.id, s.group, s.minChars ?? 2, s.debounceMs ?? 200, s.limit, s.redact].join("\u0001"))
335
+ .map((s) =>
336
+ [
337
+ s.id,
338
+ s.group,
339
+ (s.groups ?? []).map((g) => (typeof g === "string" ? g : `${g.key}\u0003${g.label}`)).join("\u0004"),
340
+ s.minChars ?? 2,
341
+ s.debounceMs ?? 200,
342
+ s.limit,
343
+ s.redact,
344
+ ].join("\u0001"),
345
+ )
315
346
  .join("\u0002");
316
347
  const sourceList = useMemo(() => sources ?? [], [sourceKey]);
317
348
 
@@ -434,10 +465,12 @@ function GlobalSearchImpl({
434
465
  for (const source of sourceList) {
435
466
  if (q.length < (source.minChars ?? 2)) continue;
436
467
  const result = sourceResults[source.id];
468
+ const declared = (source.groups ?? []).map((g) => (typeof g === "string" ? { key: g, label: g } : g));
437
469
  const status = (label: string, icon: ReactNode): CommandItem => ({
438
470
  id: `src:${source.id}:status`,
439
471
  label,
440
- group: source.group,
472
+ // One line for the one request, under the group that will head its answer.
473
+ group: declared[0]?.label ?? source.group,
441
474
  icon,
442
475
  kind: "status",
443
476
  onSelect: noop,
@@ -448,10 +481,30 @@ function GlobalSearchImpl({
448
481
  out.push(status(pl.loading, <Loader2 className="size-3.5 animate-spin" />));
449
482
  } else if (result.status === "error") {
450
483
  out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error!, <AlertCircle className="size-3.5" />));
451
- } else {
484
+ } else if (!declared.length) {
452
485
  for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {
453
486
  out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));
454
487
  }
488
+ } else {
489
+ // Bucket by the entry's own group, then emit in the DECLARED order — the answer's
490
+ // order must not reshuffle the headings between keystrokes. Within a group the
491
+ // source's ranking stands. The group key is in the row id: two record types may
492
+ // share an id ("1"), and the palette keys rows by it.
493
+ const buckets = new Map<string, { label: string; entries: SearchEntry[] }>();
494
+ for (const g of declared) buckets.set(g.key, { label: g.label, entries: [] });
495
+ const rest: SearchEntry[] = [];
496
+ for (const entry of result.entries) {
497
+ const bucket = entry.group === undefined ? undefined : buckets.get(entry.group);
498
+ (bucket ? bucket.entries : rest).push(entry);
499
+ }
500
+ const limit = source.limit ?? groupLimit;
501
+ const emit = (key: string, label: string, list: SearchEntry[]) => {
502
+ for (const entry of list.slice(0, limit)) {
503
+ out.push(toItem(`src:${source.id}:${key}:`, entry, label, source.redact ?? redactLabels));
504
+ }
505
+ };
506
+ for (const [key, { label, entries: list }] of buckets) emit(key, label, list);
507
+ emit("~", source.group, rest);
455
508
  }
456
509
  }
457
510
 
@@ -1,61 +0,0 @@
1
- import { aZ as UiKitLabels } from '../../kit-labels-DE--LmCk.js';
2
- import 'react';
3
- import '../../components/data-table-labels.js';
4
- import '../../components/mini-calendar.js';
5
- import '../../components/calendar-heatmap.js';
6
- import '../../components/popover.js';
7
- import '../../components/chip.js';
8
- import 'lucide-react';
9
- import '../../components/field-sync.js';
10
- import '../../components/tooltip.js';
11
- import '../../hooks/use-anchored-rect.js';
12
- import '../../lib/clipping.js';
13
- import '../../wizard/types.js';
14
- import '../../tour/tour.js';
15
- import '../../search/command-palette.js';
16
- import '../../search/global-search.js';
17
- import '../../search/search-index.js';
18
- import '../../components/month-picker.js';
19
- import '../../components/page-contents.js';
20
- import '../../components/series-chart-labels.js';
21
- import '../../components/pie-chart-labels.js';
22
- import '../../components/sparkline.js';
23
- import '../../components/stat-tile.js';
24
- import '../../components/signature-pad.js';
25
- import '../../components/password-strength.js';
26
- import '../../components/danger-confirm.js';
27
- import '../../components/swatch-picker.js';
28
- import '../../components/tile-radio.js';
29
- import '../../components/icon-picker.js';
30
- import '../../components/dialog-frame.js';
31
- import '../../components/modal.js';
32
- import '../../hooks/use-search-param-state.js';
33
- import '../../components/measured-grid.js';
34
- import '../../feedback-attachment-WFiXJ8We.js';
35
- import '../../feedback/feedback-thread.js';
36
- import '../../feedback/feedback-inbox.js';
37
- import '../../components/account-settings-labels.js';
38
- import '../../components/confirm-dialog.js';
39
- import '../../components/floating-panel.js';
40
- import '../../components/bulk-action-bar.js';
41
- import '../../components/list.js';
42
- import '../../components/signed-amount.js';
43
- import '../../components/breadcrumbs.js';
44
- import '../../components/toast.js';
45
- import '../../components/description-list.js';
46
- import '../../components/line-items.js';
47
- import '../../components/progress-bar.js';
48
- import '../../components/error-boundary.js';
49
- import '../../components/authed-image.js';
50
- import '../../hooks/use-authed-src.js';
51
- import '../../components/image-grid.js';
52
- import '../../components/lightbox.js';
53
-
54
- /**
55
- * Swiss Standard German, informal ("du"): the informal German labels with Swiss
56
- * spelling ("ss" for every "ß") and `de-CH` number formatting (1’234.5) — derived the
57
- * same way `de-CH` is from `de`, so both stay in sync with their source.
58
- */
59
- declare const UI_KIT_LABELS_DE_CH_INFORMAL: UiKitLabels;
60
-
61
- export { UI_KIT_LABELS_DE_CH_INFORMAL };
@@ -1,8 +0,0 @@
1
- "use client";
2
- import { swiss } from "../swiss.js";
3
- import { uiKitLabelsDeInformal } from "./de-informal.js";
4
- const UI_KIT_LABELS_DE_CH_INFORMAL = swiss(uiKitLabelsDeInformal("de-CH"));
5
- export {
6
- UI_KIT_LABELS_DE_CH_INFORMAL
7
- };
8
- //# sourceMappingURL=de-CH-informal.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/i18n/locales/de-CH-informal.ts"],"sourcesContent":["import type { UiKitLabels } from \"../kit-labels\";\nimport { swiss } from \"../swiss\";\nimport { uiKitLabelsDeInformal } from \"./de-informal\";\n\n/**\n * Swiss Standard German, informal (\"du\"): the informal German labels with Swiss\n * spelling (\"ss\" for every \"ß\") and `de-CH` number formatting (1’234.5) — derived the\n * same way `de-CH` is from `de`, so both stay in sync with their source.\n */\nexport const UI_KIT_LABELS_DE_CH_INFORMAL: UiKitLabels = swiss(uiKitLabelsDeInformal(\"de-CH\"));\n"],"mappings":";AACA,SAAS,aAAa;AACtB,SAAS,6BAA6B;AAO/B,MAAM,+BAA4C,MAAM,sBAAsB,OAAO,CAAC;","names":[]}