@eifi1/ui-kit 0.32.0 → 0.32.2

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 (216) hide show
  1. package/dist/account/delete-account-setting.d.ts +1 -1
  2. package/dist/admin/admin-action-confirm.d.ts +1 -1
  3. package/dist/admin/admin-action-log.d.ts +1 -1
  4. package/dist/admin/admin-action-log.js +4 -2
  5. package/dist/admin/admin-action-log.js.map +1 -1
  6. package/dist/admin/invitations-panel.d.ts +1 -1
  7. package/dist/admin/reviewer-scope-editor.d.ts +1 -1
  8. package/dist/admin/role-select.d.ts +1 -1
  9. package/dist/admin/transfer-ownership-dialog.d.ts +1 -1
  10. package/dist/admin/user-roster.d.ts +1 -1
  11. package/dist/auth/register-form.d.ts +1 -1
  12. package/dist/billing/billing-banner.d.ts +1 -1
  13. package/dist/billing/billing-labels.d.ts +1 -1
  14. package/dist/billing/billing-labels.js +3 -1
  15. package/dist/billing/billing-labels.js.map +1 -1
  16. package/dist/billing/billing-lock.d.ts +1 -1
  17. package/dist/billing/billing-standing.d.ts +1 -1
  18. package/dist/billing/plan-card.d.ts +1 -1
  19. package/dist/billing/plan-card.js +1 -1
  20. package/dist/billing/plan-card.js.map +1 -1
  21. package/dist/billing/plan-limit-notice.d.ts +1 -1
  22. package/dist/billing/subscription-actions.d.ts +1 -1
  23. package/dist/billing/subscription-status-chip.d.ts +1 -1
  24. package/dist/chart.d.ts +2 -2
  25. package/dist/chart.js.map +1 -1
  26. package/dist/components/alert-banner.js +1 -1
  27. package/dist/components/alert-banner.js.map +1 -1
  28. package/dist/components/amount-input.d.ts +1 -1
  29. package/dist/components/appearance-labels.d.ts +6 -0
  30. package/dist/components/appearance-labels.js +2 -1
  31. package/dist/components/appearance-labels.js.map +1 -1
  32. package/dist/components/button-group.d.ts +1 -1
  33. package/dist/components/button-group.js +7 -1
  34. package/dist/components/button-group.js.map +1 -1
  35. package/dist/components/calculator.d.ts +1 -1
  36. package/dist/components/chip.js +7 -2
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/choice-card.js +1 -1
  39. package/dist/components/choice-card.js.map +1 -1
  40. package/dist/components/confirm-dialog.d.ts +1 -1
  41. package/dist/components/copy-button.d.ts +1 -1
  42. package/dist/components/danger-confirm.d.ts +1 -1
  43. package/dist/components/data-table-pagination.d.ts +2 -1
  44. package/dist/components/data-table-pagination.js +15 -6
  45. package/dist/components/data-table-pagination.js.map +1 -1
  46. package/dist/components/data-table.js +2 -2
  47. package/dist/components/data-table.js.map +1 -1
  48. package/dist/components/date-picker.js +11 -1
  49. package/dist/components/date-picker.js.map +1 -1
  50. package/dist/components/facing-pair.d.ts +1 -1
  51. package/dist/components/file-button.d.ts +1 -1
  52. package/dist/components/file-dropzone.d.ts +1 -1
  53. package/dist/components/form-actions.d.ts +1 -1
  54. package/dist/components/iban-input.d.ts +1 -1
  55. package/dist/components/inline-edit-field.js +4 -1
  56. package/dist/components/inline-edit-field.js.map +1 -1
  57. package/dist/components/language-select.d.ts +1 -1
  58. package/dist/components/legal-page.d.ts +1 -1
  59. package/dist/components/legal.d.ts +1 -1
  60. package/dist/components/money-field.d.ts +1 -1
  61. package/dist/components/month-picker.js +41 -34
  62. package/dist/components/month-picker.js.map +1 -1
  63. package/dist/components/number-field.d.ts +1 -1
  64. package/dist/components/number-input.d.ts +1 -1
  65. package/dist/components/numpad-sheet.d.ts +1 -1
  66. package/dist/components/page-header.js +6 -1
  67. package/dist/components/page-header.js.map +1 -1
  68. package/dist/components/phone-input.d.ts +1 -1
  69. package/dist/components/phone-input.js +2 -2
  70. package/dist/components/phone-input.js.map +1 -1
  71. package/dist/components/popover.d.ts +1 -1
  72. package/dist/components/popover.js +2 -0
  73. package/dist/components/popover.js.map +1 -1
  74. package/dist/components/row-actions.d.ts +1 -1
  75. package/dist/components/row-actions.js +97 -37
  76. package/dist/components/row-actions.js.map +1 -1
  77. package/dist/components/series-chart.d.ts +1 -1
  78. package/dist/components/settings-fields.d.ts +7 -3
  79. package/dist/components/settings-fields.js +2 -1
  80. package/dist/components/settings-fields.js.map +1 -1
  81. package/dist/components/share-card.d.ts +1 -1
  82. package/dist/components/share-card.js +6 -2
  83. package/dist/components/share-card.js.map +1 -1
  84. package/dist/components/status-dot.js +9 -2
  85. package/dist/components/status-dot.js.map +1 -1
  86. package/dist/components/swipeable-row.js +8 -1
  87. package/dist/components/swipeable-row.js.map +1 -1
  88. package/dist/components/text-link.d.ts +1 -1
  89. package/dist/components/time-input.d.ts +1 -1
  90. package/dist/components/toggle-group.d.ts +9 -2
  91. package/dist/components/toggle-group.js +3 -3
  92. package/dist/components/toggle-group.js.map +1 -1
  93. package/dist/components/translation-review-editor.js +1 -1
  94. package/dist/components/translation-review-editor.js.map +1 -1
  95. package/dist/components/translation-review.js +2 -2
  96. package/dist/components/translation-review.js.map +1 -1
  97. package/dist/components/ui.d.ts +1 -1
  98. package/dist/components/ui.js +6 -6
  99. package/dist/components/ui.js.map +1 -1
  100. package/dist/components/wizard-stepper.js +20 -14
  101. package/dist/components/wizard-stepper.js.map +1 -1
  102. package/dist/{delete-account-setting-BvdQdbv3.d.ts → delete-account-setting-Dy4kJgUm.d.ts} +53 -14
  103. package/dist/feedback/feedback-attachment.d.ts +1 -1
  104. package/dist/feedback/feedback-context.d.ts +1 -1
  105. package/dist/feedback/feedback-crash.d.ts +1 -1
  106. package/dist/feedback/feedback-dialog.d.ts +1 -1
  107. package/dist/feedback/feedback-inbox.d.ts +1 -1
  108. package/dist/feedback/feedback-labels.d.ts +1 -1
  109. package/dist/feedback/feedback-menu.d.ts +1 -1
  110. package/dist/feedback/feedback-record.d.ts +1 -1
  111. package/dist/feedback/feedback-row-detail.d.ts +1 -1
  112. package/dist/feedback/feedback-status-undo.d.ts +1 -1
  113. package/dist/feedback/feedback-submit.d.ts +1 -1
  114. package/dist/feedback/feedback-swipe.d.ts +1 -1
  115. package/dist/feedback/feedback-table.d.ts +1 -1
  116. package/dist/feedback/feedback-thread.d.ts +1 -1
  117. package/dist/feedback.d.ts +1 -1
  118. package/dist/hooks/use-breakpoint.d.ts +19 -8
  119. package/dist/hooks/use-breakpoint.js +1 -1
  120. package/dist/hooks/use-breakpoint.js.map +1 -1
  121. package/dist/hooks/use-file-drop.d.ts +1 -1
  122. package/dist/i18n/defaults.d.ts +1 -1
  123. package/dist/i18n/german.d.ts +1 -1
  124. package/dist/i18n/german.js +7 -3
  125. package/dist/i18n/german.js.map +1 -1
  126. package/dist/i18n/kit-labels.d.ts +1 -1
  127. package/dist/i18n/languages.d.ts +1 -1
  128. package/dist/i18n/locales/de-CH.d.ts +1 -1
  129. package/dist/i18n/locales/en.d.ts +1 -1
  130. package/dist/i18n/locales/es.d.ts +1 -1
  131. package/dist/i18n/locales/es.js +7 -3
  132. package/dist/i18n/locales/es.js.map +1 -1
  133. package/dist/i18n/locales/fr.d.ts +1 -1
  134. package/dist/i18n/locales/fr.js +7 -3
  135. package/dist/i18n/locales/fr.js.map +1 -1
  136. package/dist/i18n/locales/hu.d.ts +1 -1
  137. package/dist/i18n/locales/hu.js +7 -3
  138. package/dist/i18n/locales/hu.js.map +1 -1
  139. package/dist/i18n/locales/it.d.ts +1 -1
  140. package/dist/i18n/locales/it.js +7 -3
  141. package/dist/i18n/locales/it.js.map +1 -1
  142. package/dist/i18n/locales/zh.d.ts +1 -1
  143. package/dist/i18n/locales/zh.js +7 -3
  144. package/dist/i18n/locales/zh.js.map +1 -1
  145. package/dist/i18n/review.d.ts +1 -1
  146. package/dist/index.d.ts +3 -3
  147. package/dist/index.js +3 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/landing/landing-actions.d.ts +1 -1
  150. package/dist/landing/landing-sections.d.ts +1 -1
  151. package/dist/landing/landing-sections.js +7 -3
  152. package/dist/landing/landing-sections.js.map +1 -1
  153. package/dist/landing/public-header.d.ts +1 -1
  154. package/dist/landing/routing.d.ts +1 -1
  155. package/dist/rhf/fields.d.ts +1 -1
  156. package/dist/rhf/form.d.ts +1 -1
  157. package/dist/rhf.d.ts +1 -1
  158. package/dist/settings/settings-section.d.ts +1 -1
  159. package/dist/settings/use-account-appearance.d.ts +1 -1
  160. package/dist/settings/use-account-appearance.js.map +1 -1
  161. package/dist/shell/app-shell.d.ts +1 -1
  162. package/dist/shell/app-shell.js +2 -1
  163. package/dist/shell/app-shell.js.map +1 -1
  164. package/dist/shell/top-bar-brand.d.ts +1 -1
  165. package/dist/shell.d.ts +1 -1
  166. package/dist/theme/contrast.d.ts +3 -2
  167. package/dist/theme/contrast.js.map +1 -1
  168. package/dist/theme/text-size.d.ts +19 -3
  169. package/dist/theme/text-size.js.map +1 -1
  170. package/dist/wizard/stepper-nav.d.ts +1 -1
  171. package/dist/wizard/wizard-summary.js +1 -1
  172. package/dist/wizard/wizard-summary.js.map +1 -1
  173. package/dist/wizard.d.ts +1 -1
  174. package/package.json +1 -1
  175. package/src/admin/admin-action-log.tsx +6 -1
  176. package/src/billing/billing-labels.ts +16 -0
  177. package/src/billing/plan-card.tsx +1 -1
  178. package/src/chart.ts +2 -0
  179. package/src/components/alert-banner.tsx +5 -1
  180. package/src/components/appearance-labels.ts +7 -0
  181. package/src/components/button-group.tsx +21 -0
  182. package/src/components/chip.tsx +34 -6
  183. package/src/components/choice-card.tsx +1 -1
  184. package/src/components/data-table-pagination.tsx +60 -31
  185. package/src/components/data-table.tsx +8 -4
  186. package/src/components/date-picker.tsx +32 -1
  187. package/src/components/inline-edit-field.tsx +20 -1
  188. package/src/components/month-picker.tsx +6 -1
  189. package/src/components/page-header.tsx +15 -1
  190. package/src/components/phone-input.tsx +7 -2
  191. package/src/components/popover.tsx +4 -0
  192. package/src/components/row-actions.tsx +156 -49
  193. package/src/components/settings-fields.tsx +8 -3
  194. package/src/components/share-card.tsx +21 -2
  195. package/src/components/status-dot.tsx +25 -2
  196. package/src/components/swipeable-row.tsx +8 -1
  197. package/src/components/toggle-group.tsx +26 -12
  198. package/src/components/translation-review-editor.tsx +1 -1
  199. package/src/components/translation-review.tsx +5 -4
  200. package/src/components/ui.tsx +28 -9
  201. package/src/components/wizard-stepper.tsx +5 -1
  202. package/src/hooks/use-breakpoint.ts +20 -9
  203. package/src/i18n/german.ts +4 -0
  204. package/src/i18n/locales/es.ts +4 -0
  205. package/src/i18n/locales/fr.ts +4 -0
  206. package/src/i18n/locales/hu.ts +4 -0
  207. package/src/i18n/locales/it.ts +4 -0
  208. package/src/i18n/locales/zh.ts +4 -0
  209. package/src/index.ts +4 -2
  210. package/src/landing/landing-sections.tsx +7 -3
  211. package/src/settings/use-account-appearance.ts +1 -1
  212. package/src/shell/app-shell.tsx +11 -6
  213. package/src/theme/contrast.ts +3 -2
  214. package/src/theme/text-size.ts +19 -3
  215. package/src/wizard/wizard-summary.tsx +4 -2
  216. package/tokens.css +15 -7
@@ -15,7 +15,7 @@ function WizardSummary({
15
15
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
16
16
  /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-[var(--text-primary)]", children: l.reviewTitle }),
17
17
  sections.map((section, sIdx) => /* @__PURE__ */ jsxs(Card, { children: [
18
- /* @__PURE__ */ jsxs(CardHeader, { className: "pb-2", children: [
18
+ /* @__PURE__ */ jsxs(CardHeader, { className: "pb-2", stackAction: false, children: [
19
19
  /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: section.label }),
20
20
  section.stepIndex !== void 0 && onEditStep && /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Tooltip, { label: l.edit, children: /* @__PURE__ */ jsx(
21
21
  IconButton,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wizard/wizard-summary.tsx"],"sourcesContent":["import { Pencil } from \"lucide-react\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle, IconButton } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport type { SummarySection, WizardLabels } from \"./types\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * The review step: one card per section, each row a label/value pair, each card\n * carrying an edit button back to the step it came from.\n *\n * Sections are data, not children, because \"jump back to step N\" is the whole\n * point of the component and a caller assembling its own cards invariably drops\n * it. Labels and values are `ReactNode`, so a value can be a badge or a link.\n *\n * A section with no `stepIndex` (file-level counts, a total) is a card without an\n * edit button. `disabled` greys every edit button at once — while the commit runs,\n * when jumping back would show a step whose edits can no longer reach the request.\n */\nexport function WizardSummary({\n sections,\n onEditStep,\n disabled = false,\n labels,\n}: {\n sections: SummarySection[];\n /** Called with the section's `stepIndex`. Optional only for a summary whose\n * sections name no step at all. */\n onEditStep?: (stepIndex: number) => void;\n /** Disable every edit button (e.g. `wizard.isSubmitting`). */\n disabled?: boolean;\n labels?: Partial<WizardLabels>;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold text-[var(--text-primary)]\">{l.reviewTitle}</h2>\n {sections.map((section, sIdx) => (\n <Card key={sIdx}>\n {/* CardHeader is a grid, and CardAction is the top-right column it\n reserves — the edit button has to go through it or it stacks under\n the title instead of sitting beside it. */}\n <CardHeader className=\"pb-2\">\n <CardTitle className=\"text-base\">{section.label}</CardTitle>\n {section.stepIndex !== undefined && onEditStep && (\n <CardAction>\n {/* The kit Tooltip, not a native `title`: it shows on keyboard focus too,\n and an app with a one-tooltip rule (keksdose dev#523) sees one. */}\n <Tooltip label={l.edit}>\n <IconButton\n size=\"sm\"\n variant=\"ghost\"\n aria-label={l.edit}\n disabled={disabled}\n onClick={() => onEditStep(section.stepIndex as number)}\n >\n <Pencil />\n </IconButton>\n </Tooltip>\n </CardAction>\n )}\n </CardHeader>\n <CardContent>\n <dl className=\"grid gap-2 text-sm\">\n {section.items.map((item, iIdx) => (\n <div key={iIdx}>\n {iIdx > 0 && (\n <div className=\"mb-2 border-t border-[var(--border)]\" role=\"presentation\" />\n )}\n <div className=\"flex items-start justify-between gap-4\">\n <dt className=\"text-[var(--text-muted)]\">{item.label}</dt>\n <dd className=\"text-end font-medium text-[var(--text-primary)]\">\n {item.value}\n </dd>\n </div>\n </div>\n ))}\n </dl>\n </CardContent>\n </Card>\n ))}\n </div>\n );\n}\n"],"mappings":";AAqCM,cAMI,YANJ;AArCN,SAAS,cAAc;AACvB,SAAS,MAAM,YAAY,aAAa,YAAY,WAAW,kBAAkB;AACjF,SAAS,eAAe;AAExB,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AActB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAQG;AACD,QAAM,IAAI,aAAa,UAAU,uBAAuB,MAAM;AAE9D,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,QAAG,WAAU,oDAAoD,YAAE,aAAY;AAAA,IAC/E,SAAS,IAAI,CAAC,SAAS,SACtB,qBAAC,QAIC;AAAA,2BAAC,cAAW,WAAU,QACpB;AAAA,4BAAC,aAAU,WAAU,aAAa,kBAAQ,OAAM;AAAA,QAC/C,QAAQ,cAAc,UAAa,cAClC,oBAAC,cAGC,8BAAC,WAAQ,OAAO,EAAE,MAChB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,cAAY,EAAE;AAAA,YACd;AAAA,YACA,SAAS,MAAM,WAAW,QAAQ,SAAmB;AAAA,YAErD,8BAAC,UAAO;AAAA;AAAA,QACV,GACF,GACF;AAAA,SAEJ;AAAA,MACA,oBAAC,eACC,8BAAC,QAAG,WAAU,sBACX,kBAAQ,MAAM,IAAI,CAAC,MAAM,SACxB,qBAAC,SACE;AAAA,eAAO,KACN,oBAAC,SAAI,WAAU,wCAAuC,MAAK,gBAAe;AAAA,QAE5E,qBAAC,SAAI,WAAU,0CACb;AAAA,8BAAC,QAAG,WAAU,4BAA4B,eAAK,OAAM;AAAA,UACrD,oBAAC,QAAG,WAAU,mDACX,eAAK,OACR;AAAA,WACF;AAAA,WATQ,IAUV,CACD,GACH,GACF;AAAA,SAxCS,IAyCX,CACD;AAAA,KACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/wizard/wizard-summary.tsx"],"sourcesContent":["import { Pencil } from \"lucide-react\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle, IconButton } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport type { SummarySection, WizardLabels } from \"./types\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * The review step: one card per section, each row a label/value pair, each card\n * carrying an edit button back to the step it came from.\n *\n * Sections are data, not children, because \"jump back to step N\" is the whole\n * point of the component and a caller assembling its own cards invariably drops\n * it. Labels and values are `ReactNode`, so a value can be a badge or a link.\n *\n * A section with no `stepIndex` (file-level counts, a total) is a card without an\n * edit button. `disabled` greys every edit button at once — while the commit runs,\n * when jumping back would show a step whose edits can no longer reach the request.\n */\nexport function WizardSummary({\n sections,\n onEditStep,\n disabled = false,\n labels,\n}: {\n sections: SummarySection[];\n /** Called with the section's `stepIndex`. Optional only for a summary whose\n * sections name no step at all. */\n onEditStep?: (stepIndex: number) => void;\n /** Disable every edit button (e.g. `wizard.isSubmitting`). */\n disabled?: boolean;\n labels?: Partial<WizardLabels>;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n\n return (\n <div className=\"space-y-4\">\n <h2 className=\"text-lg font-semibold text-[var(--text-primary)]\">{l.reviewTitle}</h2>\n {sections.map((section, sIdx) => (\n <Card key={sIdx}>\n {/* CardHeader is a grid, and CardAction is the top-right column it\n reserves — the edit button has to go through it or it stacks under\n the title instead of sitting beside it. `stackAction={false}`: the pencil\n is named by `aria-label` and stays an icon at Large, so it keeps its\n place beside the title there too (0.32.1). */}\n <CardHeader className=\"pb-2\" stackAction={false}>\n <CardTitle className=\"text-base\">{section.label}</CardTitle>\n {section.stepIndex !== undefined && onEditStep && (\n <CardAction>\n {/* The kit Tooltip, not a native `title`: it shows on keyboard focus too,\n and an app with a one-tooltip rule (keksdose dev#523) sees one. */}\n <Tooltip label={l.edit}>\n <IconButton\n size=\"sm\"\n variant=\"ghost\"\n aria-label={l.edit}\n disabled={disabled}\n onClick={() => onEditStep(section.stepIndex as number)}\n >\n <Pencil />\n </IconButton>\n </Tooltip>\n </CardAction>\n )}\n </CardHeader>\n <CardContent>\n <dl className=\"grid gap-2 text-sm\">\n {section.items.map((item, iIdx) => (\n <div key={iIdx}>\n {iIdx > 0 && (\n <div className=\"mb-2 border-t border-[var(--border)]\" role=\"presentation\" />\n )}\n <div className=\"flex items-start justify-between gap-4\">\n <dt className=\"text-[var(--text-muted)]\">{item.label}</dt>\n <dd className=\"text-end font-medium text-[var(--text-primary)]\">\n {item.value}\n </dd>\n </div>\n </div>\n ))}\n </dl>\n </CardContent>\n </Card>\n ))}\n </div>\n );\n}\n"],"mappings":";AAqCM,cAQI,YARJ;AArCN,SAAS,cAAc;AACvB,SAAS,MAAM,YAAY,aAAa,YAAY,WAAW,kBAAkB;AACjF,SAAS,eAAe;AAExB,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AActB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AACF,GAQG;AACD,QAAM,IAAI,aAAa,UAAU,uBAAuB,MAAM;AAE9D,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,QAAG,WAAU,oDAAoD,YAAE,aAAY;AAAA,IAC/E,SAAS,IAAI,CAAC,SAAS,SACtB,qBAAC,QAMC;AAAA,2BAAC,cAAW,WAAU,QAAO,aAAa,OACxC;AAAA,4BAAC,aAAU,WAAU,aAAa,kBAAQ,OAAM;AAAA,QAC/C,QAAQ,cAAc,UAAa,cAClC,oBAAC,cAGC,8BAAC,WAAQ,OAAO,EAAE,MAChB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,cAAY,EAAE;AAAA,YACd;AAAA,YACA,SAAS,MAAM,WAAW,QAAQ,SAAmB;AAAA,YAErD,8BAAC,UAAO;AAAA;AAAA,QACV,GACF,GACF;AAAA,SAEJ;AAAA,MACA,oBAAC,eACC,8BAAC,QAAG,WAAU,sBACX,kBAAQ,MAAM,IAAI,CAAC,MAAM,SACxB,qBAAC,SACE;AAAA,eAAO,KACN,oBAAC,SAAI,WAAU,wCAAuC,MAAK,gBAAe;AAAA,QAE5E,qBAAC,SAAI,WAAU,0CACb;AAAA,8BAAC,QAAG,WAAU,4BAA4B,eAAK,OAAM;AAAA,UACrD,oBAAC,QAAG,WAAU,mDACX,eAAK,OACR;AAAA,WACF;AAAA,WATQ,IAUV,CACD,GACH,GACF;AAAA,SA1CS,IA2CX,CACD;AAAA,KACH;AAEJ;","names":[]}
package/dist/wizard.d.ts CHANGED
@@ -6,7 +6,7 @@ export { WizardStep, WizardStepProps } from './wizard/wizard-step.js';
6
6
  export { WizardSummary } from './wizard/wizard-summary.js';
7
7
  export { StepperNav } from './wizard/stepper-nav.js';
8
8
  import 'react';
9
- import './delete-account-setting-BvdQdbv3.js';
9
+ import './delete-account-setting-Dy4kJgUm.js';
10
10
  import './components/tooltip.js';
11
11
  import './hooks/use-anchored-rect.js';
12
12
  import './lib/clipping.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -47,7 +47,10 @@ export type AdminActionKind =
47
47
  | "transfer"
48
48
  | "deletion_request"
49
49
  | "deletion_cancel"
50
- | "erase";
50
+ | "erase"
51
+ // 0.32.1 (keksdose's 0.32 report): server-kit 0.6's `AdminAction.PLAN`, an operator's
52
+ // plan change or grant (docs/billing-harmonization.md §6).
53
+ | "plan";
51
54
 
52
55
  /** Every {@link AdminActionKind}, in the contract's order. */
53
56
  export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
@@ -67,6 +70,7 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
67
70
  "deletion_request",
68
71
  "deletion_cancel",
69
72
  "erase",
73
+ "plan",
70
74
  ];
71
75
 
72
76
  /** One logged action — the app's `admin_actions` row with the people joined in. */
@@ -142,6 +146,7 @@ export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
142
146
  deletion_request: "Deletion requested",
143
147
  deletion_cancel: "Deletion cancelled",
144
148
  erase: "Account erased",
149
+ plan: "Plan changed",
145
150
  },
146
151
  };
147
152
 
@@ -146,6 +146,19 @@ export interface BillingLabels {
146
146
  waitingChanges: (count: number) => string;
147
147
  /** The sign-out guard's way out for someone who will not pay. */
148
148
  discardWaiting: string;
149
+
150
+ /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */
151
+ /**
152
+ * `billing_not_configured`: billing is on but the provider's settings are missing — the
153
+ * operator's mistake, so the words ask the payer to wait, not to act. For the toast or
154
+ * alert after "Choose a plan" or "Payment and invoices" (`isBillingError(err,
155
+ * "billing_not_configured")`). `billing_read_only` and `plan_limit` need none: the
156
+ * write lock, the banner and `PlanLimitNotice` say them.
157
+ */
158
+ notConfigured: string;
159
+ /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a
160
+ * link that reached it anyway. */
161
+ disabled: string;
149
162
  }
150
163
 
151
164
  /** "today", "tomorrow", "in 5 days". */
@@ -215,6 +228,9 @@ export const DEFAULT_BILLING_LABELS: BillingLabels = {
215
228
  waitingChanges: (count) =>
216
229
  count === 1 ? "1 change waiting for a plan" : `${count} changes waiting for a plan`,
217
230
  discardWaiting: "Discard waiting changes",
231
+
232
+ notConfigured: "Payments aren't set up yet. Please try again later.",
233
+ disabled: "Subscriptions aren't available here.",
218
234
  };
219
235
 
220
236
  /** The `billing` namespace, resolved: English, then the provider, then `labels` — which
@@ -420,7 +420,7 @@ export function PlanPicker({
420
420
  {legend}
421
421
  </legend>
422
422
  )}
423
- <div className={cn("grid gap-2 sm:grid-cols-2 lg:grid-cols-3", gridClassName)}>
423
+ <div className={cn("grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3", gridClassName)}>
424
424
  {plans.map((plan) => (
425
425
  <PlanCard
426
426
  key={plan.code}
package/src/chart.ts CHANGED
@@ -16,6 +16,8 @@ export * from "./components/treemap";
16
16
  // lenkbank's zoomable series chart, its synced x-zoom, the toggle legend and the
17
17
  // mirrored-pair axis geometry — moved here so the apps derive them from the package.
18
18
  export * from "./components/series-chart";
19
+ // 0.32.1 (keksdose's 0.32 report): the height every chart takes, for an app's wrappers.
20
+ export type { ChartHeight } from "./components/chart-height";
19
21
  export * from "./components/chart-zoom";
20
22
  export * from "./components/toggle-legend";
21
23
  export * from "./components/facing-pair";
@@ -336,11 +336,15 @@ export function AlertBanner({
336
336
  ) : null;
337
337
 
338
338
  // `relative z-10` so it stays clickable over a whole-row banner's stretched target.
339
+ // `max-w-full flex-wrap`: on a line of its own, two actions side by side ran 57 px
340
+ // past a 360 px phone at Extra large (the demo strip's "Request access" and "Sign in",
341
+ // 0.32.1); held to the line, they wrap there instead. Where they fit in one row,
342
+ // nothing changes.
339
343
  const trailing =
340
344
  action !== undefined && action !== null ? (
341
345
  <div
342
346
  className={cn(
343
- "relative z-10 flex shrink-0 items-center gap-2",
347
+ "relative z-10 flex max-w-full shrink-0 flex-wrap items-center gap-2",
344
348
  !strip && !inlineRow && "self-center",
345
349
  strip && "ms-auto",
346
350
  // Last, on a line of its own, lined up with the message (past the glyph).
@@ -22,6 +22,12 @@ export interface AppearanceLabels {
22
22
  contrastHelp: string;
23
23
  /** The three choices, in {@link CONTRAST_MODES} order. */
24
24
  contrastModes: Record<ContrastMode, string>;
25
+ /**
26
+ * The account could not keep the pick (`useAccountAppearance`'s `pickTextSize` /
27
+ * `pickContrast` rejected): this device keeps it, the account doesn't. For the app's
28
+ * toast or the line under the setting (0.32.1, keksdose's 0.32 report).
29
+ */
30
+ saveFailed: string;
25
31
  }
26
32
 
27
33
  export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
@@ -31,6 +37,7 @@ export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
31
37
  contrast: "Contrast",
32
38
  contrastHelp: "More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.",
33
39
  contrastModes: { system: "System", standard: "Standard", more: "More" },
40
+ saveFailed: "Couldn't save this to your account. It still applies on this device.",
34
41
  };
35
42
 
36
43
  /** The `appearance` namespace, resolved — English, then the provider, then `labels`. For
@@ -26,6 +26,26 @@ const JOINED_COMMON = cn(
26
26
  "[&>*]:relative [&>*:focus-within]:z-10",
27
27
  );
28
28
 
29
+ /**
30
+ * A joined row at Large and Extra large wraps rather than run past its container (0.32.1,
31
+ * the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 "nothing
32
+ * overflows"): "Buttons | Chips | Feedback" was 329 px at 150 %, 67 px past a 360 px
33
+ * phone's card. A wrapped row cannot keep one frame with dividers between members — the
34
+ * second line's first member would carry a divider against the frame and no line would
35
+ * part the rows — so at Large the frame is drawn by the members: each its own 1px
36
+ * border, overlapping its neighbours by a pixel on both axes (the container's `ps-px
37
+ * pt-px` takes the overlap back at the edges). On one row that draws the same frame and
38
+ * dividers as Normal; wrapped, every line and every member is parted by one hairline,
39
+ * and the members share out each line (`grow`), so the lines make one block. A group
40
+ * that fits is only as wide as its members, so nothing grows there. A `large:` class,
41
+ * so Normal keeps the frame it has, and it costs no render.
42
+ */
43
+ const JOINED_WRAP_AT_LARGE = cn(
44
+ "large:max-w-full large:flex-wrap large:border-0 large:ps-px large:pt-px",
45
+ "large:[&>*]:border large:[&>*]:-ms-px large:[&>*]:-mt-px",
46
+ "large:[&>*]:grow large:[&>*>button]:grow large:[&>*>a]:grow",
47
+ );
48
+
29
49
  const JOINED: Record<"horizontal" | "vertical", string> = {
30
50
  // Logical edges and corners throughout (`border-s`, `rounded-s-*`), so in RTL the
31
51
  // first member sits on the right with the right-hand corners rounded — the physical
@@ -37,6 +57,7 @@ const JOINED: Record<"horizontal" | "vertical", string> = {
37
57
  "[&>:first-child]:rounded-s-md [&>:last-child]:rounded-e-md",
38
58
  "[&>:first-child>button]:rounded-s-md [&>:last-child>button]:rounded-e-md",
39
59
  "[&>:first-child>a]:rounded-s-md [&>:last-child>a]:rounded-e-md",
60
+ JOINED_WRAP_AT_LARGE,
40
61
  ),
41
62
  // The block axis is top-to-bottom in both reading directions, so top/bottom are
42
63
  // already the logical edges here.
@@ -382,8 +382,10 @@ export function refreshChipEdges(): void {
382
382
  const r = el.getBoundingClientRect();
383
383
  const label = el.querySelector<HTMLElement>(".truncate");
384
384
  // Not laid out (a hidden ancestor, jsdom), or truncated at its container's edge,
385
- // where a wider chip would overflow: leave it alone.
386
- if (!r.width || (label && label.scrollWidth > label.clientWidth)) return null;
385
+ // where a wider chip would overflow: leave it alone. So too a label wrapped onto
386
+ // a second line at Large (0.32.1): the chip is already as wide as its row lets it
387
+ // be, and a min-width past that would push it over the edge just the same.
388
+ if (!r.width || (label && (label.scrollWidth > label.clientWidth || labelWraps(label)))) return null;
387
389
  const width = Math.round(r.width * 64) / 64;
388
390
  const target = snappedChipEdgeWidth(width, r.left + window.scrollX, dpr);
389
391
  return target === width ? null : target;
@@ -406,6 +408,13 @@ export function refreshChipEdges(): void {
406
408
  }
407
409
  }
408
410
 
411
+ /** Whether a chip's label runs to more than one line — taller than one and a half of
412
+ * its line height. A line height of `normal` (no number to compare) counts as one line. */
413
+ function labelWraps(label: HTMLElement): boolean {
414
+ const line = Number.parseFloat(getComputedStyle(label).lineHeight);
415
+ return line > 0 && label.clientHeight > 1.5 * line;
416
+ }
417
+
409
418
  function scheduleChipEdges() {
410
419
  if (snapFrame || typeof requestAnimationFrame === "undefined") return;
411
420
  snapFrame = requestAnimationFrame(() => {
@@ -489,12 +498,30 @@ function assignRef<T>(target: Ref<T> | undefined, value: T | null) {
489
498
  }
490
499
 
491
500
  const CHIP_PILL = "inline-flex max-w-full items-center rounded-full border transition-colors";
501
+
492
502
  // `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the
493
503
  // ChipInput moves focus onto one after a removal) and a ring that appears on a
494
504
  // pointer click reads as a stuck selection.
495
505
  const CHIP_RING = `${FOCUS_RING} focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]`;
496
506
  const CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;
497
507
 
508
+ /**
509
+ * A text chip's label at Large and Extra large (0.32.1, keksdose's 0.32 report;
510
+ * docs/text-size-harmonization.md §4 "nothing truncates"): it wraps instead of ending in
511
+ * "…". keksdose's "Zur Prüfung zurücklegen" read "Zur Prüfung zu…" on a phone at Extra
512
+ * large, which is a different instruction. No size has a fixed height (padding and
513
+ * `min-h-*` only), so the pill grows with its lines; the mark before the text stays
514
+ * centred on them, as in a pill. A `large:` class, so Normal keeps its one-line chip.
515
+ *
516
+ * `break-words`, not `[overflow-wrap:anywhere]`: a status chip often sits in a table
517
+ * cell, and `anywhere` lowers the label's min-content width to one letter, so an
518
+ * auto-sized column can squeeze it to a letter per line (StatusDot's label did, in the
519
+ * showcase's tone table at 360 px, Extra large). `break-word` keeps each word whole
520
+ * wherever the width is the content's to choose, and still breaks one that cannot fit
521
+ * the row at all.
522
+ */
523
+ const CHIP_LABEL_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
524
+
498
525
  /**
499
526
  * An interactive chip's touch target at Large and Extra large (docs/text-size-harmonization.md
500
527
  * §4: "touch targets at least 48 px … for buttons, inputs, chips"): a link or toggle chip
@@ -814,14 +841,15 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
814
841
  ) : (
815
842
  <Icon className={cn(s.icon, "shrink-0")} aria-hidden />
816
843
  ))}
817
- {/* Text truncates. Mixed children (an icon and a word) sit in a row instead: as a
818
- plain span, the svg — a block under Tailwind's preflight — stacked above the
819
- text (keksdose live #358). */}
844
+ {/* Text truncates at Normal and wraps at Large (CHIP_LABEL_WRAP_AT_LARGE). Mixed
845
+ children (an icon and a word) sit in a row instead: as a plain span, the svg —
846
+ a block under Tailwind's preflight — stacked above the text (keksdose live
847
+ #358). */}
820
848
  <span
821
849
  className={cn(
822
850
  "min-w-0",
823
851
  typeof children === "string" || typeof children === "number"
824
- ? "truncate"
852
+ ? cn("truncate", CHIP_LABEL_WRAP_AT_LARGE)
825
853
  : "inline-flex items-center gap-1 [&>svg]:size-[1em] [&>svg]:shrink-0",
826
854
  )}
827
855
  >
@@ -528,7 +528,7 @@ export function ChoiceCardGroup<T extends string>(props: ChoiceCardGroupProps<T>
528
528
  {required && <RequiredMark />}
529
529
  </legend>
530
530
  )}
531
- <div className={cn("grid gap-2 sm:grid-cols-2", className)}>
531
+ <div className={cn("grid grid-cols-1 gap-2 sm:grid-cols-2", className)}>
532
532
  {options.map((o) => (
533
533
  <ChoiceCard
534
534
  key={o.value}
@@ -3,6 +3,8 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from "./data-table-labels";
5
5
  import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
6
+ import { usePhoneLayout } from "../hooks/use-breakpoint";
7
+ import { useLargeText } from "../hooks/use-large-text";
6
8
 
7
9
  export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 200] as const;
8
10
 
@@ -32,8 +34,28 @@ interface PaginationProps {
32
34
  density?: "comfortable" | "compact";
33
35
  }
34
36
 
37
+ /**
38
+ * How many page numbers flank the current one on the strip: ±2, and ±1 on a phone at
39
+ * Large and Extra large (0.32.1, the 360 px Extra-large sweep; docs/text-size-
40
+ * harmonization.md §4). At 150 % a number button is 42 px, and "‹ 1 2 3 … 494 ›" was
41
+ * 312 px on a 360 px phone's 262; one neighbour fewer on each side saves two buttons.
42
+ */
43
+ const PAGE_WINDOW = 2;
44
+ const PAGE_WINDOW_LARGE_PHONE = 1;
45
+
46
+ /**
47
+ * The strip's two parts (0.32.1, same sweep, §4 "nothing overflows"). The arrows keep
48
+ * the ends; the numbers between them wrap, centred, when even the narrower window does
49
+ * not fit — a middle page of a long list ("1 … 249 250 251 … 494") at Extra large, or at
50
+ * Normal on a narrow phone, where the one-row strip ran past the screen. Where the
51
+ * strip fits it is one row, as before.
52
+ */
53
+ const PAGE_STRIP = "flex min-w-0 max-w-full items-center gap-1";
54
+ const PAGE_NUMBERS = "flex min-w-0 flex-wrap items-center justify-center gap-1";
55
+
35
56
  /** Footer for {@link DataTable}: range summary, page-size select, and a
36
- * windowed page-number strip (first/last + ±2 around the current page). */
57
+ * windowed page-number strip (first/last + ±2 around the current page, ±1 on a phone
58
+ * at Large — see {@link PAGE_WINDOW}). */
37
59
  export function Pagination({
38
60
  page,
39
61
  totalPages,
@@ -57,15 +79,20 @@ export function Pagination({
57
79
  // summary is not formatted here — `pageRange` / `rowCount` take NUMBERS so the
58
80
  // translation can format them itself, alongside the words around them.
59
81
  const num = useMemo(() => new Intl.NumberFormat(locale), [locale]);
82
+ const large = useLargeText();
83
+ const phone = usePhoneLayout();
84
+ const radius = large && phone ? PAGE_WINDOW_LARGE_PHONE : PAGE_WINDOW;
60
85
  const visible = useMemo(() => {
61
- if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i);
86
+ // Every page when that is no longer than the window would be (first, last, the
87
+ // current one, its neighbours and two ellipses).
88
+ if (totalPages <= 2 * radius + 3) return Array.from({ length: totalPages }, (_, i) => i);
62
89
  const set = new Set<number>([0, totalPages - 1, page]);
63
- for (let d = 1; d <= 2; d++) {
90
+ for (let d = 1; d <= radius; d++) {
64
91
  if (page - d >= 0) set.add(page - d);
65
92
  if (page + d <= totalPages - 1) set.add(page + d);
66
93
  }
67
94
  return Array.from(set).sort((a, b) => a - b);
68
- }, [page, totalPages]);
95
+ }, [page, totalPages, radius]);
69
96
 
70
97
  const items: (number | "ellipsis")[] = [];
71
98
  visible.forEach((p, idx) => {
@@ -108,7 +135,7 @@ export function Pagination({
108
135
  )}
109
136
  </div>
110
137
  {pageSize !== Infinity && totalPages > 1 && (
111
- <div className="flex items-center gap-1">
138
+ <div data-slot="page-strip" className={PAGE_STRIP}>
112
139
  <button
113
140
  type="button"
114
141
  onClick={() => onPage(Math.max(0, page - 1))}
@@ -119,32 +146,34 @@ export function Pagination({
119
146
  {/* "Previous" points back along the line — left in LTR, right in RTL. */}
120
147
  <ChevronLeft className={cn("rtl:-scale-x-100", compact ? "size-3.5" : "size-4")} />
121
148
  </button>
122
- {items.map((it, i) =>
123
- it === "ellipsis" ? (
124
- <span key={`e${i}`} className="px-1 text-[var(--text-placeholder)]">
125
- …
126
- </span>
127
- ) : (
128
- <button
129
- key={it}
130
- type="button"
131
- onClick={() => onPage(it)}
132
- // Which page you are on is carried only by a background colour
133
- // otherwise, and the strip is a row of bare numbers with nothing
134
- // else to tell them apart.
135
- aria-current={it === page ? "page" : undefined}
136
- className={cn(
137
- "rounded",
138
- compact ? "min-w-6 px-1.5 py-0" : "min-w-7 px-2 py-0.5",
139
- it === page
140
- ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
141
- : "hover:bg-[var(--bg-hover)]",
142
- )}
143
- >
144
- {num.format(it + 1)}
145
- </button>
146
- ),
147
- )}
149
+ <div data-slot="page-numbers" className={PAGE_NUMBERS}>
150
+ {items.map((it, i) =>
151
+ it === "ellipsis" ? (
152
+ <span key={`e${i}`} className="px-1 text-[var(--text-placeholder)]">
153
+ …
154
+ </span>
155
+ ) : (
156
+ <button
157
+ key={it}
158
+ type="button"
159
+ onClick={() => onPage(it)}
160
+ // Which page you are on is carried only by a background colour
161
+ // otherwise, and the strip is a row of bare numbers with nothing
162
+ // else to tell them apart.
163
+ aria-current={it === page ? "page" : undefined}
164
+ className={cn(
165
+ "rounded",
166
+ compact ? "min-w-6 px-1.5 py-0" : "min-w-7 px-2 py-0.5",
167
+ it === page
168
+ ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
169
+ : "hover:bg-[var(--bg-hover)]",
170
+ )}
171
+ >
172
+ {num.format(it + 1)}
173
+ </button>
174
+ ),
175
+ )}
176
+ </div>
148
177
  <button
149
178
  type="button"
150
179
  onClick={() => onPage(Math.min(totalPages - 1, page + 1))}
@@ -35,7 +35,7 @@ import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
35
35
  import { useOverlayHistory } from "../hooks/use-overlay-history";
36
36
  import { FullBleedDialog } from "./full-bleed-dialog";
37
37
  import { Popover } from "./popover";
38
- import { useBreakpoint } from "../hooks/use-breakpoint";
38
+ import { usePhoneLayout } from "../hooks/use-breakpoint";
39
39
  import { useAnnounce } from "../hooks/use-announce";
40
40
  import { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels, type DataTableLabels } from "./data-table-labels";
41
41
  import { useKitLabelOverrides, useKitLocale } from "../i18n/kit-labels";
@@ -1655,9 +1655,13 @@ export function DataTable<T>({
1655
1655
  : labels.columnsCount(visibleCount, totalCount);
1656
1656
  // Match the `md:` breakpoint at the text size in force: we render either the table or
1657
1657
  // the card list — never both — so we don't double up DOM nodes that screen readers and
1658
- // integration tests would have to disambiguate. The same answer as `usePhoneLayout()`
1659
- // (inverted), which an app's own phone checks use (§10.4).
1660
- const isMdUp = useBreakpoint("md", true);
1658
+ // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin
1659
+ // `useBreakpoint("md")`: the same query an app's own phone checks ask (keksdose's
1660
+ // sticky editor footer), so the card list and the full-screen row dialog switch with
1661
+ // them and never a frame apart (§10.4 "usePhoneLayout() is the single answer"; 0.32.1,
1662
+ // keksdose's 0.32 report, where `md` and `max-md` both missed at 767.5 px). Without
1663
+ // `matchMedia` (SSR, jsdom) it is the table, as before.
1664
+ const isMdUp = !usePhoneLayout();
1661
1665
  // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.
1662
1666
  const desktopScroller = useRef<HTMLDivElement | null>(null);
1663
1667
  const edgeFadeProps = useEdgeFade(desktopScroller, edgeFade && isMdUp);
@@ -206,6 +206,16 @@ function CustomTrigger({
206
206
  return render(attrs, valueId);
207
207
  }
208
208
 
209
+ /**
210
+ * The trigger's value wraps instead of truncating at Large and Extra large (0.32.1, the
211
+ * 360 px Extra-large sweep; docs/text-size-harmonization.md §4 "nothing truncates"). A
212
+ * range ("01/09/2026 – 30/09/2026") is 312 px at 150 %, and on a 360 px phone it read
213
+ * "01/09/2026 – 30/…" — its end, the half a reader needs, was the half cut off. It breaks
214
+ * at its spaces; a single date breaks only where it cannot fit at all. A `large:` class,
215
+ * so Normal keeps its one-line field.
216
+ */
217
+ const VALUE_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
218
+
209
219
  /**
210
220
  * The field-shaped button that opens the calendar.
211
221
  *
@@ -320,7 +330,7 @@ function DateFieldTrigger({
320
330
 
321
331
  return (
322
332
  <button {...attrs}>
323
- <span id={valueId} className={cn("truncate", !hasValue && "text-[var(--text-placeholder)]")}>
333
+ <span id={valueId} className={cn("truncate", VALUE_WRAP_AT_LARGE, !hasValue && "text-[var(--text-placeholder)]")}>
324
334
  {/* `|| " "` (a NON-BREAKING space) — triggerText is "" when there is no value and no
325
335
  placeholder was passed. An empty span has no line box, so the
326
336
  trigger collapses to its padding and sits shorter than every
@@ -558,6 +568,23 @@ function DateField({
558
568
  );
559
569
  }
560
570
 
571
+ /**
572
+ * The ‹ date › (today) row on a phone at Large and Extra large (0.32.1, the 360 px
573
+ * Extra-large sweep; docs/text-size-harmonization.md §4): the field takes the first line
574
+ * on its own, whole and with all four corners, and the buttons share the line under it
575
+ * as one joined strip. Three 2.5rem buttons are 180 px at 150 %, and on a 360 px phone
576
+ * they left the field 84 px — "Book… 0" for "Booking date 07/09/2026" — where a 360 px
577
+ * row would hold the date only by cutting it. `large:max-sm:`, the scaled phone width,
578
+ * so a tablet or a desktop at Large keeps the one row, and Normal is unchanged.
579
+ */
580
+ const STEP_ROW_STACK_AT_LARGE = cn(
581
+ "large:max-sm:flex-wrap large:max-sm:gap-y-2",
582
+ "large:max-sm:[&>[data-slot=date-step-field]]:order-first large:max-sm:[&>[data-slot=date-step-field]]:basis-full",
583
+ "large:max-sm:[&>[data-slot=date-step-field]]:ms-0 large:max-sm:[&_[role=combobox]]:rounded-md",
584
+ // The buttons (each in its tooltip's wrapper) share the line under the field.
585
+ "large:max-sm:[&>span]:flex-1 large:max-sm:[&>span>button]:w-full",
586
+ );
587
+
561
588
  /** One ‹ / › day-step button, sized to sit flush beside the field. */
562
589
  function StepButton({
563
590
  icon: Icon,
@@ -1105,6 +1132,9 @@ export function DatePicker(props: DatePickerProps) {
1105
1132
  disabled={disabled}
1106
1133
  invalid={messages.isInvalid}
1107
1134
  className={step ? "min-w-0 flex-1" : className}
1135
+ // Named for STEP_ROW_STACK_AT_LARGE; only in the row, so a caller's own
1136
+ // `data-slot` on a bare field is left alone.
1137
+ {...(wrapped ? { "data-slot": "date-step-field" } : {})}
1108
1138
  hasValue={Boolean(value)}
1109
1139
  triggerText={value ? render(value) : (placeholder ?? "")}
1110
1140
  onClear={() => onChange("")}
@@ -1173,6 +1203,7 @@ export function DatePicker(props: DatePickerProps) {
1173
1203
  // the trigger, which raising the trigger alone covered. Hover raises nothing: it
1174
1204
  // changes no border, and would let a hovered › cover a focused field's edge.
1175
1205
  "[&>*:focus-within]:z-20 [&>*:active]:z-20 [&>*:has([aria-invalid=true])]:z-10",
1206
+ STEP_ROW_STACK_AT_LARGE,
1176
1207
  className,
1177
1208
  )}
1178
1209
  >
@@ -155,6 +155,23 @@ function RenderEditor<T>({ render, ...props }: InlineEditorProps<T> & { render:
155
155
  return render(props);
156
156
  }
157
157
 
158
+ /**
159
+ * The value at Large and Extra large (0.32.1, keksdose's 0.32 report;
160
+ * docs/text-size-harmonization.md §4 "nothing truncates"): the display button, the
161
+ * locked one and the read-only text wrap instead of ending in "…", and break a word too
162
+ * long for the cell, so an IBAN or an e-mail address shows to its last character. The
163
+ * value IS the button's accessible name and the thing being edited: a reader who needs
164
+ * big type cannot recover a cut-off end by squinting. A `large:` class, so a table of
165
+ * these keeps its one-line cells at Normal.
166
+ *
167
+ * `break-words`, not `[overflow-wrap:anywhere]`: these live in table cells (keksdose's
168
+ * budget and invoice lines), and `anywhere` lowers the value's min-content width to one
169
+ * letter, so an auto-sized column could squeeze an amount to a digit per line. With
170
+ * `break-word` a column keeps each word whole; the button is `w-full`, so a word wider
171
+ * than a sized cell still breaks.
172
+ */
173
+ const VALUE_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
174
+
158
175
  /** `onCommit`'s result, if it is a promise. */
159
176
  function asPromise(result: unknown): Promise<unknown> | null {
160
177
  return result && typeof (result as Promise<unknown>).then === "function" ? (result as Promise<unknown>) : null;
@@ -415,7 +432,7 @@ export function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {
415
432
  let body: ReactNode;
416
433
  if (readOnly) {
417
434
  body = (
418
- <span {...private_} className={cn("block min-w-0 truncate", alignClass)}>
435
+ <span {...private_} className={cn("block min-w-0 truncate", VALUE_WRAP_AT_LARGE, alignClass)}>
419
436
  {content}
420
437
  </span>
421
438
  );
@@ -492,6 +509,7 @@ export function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {
492
509
  onClick={(e) => e.preventDefault()}
493
510
  className={cn(
494
511
  "block w-full min-w-0 cursor-not-allowed truncate rounded-sm focus:outline-none",
512
+ VALUE_WRAP_AT_LARGE,
495
513
  FOCUS_RING,
496
514
  alignClass,
497
515
  )}
@@ -516,6 +534,7 @@ export function InlineEditField<T = string>(props: InlineEditFieldProps<T>) {
516
534
  onClick={() => setOpen(true)}
517
535
  className={cn(
518
536
  "block w-full min-w-0 truncate rounded-sm underline-offset-2 hover:underline focus:outline-none",
537
+ VALUE_WRAP_AT_LARGE,
519
538
  FOCUS_RING,
520
539
  alignClass,
521
540
  )}
@@ -962,7 +962,12 @@ export function MonthPicker(props: MonthPickerProps) {
962
962
  </Popover>
963
963
  );
964
964
  return (
965
- <div {...wrapperRest} ref={rootRef} className={cn("flex min-w-0 items-center gap-2", className)}>
965
+ // `flex-wrap`: where the heading and the buttons do not fit one row, the buttons
966
+ // take the next line, at the end (0.32.1, the 360 px Extra-large sweep;
967
+ // docs/text-size-harmonization.md §4). In one row at 150 % "Today ‹ ›" left the
968
+ // heading 30 px on a 360 px phone, and "October 2026" was drawn under "Today".
969
+ // Where they fit, nothing changes.
970
+ <div {...wrapperRest} ref={rootRef} className={cn("flex min-w-0 flex-wrap items-center gap-2", className)}>
966
971
  {Heading ? <Heading className="m-0 min-w-0 text-lg font-semibold">{trigger}</Heading> : trigger}
967
972
  <div className="ms-auto flex shrink-0 items-center gap-1">
968
973
  {showToday && (
@@ -30,6 +30,16 @@ const TITLE: Record<PageHeaderSize, string> = {
30
30
  */
31
31
  export type PageHeaderMobileLayout = "stacked" | "inline";
32
32
 
33
+ /**
34
+ * `stacked` at Large and Extra large on a phone (0.32.2, kastlan's 0.32.1 report;
35
+ * docs/text-size-harmonization.md §4, as FormActions does): the actions take the full
36
+ * width, one under the other. Side by side in 240 px of layout, two buttons each wrapped
37
+ * their label onto three lines. `large:max-sm:`, the scaled phone width, so Normal and
38
+ * every wider screen keep their row; `inline` is for actions known to fit, and stays.
39
+ */
40
+ const STACK_ACTIONS_AT_LARGE =
41
+ "large:max-sm:flex-col large:max-sm:items-stretch large:max-sm:[&>*]:w-full";
42
+
33
43
  const ROW: Record<PageHeaderMobileLayout, string> = {
34
44
  stacked: "flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
35
45
  inline: "flex-row items-center justify-between gap-2 sm:gap-4",
@@ -232,7 +242,11 @@ export function PageHeader({
232
242
  {actions != null && (
233
243
  <div
234
244
  ref={actionsRef}
235
- className={cn("flex shrink-0 flex-wrap items-center gap-2", hasSecondary && "sm:order-2")}
245
+ className={cn(
246
+ "flex shrink-0 flex-wrap items-center gap-2",
247
+ hasSecondary && "sm:order-2",
248
+ mobileLayout === "stacked" && STACK_ACTIONS_AT_LARGE,
249
+ )}
236
250
  >
237
251
  {actions}
238
252
  </div>