@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
@@ -5,7 +5,7 @@ import { LegendEntry } from './toggle-legend.js';
5
5
  import { SeriesChartLabels } from './series-chart-labels.js';
6
6
  import { TimeTickUnit } from './series-chart-ticks.js';
7
7
  import { SeriesChartAxisBudget } from './series-chart-budget.js';
8
- import { c6 as ChartTooltipPlacement } from '../delete-account-setting-BvdQdbv3.js';
8
+ import { c6 as ChartTooltipPlacement } from '../delete-account-setting-Dy4kJgUm.js';
9
9
  import { ChartHeight } from './chart-height.js';
10
10
  import 'recharts';
11
11
  import './tooltip.js';
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { eY as SelectProps } from '../delete-account-setting-BvdQdbv3.js';
3
+ import { eY as SelectProps } from '../delete-account-setting-Dy4kJgUm.js';
4
4
  import { ToggleGroupBaseProps } from './toggle-group.js';
5
5
  import { ThemePreference } from '../theme/theme-store.js';
6
6
  import { TextSize } from '../theme/text-size.js';
@@ -136,8 +136,12 @@ interface ThemeSettingProps extends Omit<SelectProps, "value" | "onChange" | "ch
136
136
  * one segmented control instead of a select — system, light and dark side by side, the
137
137
  * choice visible without opening anything, in every app (keksdose offered light and dark
138
138
  * only; its store already knew "system"). A {@link ToggleGroup}'s props pass through —
139
- * `labelPlacement`, `disabledReason`, `commit`, `size` — minus the options and the value,
140
- * which this owns.
139
+ * `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the options
140
+ * and the value, which this owns.
141
+ *
142
+ * The options wrap rather than truncate (`overflow="wrap"`), as in its two siblings
143
+ * {@link TextSizeSetting} and {@link ContrastSetting} (0.32.1, Kurvenschmiede's 0.32
144
+ * report: "Syst… Li… D…" in a card at Extra large on a 360 px phone).
141
145
  *
142
146
  * Bind it to the PREFERENCE (`"system"` included), never to the resolved mode: on a
143
147
  * system-dark device a control bound to the mode shows "dark" and cannot say "follow the
@@ -7,11 +7,12 @@ import { CONTRAST_MODES } from "../theme/contrast.js";
7
7
  import { useAppearanceLabels } from "./appearance-labels.js";
8
8
  function ThemeSetting(props) {
9
9
  if (props.variant === "toggle") {
10
- const { variant: _variant2, value: value2, onChange: onChange2, label: label2, optionLabels: optionLabels2, ...rest2 } = props;
10
+ const { variant: _variant2, value: value2, onChange: onChange2, label: label2, optionLabels: optionLabels2, overflow = "wrap", ...rest2 } = props;
11
11
  return /* @__PURE__ */ jsx(
12
12
  ToggleGroup,
13
13
  {
14
14
  ...rest2,
15
+ overflow,
15
16
  label: label2,
16
17
  value: value2,
17
18
  onChange: onChange2,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport type { ToggleGroupBaseProps } from \"./toggle-group\";\nimport type { ThemePreference } from \"../theme/theme-store\";\nimport { TEXT_SIZES } from \"../theme/text-size\";\nimport type { TextSize } from \"../theme/text-size\";\nimport { CONTRAST_MODES } from \"../theme/contrast\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport { useAppearanceLabels } from \"./appearance-labels\";\nimport type { AppearanceLabels } from \"./appearance-labels\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs — and, since 0.32, the text size and\n * the contrast (docs/text-size-harmonization.md §6), whose words are the kit's own\n * `appearance` namespace — so they live in @hb/ui and the app just supplies the\n * current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\n/**\n * Everything a {@link Select} takes, minus the two this component owns: the value is a\n * {@link ThemePreference} rather than a string, and `onChange` hands over the preference\n * itself because no caller of this wants the event. The rest — `disabled`, `required`,\n * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes\n * this a settings FIELD rather than a fixed widget.\n */\nexport interface ThemeSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n /** The `Select` (default). See {@link ThemeSettingToggleProps} for `\"toggle\"`. */\n variant?: \"select\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\n/**\n * `variant=\"toggle\"` (0.31.0, docs/settings-harmonization.md §2.8): the three choices as\n * one segmented control instead of a select — system, light and dark side by side, the\n * choice visible without opening anything, in every app (keksdose offered light and dark\n * only; its store already knew \"system\"). A {@link ToggleGroup}'s props pass through —\n * `labelPlacement`, `disabledReason`, `commit`, `size` — minus the options and the value,\n * which this owns.\n *\n * Bind it to the PREFERENCE (`\"system\"` included), never to the resolved mode: on a\n * system-dark device a control bound to the mode shows \"dark\" and cannot say \"follow the\n * system\" (keksdose's control, §8).\n */\nexport interface ThemeSettingToggleProps\n extends Omit<ToggleGroupBaseProps<ThemePreference>, \"options\" | \"label\" | \"ariaLabel\"> {\n variant: \"toggle\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n /**\n * Names the group: the field's label, or the label above it (`labelPlacement`). Leave\n * it out inside a card already titled \"Theme\" and name the group with `aria-label` or\n * `aria-labelledby` instead, so the card does not say \"Theme\" twice.\n */\n label?: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting(props: ThemeSettingProps | ThemeSettingToggleProps) {\n if (props.variant === \"toggle\") {\n const { variant: _variant, value, onChange, label, optionLabels, ...rest } = props;\n return (\n <ToggleGroup<ThemePreference>\n {...rest}\n label={label}\n value={value}\n onChange={onChange}\n options={[\n { value: \"system\", label: optionLabels.system },\n { value: \"light\", label: optionLabels.light },\n { value: \"dark\", label: optionLabels.dark },\n ]}\n />\n );\n }\n const { variant: _variant, value, onChange, label, optionLabels, id, ...rest } = props;\n return (\n <Select\n {...rest}\n id={id}\n label={label}\n value={value}\n onChange={(e) => onChange(e.target.value as ThemePreference)}\n >\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\n/** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change\n * handler re-typed to the language CODE the caller stores. */\nexport interface LanguageSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps) {\n return (\n <Select {...rest} id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n\n/**\n * What {@link TextSizeSetting} and {@link ContrastSetting} take: a {@link ToggleGroup}'s\n * props — `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the\n * options and the value, which they own, as {@link ThemeSettingToggleProps} does.\n */\ntype AppearanceToggleProps<T extends string> = Omit<ToggleGroupBaseProps<T>, \"options\" | \"label\" | \"ariaLabel\"> & {\n value: T;\n onChange: (value: T) => void;\n /**\n * The field's visible label. Leave it out inside a row already titled with the\n * setting's name: the group is then named by `aria-label` / `aria-labelledby` if\n * given, else by the kit's own word (\"Text size\", \"Contrast\").\n */\n label?: ReactNode;\n /** The `appearance` namespace, for this instance. */\n labels?: Partial<AppearanceLabels>;\n};\n\nexport type TextSizeSettingProps = AppearanceToggleProps<TextSize>;\n\n/**\n * The text size (0.32, docs/text-size-harmonization.md §6): Normal, Large, Extra large as\n * one segmented control, in the core `appearance` group after the theme. Bind it to the\n * size IN FORCE — `useAccountAppearance().textSize` — and hand a pick to its\n * `pickTextSize`, which stores the device's choice and, signed in, the account's.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`): at Extra large on a 360 px\n * phone the group has 240 px, and \"Extra large\" cut to \"Extra l…\" would be the one\n * setting that cannot read its own value.\n */\nexport function TextSizeSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: TextSizeSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<TextSize>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.textSize)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={TEXT_SIZES.map((size) => ({ value: size, label: labels.textSizes[size] }))}\n />\n );\n}\n\nexport type ContrastSettingProps = AppearanceToggleProps<ContrastMode>;\n\n/**\n * More contrast (0.32, §5, §6): System, Standard, More as one segmented control, beside\n * the text size. Bind it to the stored MODE, \"system\" included — never to whether more\n * contrast is on at the moment, or a device that asks for more contrast would show\n * \"More\" and could not say \"follow the device\" (the theme's lesson, §8 of the settings\n * contract). `useAccountAppearance().contrast` is that mode.\n */\nexport function ContrastSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: ContrastSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<ContrastMode>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.contrast)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={CONTRAST_MODES.map((mode) => ({ value: mode, label: labels.contrastModes[mode] }))}\n />\n );\n}\n"],"mappings":";AA0EM,cAeF,YAfE;AAzEN,SAAS,cAAc;AAEvB,SAAS,mBAAmB;AAG5B,SAAS,kBAAkB;AAE3B,SAAS,sBAAsB;AAE/B,SAAS,2BAA2B;AA4D7B,SAAS,aAAa,OAAoD;AAC/E,MAAI,MAAM,YAAY,UAAU;AAC9B,UAAM,EAAE,SAASA,WAAU,OAAAC,QAAO,UAAAC,WAAU,OAAAC,QAAO,cAAAC,eAAc,GAAGC,MAAK,IAAI;AAC7E,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAGA;AAAA,QACJ,OAAOF;AAAA,QACP,OAAOF;AAAA,QACP,UAAUC;AAAA,QACV,SAAS;AAAA,UACP,EAAE,OAAO,UAAU,OAAOE,cAAa,OAAO;AAAA,UAC9C,EAAE,OAAO,SAAS,OAAOA,cAAa,MAAM;AAAA,UAC5C,EAAE,OAAO,QAAQ,OAAOA,cAAa,KAAK;AAAA,QAC5C;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,QAAM,EAAE,SAAS,UAAU,OAAO,UAAU,OAAO,cAAc,IAAI,GAAG,KAAK,IAAI;AACjF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB;AAAA,MAE3D;AAAA,4BAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,QAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,QAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA;AAAA;AAAA,EAC1C;AAEJ;AAaO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,IAAI,GAAG,KAAK,GAAyB;AACtG,SACE,oBAAC,UAAQ,GAAG,MAAM,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC3F,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;AAgCO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,UAAU,IAAI,EAAE,EAAE;AAAA;AAAA,EACpF;AAEJ;AAWO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,eAAe,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,cAAc,IAAI,EAAE,EAAE;AAAA;AAAA,EAC5F;AAEJ;","names":["_variant","value","onChange","label","optionLabels","rest"]}
1
+ {"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport type { ToggleGroupBaseProps } from \"./toggle-group\";\nimport type { ThemePreference } from \"../theme/theme-store\";\nimport { TEXT_SIZES } from \"../theme/text-size\";\nimport type { TextSize } from \"../theme/text-size\";\nimport { CONTRAST_MODES } from \"../theme/contrast\";\nimport type { ContrastMode } from \"../theme/contrast\";\nimport { useAppearanceLabels } from \"./appearance-labels\";\nimport type { AppearanceLabels } from \"./appearance-labels\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs — and, since 0.32, the text size and\n * the contrast (docs/text-size-harmonization.md §6), whose words are the kit's own\n * `appearance` namespace — so they live in @hb/ui and the app just supplies the\n * current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\n/**\n * Everything a {@link Select} takes, minus the two this component owns: the value is a\n * {@link ThemePreference} rather than a string, and `onChange` hands over the preference\n * itself because no caller of this wants the event. The rest — `disabled`, `required`,\n * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes\n * this a settings FIELD rather than a fixed widget.\n */\nexport interface ThemeSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n /** The `Select` (default). See {@link ThemeSettingToggleProps} for `\"toggle\"`. */\n variant?: \"select\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\n/**\n * `variant=\"toggle\"` (0.31.0, docs/settings-harmonization.md §2.8): the three choices as\n * one segmented control instead of a select — system, light and dark side by side, the\n * choice visible without opening anything, in every app (keksdose offered light and dark\n * only; its store already knew \"system\"). A {@link ToggleGroup}'s props pass through —\n * `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the options\n * and the value, which this owns.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`), as in its two siblings\n * {@link TextSizeSetting} and {@link ContrastSetting} (0.32.1, Kurvenschmiede's 0.32\n * report: \"Syst… Li… D…\" in a card at Extra large on a 360 px phone).\n *\n * Bind it to the PREFERENCE (`\"system\"` included), never to the resolved mode: on a\n * system-dark device a control bound to the mode shows \"dark\" and cannot say \"follow the\n * system\" (keksdose's control, §8).\n */\nexport interface ThemeSettingToggleProps\n extends Omit<ToggleGroupBaseProps<ThemePreference>, \"options\" | \"label\" | \"ariaLabel\"> {\n variant: \"toggle\";\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n /**\n * Names the group: the field's label, or the label above it (`labelPlacement`). Leave\n * it out inside a card already titled \"Theme\" and name the group with `aria-label` or\n * `aria-labelledby` instead, so the card does not say \"Theme\" twice.\n */\n label?: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting(props: ThemeSettingProps | ThemeSettingToggleProps) {\n if (props.variant === \"toggle\") {\n const { variant: _variant, value, onChange, label, optionLabels, overflow = \"wrap\", ...rest } = props;\n return (\n <ToggleGroup<ThemePreference>\n {...rest}\n overflow={overflow}\n label={label}\n value={value}\n onChange={onChange}\n options={[\n { value: \"system\", label: optionLabels.system },\n { value: \"light\", label: optionLabels.light },\n { value: \"dark\", label: optionLabels.dark },\n ]}\n />\n );\n }\n const { variant: _variant, value, onChange, label, optionLabels, id, ...rest } = props;\n return (\n <Select\n {...rest}\n id={id}\n label={label}\n value={value}\n onChange={(e) => onChange(e.target.value as ThemePreference)}\n >\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\n/** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change\n * handler re-typed to the language CODE the caller stores. */\nexport interface LanguageSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps) {\n return (\n <Select {...rest} id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n\n/**\n * What {@link TextSizeSetting} and {@link ContrastSetting} take: a {@link ToggleGroup}'s\n * props — `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the\n * options and the value, which they own, as {@link ThemeSettingToggleProps} does.\n */\ntype AppearanceToggleProps<T extends string> = Omit<ToggleGroupBaseProps<T>, \"options\" | \"label\" | \"ariaLabel\"> & {\n value: T;\n onChange: (value: T) => void;\n /**\n * The field's visible label. Leave it out inside a row already titled with the\n * setting's name: the group is then named by `aria-label` / `aria-labelledby` if\n * given, else by the kit's own word (\"Text size\", \"Contrast\").\n */\n label?: ReactNode;\n /** The `appearance` namespace, for this instance. */\n labels?: Partial<AppearanceLabels>;\n};\n\nexport type TextSizeSettingProps = AppearanceToggleProps<TextSize>;\n\n/**\n * The text size (0.32, docs/text-size-harmonization.md §6): Normal, Large, Extra large as\n * one segmented control, in the core `appearance` group after the theme. Bind it to the\n * size IN FORCE — `useAccountAppearance().textSize` — and hand a pick to its\n * `pickTextSize`, which stores the device's choice and, signed in, the account's.\n *\n * The options wrap rather than truncate (`overflow=\"wrap\"`): at Extra large on a 360 px\n * phone the group has 240 px, and \"Extra large\" cut to \"Extra l…\" would be the one\n * setting that cannot read its own value.\n */\nexport function TextSizeSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: TextSizeSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<TextSize>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.textSize)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={TEXT_SIZES.map((size) => ({ value: size, label: labels.textSizes[size] }))}\n />\n );\n}\n\nexport type ContrastSettingProps = AppearanceToggleProps<ContrastMode>;\n\n/**\n * More contrast (0.32, §5, §6): System, Standard, More as one segmented control, beside\n * the text size. Bind it to the stored MODE, \"system\" included — never to whether more\n * contrast is on at the moment, or a device that asks for more contrast would show\n * \"More\" and could not say \"follow the device\" (the theme's lesson, §8 of the settings\n * contract). `useAccountAppearance().contrast` is that mode.\n */\nexport function ContrastSetting({ value, onChange, label, labels: labelsProp, overflow = \"wrap\", ...rest }: ContrastSettingProps) {\n const labels = useAppearanceLabels(labelsProp);\n const named = label !== undefined && label !== null && label !== false;\n return (\n <ToggleGroup<ContrastMode>\n {...rest}\n aria-label={named || rest[\"aria-labelledby\"] ? rest[\"aria-label\"] : (rest[\"aria-label\"] ?? labels.contrast)}\n label={label}\n overflow={overflow}\n value={value}\n onChange={onChange}\n options={CONTRAST_MODES.map((mode) => ({ value: mode, label: labels.contrastModes[mode] }))}\n />\n );\n}\n"],"mappings":";AA8EM,cAgBF,YAhBE;AA7EN,SAAS,cAAc;AAEvB,SAAS,mBAAmB;AAG5B,SAAS,kBAAkB;AAE3B,SAAS,sBAAsB;AAE/B,SAAS,2BAA2B;AAgE7B,SAAS,aAAa,OAAoD;AAC/E,MAAI,MAAM,YAAY,UAAU;AAC9B,UAAM,EAAE,SAASA,WAAU,OAAAC,QAAO,UAAAC,WAAU,OAAAC,QAAO,cAAAC,eAAc,WAAW,QAAQ,GAAGC,MAAK,IAAI;AAChG,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAGA;AAAA,QACJ;AAAA,QACA,OAAOF;AAAA,QACP,OAAOF;AAAA,QACP,UAAUC;AAAA,QACV,SAAS;AAAA,UACP,EAAE,OAAO,UAAU,OAAOE,cAAa,OAAO;AAAA,UAC9C,EAAE,OAAO,SAAS,OAAOA,cAAa,MAAM;AAAA,UAC5C,EAAE,OAAO,QAAQ,OAAOA,cAAa,KAAK;AAAA,QAC5C;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,QAAM,EAAE,SAAS,UAAU,OAAO,UAAU,OAAO,cAAc,IAAI,GAAG,KAAK,IAAI;AACjF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB;AAAA,MAE3D;AAAA,4BAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,QAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,QAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA;AAAA;AAAA,EAC1C;AAEJ;AAaO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,IAAI,GAAG,KAAK,GAAyB;AACtG,SACE,oBAAC,UAAQ,GAAG,MAAM,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC3F,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;AAgCO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,UAAU,IAAI,EAAE,EAAE;AAAA;AAAA,EACpF;AAEJ;AAWO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,QAAQ,YAAY,WAAW,QAAQ,GAAG,KAAK,GAAyB;AAChI,QAAM,SAAS,oBAAoB,UAAU;AAC7C,QAAM,QAAQ,UAAU,UAAa,UAAU,QAAQ,UAAU;AACjE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,cAAY,SAAS,KAAK,iBAAiB,IAAI,KAAK,YAAY,IAAK,KAAK,YAAY,KAAK,OAAO;AAAA,MAClG;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,eAAe,IAAI,CAAC,UAAU,EAAE,OAAO,MAAM,OAAO,OAAO,cAAc,IAAI,EAAE,EAAE;AAAA;AAAA,EAC5F;AAEJ;","names":["_variant","value","onChange","label","optionLabels","rest"]}
@@ -1,7 +1,7 @@
1
1
  import 'react';
2
2
  import './account-chips.js';
3
3
  import './dialog-frame.js';
4
- export { cP as DEFAULT_SHARE_CARD_LABELS, eZ as ShareAddRequest, e_ as ShareCandidate, e$ as ShareCard, f0 as ShareCardLabels, f1 as ShareCardProps, f2 as ShareDialog, f3 as ShareDialogProps, f4 as ShareGrantee, f5 as SharePanel, f6 as SharePanelProps, f7 as SharePendingGrant, f8 as ShareRole } from '../delete-account-setting-BvdQdbv3.js';
4
+ export { cP as DEFAULT_SHARE_CARD_LABELS, eZ as ShareAddRequest, e_ as ShareCandidate, e$ as ShareCard, f0 as ShareCardLabels, f1 as ShareCardProps, f2 as ShareDialog, f3 as ShareDialogProps, f4 as ShareGrantee, f5 as SharePanel, f6 as SharePanelProps, f7 as SharePendingGrant, f8 as ShareRole } from '../delete-account-setting-Dy4kJgUm.js';
5
5
  import 'lucide-react';
6
6
  import '../lib/format.js';
7
7
  import './chip.js';
@@ -46,6 +46,7 @@ function ShareRoleChip({ role }) {
46
46
  const chip = /* @__PURE__ */ jsx(RoleChip, { value: role.key, roles: { [role.key]: role } });
47
47
  return role.description ? /* @__PURE__ */ jsx(Tooltip, { label: role.description, children: chip }) : chip;
48
48
  }
49
+ const ROW_CONTROLS = "ms-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-1";
49
50
  function LockedRoleChoice({ reason, children }) {
50
51
  return reason === void 0 ? children : /* @__PURE__ */ jsx(Tooltip, { label: reason, children });
51
52
  }
@@ -81,6 +82,7 @@ function SharePanel({
81
82
  const roleOf = (key) => roles.find((r) => r.key === key);
82
83
  const choosable = roles.length > 1;
83
84
  const canAdd = !readOnly && onAdd !== void 0;
85
+ const rowReason = canAdd ? "tooltip" : "auto";
84
86
  const run = async (key, action) => {
85
87
  setBusy(key);
86
88
  setFailure(null);
@@ -230,7 +232,7 @@ function SharePanel({
230
232
  ] }),
231
233
  (g.kind === "team" || g.email) && /* @__PURE__ */ jsx("div", { "data-private": true, className: "truncate text-xs text-[var(--text-muted)]", children: g.kind === "team" ? labels.team : g.email })
232
234
  ] }),
233
- (changeable || editable && onRemove) && /* @__PURE__ */ jsxs("div", { className: "ms-auto flex shrink-0 items-center gap-1", children: [
235
+ (changeable || editable && onRemove) && /* @__PURE__ */ jsxs("div", { className: ROW_CONTROLS, children: [
234
236
  changeable && /* @__PURE__ */ jsx(LockedRoleChoice, { reason: lock.locked ? lock.reason : void 0, children: /* @__PURE__ */ jsx(
235
237
  ToggleGroup,
236
238
  {
@@ -252,6 +254,7 @@ function SharePanel({
252
254
  commit: true,
253
255
  label: labels.remove,
254
256
  labelVisible: false,
257
+ disabledReasonDisplay: rowReason,
255
258
  pending: busy === key,
256
259
  disabled: busy !== null,
257
260
  onClick: async () => {
@@ -291,7 +294,7 @@ function SharePanel({
291
294
  /* @__PURE__ */ jsx(ShareRoleChip, { role: roleOf(p.role) })
292
295
  ] })
293
296
  ] }),
294
- (p.link || !readOnly && onRevokePending) && /* @__PURE__ */ jsxs("div", { className: "ms-auto flex shrink-0 items-center gap-1", children: [
297
+ (p.link || !readOnly && onRevokePending) && /* @__PURE__ */ jsxs("div", { className: ROW_CONTROLS, children: [
295
298
  p.link && /* @__PURE__ */ jsx(CopyButton, { size: "xs", tone: "muted", text: p.link, label: labels.copyLink }),
296
299
  !readOnly && onRevokePending && /* @__PURE__ */ jsx(
297
300
  IconButton,
@@ -301,6 +304,7 @@ function SharePanel({
301
304
  commit: true,
302
305
  label: labels.revokePending,
303
306
  labelVisible: false,
307
+ disabledReasonDisplay: rowReason,
304
308
  pending: busy === key,
305
309
  disabled: busy !== null,
306
310
  onClick: async () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/share-card.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Trash2, UserPlus, UsersRound } from \"lucide-react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Autocomplete } from \"./autocomplete\";\nimport type { ComboOption } from \"./combobox-core\";\nimport { Chip } from \"./chip\";\nimport { RoleChip } from \"./account-chips\";\nimport type { RoleDefinition } from \"./account-chips\";\nimport { useConfirm } from \"./confirm-dialog\";\nimport { CopyButton } from \"./copy-button\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport type { DialogFrameProps } from \"./dialog-frame\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { Tooltip } from \"./tooltip\";\nimport { Button, Card, IconButton, Spinner } from \"./ui\";\nimport { useWriteLock } from \"./write-lock\";\nimport type { CardProps } from \"./ui\";\n\n/**\n * Who holds one record, and the owner's every way of changing that — Kurvenschmiede's\n * `features/sharing/share-dialog.tsx` and keksdose's `features/budgets/budget-share-card.tsx`,\n * which are the same component twice (Kurvenschmiede's own doc comment says so).\n *\n * Presentational: the kit draws the rows, asks before a removal (the kit's\n * {@link useConfirm}), keeps the field and the busy state, and checks only that an\n * address is SHAPED like one. Everything that is policy stays with the app — who may\n * share, whether an address has an account (that is what makes a grant pending), what\n * a role permits, every request — through the callbacks, which may return a promise.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ShareCardLabels {\n /** ShareDialog's heading. */\n dialogTitle: string;\n /** ShareDialog's close button. */\n close: string;\n /** The add field's label. */\n email: string;\n /** The add field's label when `addWithoutEmail` lets it stay empty. */\n emailOptional: string;\n emailPlaceholder: string;\n /** Under the field when what was typed is not an address. */\n invalidEmail: string;\n /** Accessible name of the role choice beside the field. */\n role: string;\n /** Accessible name of one grantee's role choice. */\n roleOf: (name: string) => string;\n /** The add button. */\n add: string;\n /** Heading over the list. */\n whoHasAccess: string;\n /** The list is empty. */\n nobodyYet: string;\n /** Chip on a grant whose address has no account yet. */\n pending: string;\n /** A pending grant with no address — a link anybody holding it can redeem. */\n openInvite: string;\n copyLink: string;\n /** A candidate that is a team (its sublabel, and the line under a team row). */\n team: string;\n /** Under the field while a team is the one named. */\n teamHint: (name: string) => string;\n /** The remove button on a grantee. */\n remove: string;\n removeConfirm: (name: string) => string;\n /** The remove button on a pending grant. */\n revokePending: string;\n revokePendingConfirm: (name: string) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** Shown while `loading`. */\n loading: string;\n}\n\nexport const DEFAULT_SHARE_CARD_LABELS: ShareCardLabels = {\n dialogTitle: \"Share\",\n close: \"Close\",\n email: \"Email address\",\n emailOptional: \"Email address (optional)\",\n emailPlaceholder: \"name@example.com\",\n invalidEmail: \"Enter a complete email address.\",\n role: \"Role\",\n roleOf: (name) => `Role of ${name}`,\n add: \"Share\",\n whoHasAccess: \"Who has access\",\n nobodyYet: \"Nobody else has access yet.\",\n pending: \"Pending\",\n openInvite: \"Open invite link\",\n copyLink: \"Copy link\",\n team: \"Team\",\n teamHint: (name) => `Everyone in ${name} gets access.`,\n remove: \"Remove access\",\n removeConfirm: (name) => `Remove access for ${name}?`,\n revokePending: \"Withdraw invitation\",\n revokePendingConfirm: (name) => `Withdraw the invitation for ${name}?`,\n failed: \"That did not work. Please try again.\",\n loading: \"Loading…\",\n};\n\n// ── Data shapes ───────────────────────────────────────────────────────────────\n\n/**\n * One role of the app's vocabulary: Kurvenschmiede's viewer/editor, keksdose's guest.\n *\n * The admin roster's {@link RoleDefinition} (label, tone, icon) plus the key it is\n * filed under — an ARRAY here rather than the keyed `RoleVocabulary` record,\n * because the order is the order of the role choice's segments.\n */\nexport type ShareRole = RoleDefinition & {\n key: string;\n /** What the role allows — under the role choice once chosen, and the chip's tooltip. */\n description?: string;\n};\n\n/** Somebody (or a team) who holds the record now. */\nexport interface ShareGrantee {\n id: string | number;\n name: string;\n /** The second line. A team row says {@link ShareCardLabels.team} instead. */\n email?: string | null;\n /** A {@link ShareRole.key}. */\n role: string;\n /** `team`: the grant reaches everybody in it (Kurvenschmiede). Default `person`. */\n kind?: \"person\" | \"team\";\n /** Shown, but neither its role nor its access can be changed here (an owner row). */\n locked?: boolean;\n /** The app's own line under the row — keksdose's per-guest key delivery. */\n extra?: ReactNode;\n}\n\n/** A grant waiting for its address to have an account — or, with no address, an\n * open invite link (keksdose). */\nexport interface SharePendingGrant {\n id: string | number;\n email?: string | null;\n /** A {@link ShareRole.key}; left out when the app has only one role. */\n role?: string;\n /** The invite link, offered with a copy button. */\n link?: string;\n}\n\n/** A completion offered while typing: a past grantee, or one of the sharer's teams. */\nexport interface ShareCandidate {\n id: string | number;\n name: string;\n /** A person's address: taking the row fills the field with it, and the grant is\n * then an ordinary address grant. */\n email?: string | null;\n /** `team` (or a person with no address): taking the row names THIS candidate, and\n * `onAdd` receives it instead of an address. */\n kind?: \"person\" | \"team\";\n icon?: ReactNode;\n}\n\n/** What the add button asks for. Exactly one of `email` / `candidate` is set, except\n * under `addWithoutEmail`, where both may be null (an open invite). */\nexport interface ShareAddRequest {\n email: string | null;\n candidate: ShareCandidate | null;\n /** A {@link ShareRole.key}. */\n role: string;\n}\n\ntype MaybePromise = void | Promise<unknown>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SharePanelProps {\n grantees: ShareGrantee[];\n /** The role vocabulary, in display order. One role: no choice is offered and each\n * row shows its chip. */\n roles: ShareRole[];\n pending?: SharePendingGrant[];\n /** Completions for the field. Left out: the field is a plain address field. */\n candidates?: ShareCandidate[];\n /** The role a new grant starts with. Default: the first of `roles`. */\n defaultRole?: string;\n /** Resolve to clear the field; reject to keep it and show the error. Left out (or\n * `readOnly`): no add form. */\n onAdd?: (request: ShareAddRequest) => MaybePromise;\n /** Left out: roles are shown as chips. */\n onRoleChange?: (grantee: ShareGrantee, role: string) => MaybePromise;\n /** Called after the kit's confirm. Left out: no remove button. */\n onRemove?: (grantee: ShareGrantee) => MaybePromise;\n /** Called after the kit's confirm. Left out: no withdraw button. */\n onRevokePending?: (grant: SharePendingGrant) => MaybePromise;\n /** keksdose: an empty field is a request too — an invite link with no address. */\n addWithoutEmail?: boolean;\n /** Under the add field: whatever the app needs said there (Kurvenschmiede's \"your\n * pending grant does not let anybody register\"). A function receives the taken\n * candidate, so it can say something else while a team is named. */\n caption?: ReactNode | ((candidate: ShareCandidate | null) => ReactNode);\n /** Above everything — what sharing means here (keksdose's guest explanation). */\n intro?: ReactNode;\n /** The list is being fetched: a spinner instead of the rows. */\n loading?: boolean;\n /** The app's own error, shown in the error box (wins over a caught rejection). */\n error?: ReactNode;\n /** Turns a callback's rejection into words. Default {@link ShareCardLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /** Look, do not touch: no form, chips for roles, no remove buttons. Copying an\n * invite link still works. */\n readOnly?: boolean;\n /** Focus the field on mount — a dialog that exists to add somebody. */\n autoFocus?: boolean;\n labels?: Partial<ShareCardLabels>;\n}\n\n// Shape only. Whether the address exists, is allowed, or is the sharer's own is the\n// app's question — the field must never become a way to probe for accounts.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** The kit's {@link RoleChip}, so a role looks the same here as in the admin roster,\n * with the role's description in a tooltip. */\nfunction ShareRoleChip({ role }: { role: ShareRole | undefined }) {\n if (!role) return null;\n const chip = <RoleChip value={role.key} roles={{ [role.key]: role }} />;\n return role.description ? <Tooltip label={role.description}>{chip}</Tooltip> : chip;\n}\n\n/**\n * A row's role choice saves on change, so it is a commit that cannot take `commit`:\n * under a write lock it is disabled, and the lock's reason is put in the kit Tooltip\n * around it — the same sentence the locked buttons beside it give.\n */\nfunction LockedRoleChoice({ reason, children }: { reason: ReactNode; children: ReactNode }) {\n return reason === undefined ? children : <Tooltip label={reason}>{children}</Tooltip>;\n}\n\n/**\n * The body both forms share: the add form, then the list. Exported for an app that\n * frames it itself (a settings section, a sheet).\n */\nexport function SharePanel({\n grantees,\n roles,\n pending = [],\n candidates,\n defaultRole,\n onAdd,\n onRoleChange,\n onRemove,\n onRevokePending,\n addWithoutEmail = false,\n caption,\n intro,\n loading = false,\n error,\n formatError,\n readOnly = false,\n autoFocus,\n labels: labelsProp,\n}: SharePanelProps) {\n // The `shareCard` namespace: prop > `<UiKitProvider labels>` > English.\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, labelsProp);\n const confirm = useConfirm();\n // Every commit below opts into the surrounding WriteLockProvider with `commit`; the\n // role choices save on change, cannot take it, and read the lock themselves.\n const lock = useWriteLock();\n const [email, setEmail] = useState(\"\");\n // A candidate taken from the completion. Typing again lets it go: the field then\n // names an address again, and a chosen team beside free text would be two answers\n // to \"who\" (Kurvenschmiede's rule).\n const [target, setTarget] = useState<ShareCandidate | null>(null);\n const [newRole, setNewRole] = useState(defaultRole ?? roles[0]?.key ?? \"\");\n const [invalid, setInvalid] = useState(false);\n // One action at a time, for the whole panel: two in flight against one record's\n // grants can answer out of order. The key says which control draws the spinner.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n\n const roleOf = (key: string | undefined) => roles.find((r) => r.key === key);\n const choosable = roles.length > 1;\n const canAdd = !readOnly && onAdd !== undefined;\n\n const run = async (key: string, action: () => MaybePromise): Promise<boolean> => {\n setBusy(key);\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n setBusy(null);\n }\n };\n\n const address = email.trim();\n const empty = !target && address === \"\";\n\n const submit = async () => {\n // Enter in the field is a commit too — the button's lock does not see it.\n if (!onAdd || busy || lock.locked) return;\n if (empty && !addWithoutEmail) return;\n if (!target && address !== \"\" && !EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n const request: ShareAddRequest = {\n email: target ? null : address || null,\n candidate: target,\n role: newRole,\n };\n if (await run(\"add\", () => onAdd(request))) {\n setEmail(\"\");\n setTarget(null);\n }\n };\n\n const options: ComboOption<string>[] = (candidates ?? []).map((c) => ({\n value: String(c.id),\n label: c.name,\n sublabel: c.kind === \"team\" ? labels.team : (c.email ?? undefined),\n icon: c.icon ?? (c.kind === \"team\" ? <UsersRound className=\"size-4\" /> : undefined),\n }));\n\n // The completion is a convenience, not a search: an address that matches nobody is\n // the ordinary case (somebody new), so no \"No results\" box opens over the field —\n // and over its error. The same narrowing Autocomplete applies, asked first.\n const needle = (target ? \"\" : email).trim().toLowerCase();\n const offers =\n needle !== \"\" &&\n options.some((o) => o.label.toLowerCase().includes(needle) || (o.sublabel ?? \"\").toLowerCase().includes(needle));\n\n const captionNode = typeof caption === \"function\" ? caption(target) : caption;\n const shownError = error ?? failure;\n const nobody = grantees.length === 0 && pending.length === 0;\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-xs text-[var(--text-muted)]\">{intro}</div>}\n\n {canAdd && (\n <div className=\"space-y-2\">\n <div\n className={cn(\n \"grid grid-cols-1 gap-2\",\n choosable && \"sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start\",\n )}\n >\n <Autocomplete<string>\n label={addWithoutEmail ? labels.emailOptional : labels.email}\n placeholder={labels.emailPlaceholder}\n value={target ? target.name : email}\n options={options}\n open={offers ? undefined : false}\n fillOnSelect={false}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n disabled={busy !== null}\n error={invalid ? labels.invalidEmail : undefined}\n onChange={(text) => {\n setTarget(null);\n setInvalid(false);\n setFailure(null);\n setEmail(text);\n }}\n onSelect={(option) => {\n const chosen = (candidates ?? []).find((c) => String(c.id) === option.value);\n if (!chosen) return;\n setInvalid(false);\n if (chosen.kind !== \"team\" && chosen.email) {\n setTarget(null);\n setEmail(chosen.email);\n } else {\n setTarget(chosen);\n setEmail(\"\");\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") void submit();\n }}\n />\n {choosable && (\n <ToggleGroup<string>\n aria-label={labels.role}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={newRole}\n onChange={setNewRole}\n disabled={busy !== null}\n caption={\n roles.some((r) => r.description) ? (key: string) => roleOf(key)?.description : undefined\n }\n // Level with the floating field beside it, as the two apps do by hand.\n optionClassName=\"sm:min-h-10\"\n />\n )}\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n {target?.kind === \"team\" && <Caption>{labels.teamHint(target.name)}</Caption>}\n {captionNode && <Caption as=\"div\">{captionNode}</Caption>}\n </div>\n <Button\n variant=\"brand\"\n commit\n pending={busy === \"add\"}\n disabled={busy !== null || (empty && !addWithoutEmail)}\n onClick={() => void submit()}\n className=\"ms-auto\"\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.add}\n </Button>\n </div>\n </div>\n )}\n\n {shownError && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {shownError}\n </AlertBanner>\n )}\n\n {loading ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : (\n <div className=\"space-y-2\">\n <SectionLabel as=\"h4\">{labels.whoHasAccess}</SectionLabel>\n {nobody && <Caption>{labels.nobodyYet}</Caption>}\n <ul className=\"divide-y divide-[var(--border)]\">\n {grantees.map((g) => {\n const editable = !readOnly && !g.locked;\n const changeable = editable && onRoleChange !== undefined && choosable;\n const key = `grantee:${g.id}`;\n return (\n <li key={key} className=\"space-y-1.5 py-2 first:pt-0 last:pb-0\">\n {/* Wraps rather than squeezes: on a phone the controls drop under\n the name, full row, instead of truncating it to a letter. */}\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium text-[var(--text-primary)]\">\n {g.kind === \"team\" && <UsersRound className=\"size-4 shrink-0\" aria-hidden />}\n <span data-private className=\"truncate\">\n {g.name}\n </span>\n {!changeable && <ShareRoleChip role={roleOf(g.role)} />}\n </div>\n {(g.kind === \"team\" || g.email) && (\n <div data-private className=\"truncate text-xs text-[var(--text-muted)]\">\n {g.kind === \"team\" ? labels.team : g.email}\n </div>\n )}\n </div>\n {(changeable || (editable && onRemove)) && (\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {changeable && (\n <LockedRoleChoice reason={lock.locked ? lock.reason : undefined}>\n <ToggleGroup<string>\n size=\"sm\"\n aria-label={labels.roleOf(g.name)}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={g.role}\n disabled={busy !== null || lock.locked}\n onChange={(next) => {\n if (next !== g.role) void run(`role:${g.id}`, () => onRoleChange(g, next));\n }}\n />\n </LockedRoleChoice>\n )}\n {editable && onRemove && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n commit\n label={labels.remove}\n // The icon alone at every text size (§10.8): the row's one\n // control beside its role toggle, where \"Remove access\" as\n // text at Large would not fit a 240 px row. It carries a\n // spinner and the write lock, which a \"⋯\" menu entry\n // cannot, and its confirmation names the action in words.\n labelVisible={false}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.removeConfirm(g.name),\n confirmLabel: labels.remove,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRemove(g));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n {g.extra}\n </li>\n );\n })}\n {pending.map((p) => {\n const key = `pending:${p.id}`;\n const name = p.email || labels.openInvite;\n return (\n <li key={key} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div\n data-private={p.email ? true : undefined}\n className={cn(\n \"truncate text-sm\",\n p.email ? \"text-[var(--text-primary)]\" : \"text-[var(--text-muted)]\",\n )}\n >\n {name}\n </div>\n <div className=\"mt-0.5 flex flex-wrap items-center gap-1\">\n <Chip size=\"sm\" shape=\"square\" tone=\"warning\" caps>\n {labels.pending}\n </Chip>\n <ShareRoleChip role={roleOf(p.role)} />\n </div>\n </div>\n {(p.link || (!readOnly && onRevokePending)) && (\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {p.link && <CopyButton size=\"xs\" tone=\"muted\" text={p.link} label={labels.copyLink} />}\n {!readOnly && onRevokePending && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n commit\n label={labels.revokePending}\n // As the grantee's remove above: one action beside the\n // copy icon (which is an icon at every size), and the\n // confirmation says it in words.\n labelVisible={false}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.revokePendingConfirm(name),\n confirmLabel: labels.revokePending,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRevokePending(p));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n}\n\n// ── The two frames ────────────────────────────────────────────────────────────\n\nexport interface ShareCardProps extends SharePanelProps {\n /** A heading over the card. */\n title?: ReactNode;\n /** The Card's variant. Default `outline` — a block inside a section, as keksdose's\n * card under a budget row is. */\n variant?: CardProps[\"variant\"];\n className?: string;\n}\n\n/** {@link SharePanel} in a card — keksdose's inline form, under the row it shares. */\nexport function ShareCard({ title, variant = \"outline\", className, ...panel }: ShareCardProps) {\n return (\n <Card variant={variant} padding=\"sm\" className={cn(\"space-y-3\", className)}>\n {title && <h3 className=\"text-sm font-semibold text-[var(--text-primary)]\">{title}</h3>}\n <SharePanel {...panel} />\n </Card>\n );\n}\n\nexport interface ShareDialogProps\n extends SharePanelProps,\n Pick<DialogFrameProps, \"open\" | \"onClose\" | \"size\" | \"className\"> {\n /** The heading. Default {@link ShareCardLabels.dialogTitle}. */\n title?: ReactNode;\n /** Under the heading — what is being shared (Kurvenschmiede passes its name). */\n description?: ReactNode;\n}\n\n/**\n * {@link SharePanel} in the kit's {@link DialogFrame} with a Close button —\n * Kurvenschmiede's form. Sharing commits as it goes, so there is nothing to save or\n * cancel: Close is the only action.\n */\nexport function ShareDialog({ open, onClose, size, className, title, description, ...panel }: ShareDialogProps) {\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, panel.labels);\n return (\n <DialogFrame\n open={open}\n onClose={onClose}\n size={size}\n className={className}\n title={title ?? labels.dialogTitle}\n description={description}\n actions={(close) => (\n <Button variant=\"secondary\" onClick={close}>\n {labels.close}\n </Button>\n )}\n >\n <SharePanel {...panel} />\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/share-card.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Trash2, UserPlus, UsersRound } from \"lucide-react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Autocomplete } from \"./autocomplete\";\nimport type { ComboOption } from \"./combobox-core\";\nimport { Chip } from \"./chip\";\nimport { RoleChip } from \"./account-chips\";\nimport type { RoleDefinition } from \"./account-chips\";\nimport { useConfirm } from \"./confirm-dialog\";\nimport { CopyButton } from \"./copy-button\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport type { DialogFrameProps } from \"./dialog-frame\";\nimport { Caption, SectionLabel } from \"./text\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { Tooltip } from \"./tooltip\";\nimport { Button, Card, IconButton, Spinner } from \"./ui\";\nimport { useWriteLock } from \"./write-lock\";\nimport type { CardProps } from \"./ui\";\nimport type { DisabledReasonDisplay } from \"./field-parts\";\n\n/**\n * Who holds one record, and the owner's every way of changing that — Kurvenschmiede's\n * `features/sharing/share-dialog.tsx` and keksdose's `features/budgets/budget-share-card.tsx`,\n * which are the same component twice (Kurvenschmiede's own doc comment says so).\n *\n * Presentational: the kit draws the rows, asks before a removal (the kit's\n * {@link useConfirm}), keeps the field and the busy state, and checks only that an\n * address is SHAPED like one. Everything that is policy stays with the app — who may\n * share, whether an address has an account (that is what makes a grant pending), what\n * a role permits, every request — through the callbacks, which may return a promise.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ShareCardLabels {\n /** ShareDialog's heading. */\n dialogTitle: string;\n /** ShareDialog's close button. */\n close: string;\n /** The add field's label. */\n email: string;\n /** The add field's label when `addWithoutEmail` lets it stay empty. */\n emailOptional: string;\n emailPlaceholder: string;\n /** Under the field when what was typed is not an address. */\n invalidEmail: string;\n /** Accessible name of the role choice beside the field. */\n role: string;\n /** Accessible name of one grantee's role choice. */\n roleOf: (name: string) => string;\n /** The add button. */\n add: string;\n /** Heading over the list. */\n whoHasAccess: string;\n /** The list is empty. */\n nobodyYet: string;\n /** Chip on a grant whose address has no account yet. */\n pending: string;\n /** A pending grant with no address — a link anybody holding it can redeem. */\n openInvite: string;\n copyLink: string;\n /** A candidate that is a team (its sublabel, and the line under a team row). */\n team: string;\n /** Under the field while a team is the one named. */\n teamHint: (name: string) => string;\n /** The remove button on a grantee. */\n remove: string;\n removeConfirm: (name: string) => string;\n /** The remove button on a pending grant. */\n revokePending: string;\n revokePendingConfirm: (name: string) => string;\n /** A callback rejected and no `formatError` says better. */\n failed: string;\n /** Shown while `loading`. */\n loading: string;\n}\n\nexport const DEFAULT_SHARE_CARD_LABELS: ShareCardLabels = {\n dialogTitle: \"Share\",\n close: \"Close\",\n email: \"Email address\",\n emailOptional: \"Email address (optional)\",\n emailPlaceholder: \"name@example.com\",\n invalidEmail: \"Enter a complete email address.\",\n role: \"Role\",\n roleOf: (name) => `Role of ${name}`,\n add: \"Share\",\n whoHasAccess: \"Who has access\",\n nobodyYet: \"Nobody else has access yet.\",\n pending: \"Pending\",\n openInvite: \"Open invite link\",\n copyLink: \"Copy link\",\n team: \"Team\",\n teamHint: (name) => `Everyone in ${name} gets access.`,\n remove: \"Remove access\",\n removeConfirm: (name) => `Remove access for ${name}?`,\n revokePending: \"Withdraw invitation\",\n revokePendingConfirm: (name) => `Withdraw the invitation for ${name}?`,\n failed: \"That did not work. Please try again.\",\n loading: \"Loading…\",\n};\n\n// ── Data shapes ───────────────────────────────────────────────────────────────\n\n/**\n * One role of the app's vocabulary: Kurvenschmiede's viewer/editor, keksdose's guest.\n *\n * The admin roster's {@link RoleDefinition} (label, tone, icon) plus the key it is\n * filed under — an ARRAY here rather than the keyed `RoleVocabulary` record,\n * because the order is the order of the role choice's segments.\n */\nexport type ShareRole = RoleDefinition & {\n key: string;\n /** What the role allows — under the role choice once chosen, and the chip's tooltip. */\n description?: string;\n};\n\n/** Somebody (or a team) who holds the record now. */\nexport interface ShareGrantee {\n id: string | number;\n name: string;\n /** The second line. A team row says {@link ShareCardLabels.team} instead. */\n email?: string | null;\n /** A {@link ShareRole.key}. */\n role: string;\n /** `team`: the grant reaches everybody in it (Kurvenschmiede). Default `person`. */\n kind?: \"person\" | \"team\";\n /** Shown, but neither its role nor its access can be changed here (an owner row). */\n locked?: boolean;\n /** The app's own line under the row — keksdose's per-guest key delivery. */\n extra?: ReactNode;\n}\n\n/** A grant waiting for its address to have an account — or, with no address, an\n * open invite link (keksdose). */\nexport interface SharePendingGrant {\n id: string | number;\n email?: string | null;\n /** A {@link ShareRole.key}; left out when the app has only one role. */\n role?: string;\n /** The invite link, offered with a copy button. */\n link?: string;\n}\n\n/** A completion offered while typing: a past grantee, or one of the sharer's teams. */\nexport interface ShareCandidate {\n id: string | number;\n name: string;\n /** A person's address: taking the row fills the field with it, and the grant is\n * then an ordinary address grant. */\n email?: string | null;\n /** `team` (or a person with no address): taking the row names THIS candidate, and\n * `onAdd` receives it instead of an address. */\n kind?: \"person\" | \"team\";\n icon?: ReactNode;\n}\n\n/** What the add button asks for. Exactly one of `email` / `candidate` is set, except\n * under `addWithoutEmail`, where both may be null (an open invite). */\nexport interface ShareAddRequest {\n email: string | null;\n candidate: ShareCandidate | null;\n /** A {@link ShareRole.key}. */\n role: string;\n}\n\ntype MaybePromise = void | Promise<unknown>;\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\nexport interface SharePanelProps {\n grantees: ShareGrantee[];\n /** The role vocabulary, in display order. One role: no choice is offered and each\n * row shows its chip. */\n roles: ShareRole[];\n pending?: SharePendingGrant[];\n /** Completions for the field. Left out: the field is a plain address field. */\n candidates?: ShareCandidate[];\n /** The role a new grant starts with. Default: the first of `roles`. */\n defaultRole?: string;\n /** Resolve to clear the field; reject to keep it and show the error. Left out (or\n * `readOnly`): no add form. */\n onAdd?: (request: ShareAddRequest) => MaybePromise;\n /** Left out: roles are shown as chips. */\n onRoleChange?: (grantee: ShareGrantee, role: string) => MaybePromise;\n /** Called after the kit's confirm. Left out: no remove button. */\n onRemove?: (grantee: ShareGrantee) => MaybePromise;\n /** Called after the kit's confirm. Left out: no withdraw button. */\n onRevokePending?: (grant: SharePendingGrant) => MaybePromise;\n /** keksdose: an empty field is a request too — an invite link with no address. */\n addWithoutEmail?: boolean;\n /** Under the add field: whatever the app needs said there (Kurvenschmiede's \"your\n * pending grant does not let anybody register\"). A function receives the taken\n * candidate, so it can say something else while a team is named. */\n caption?: ReactNode | ((candidate: ShareCandidate | null) => ReactNode);\n /** Above everything — what sharing means here (keksdose's guest explanation). */\n intro?: ReactNode;\n /** The list is being fetched: a spinner instead of the rows. */\n loading?: boolean;\n /** The app's own error, shown in the error box (wins over a caught rejection). */\n error?: ReactNode;\n /** Turns a callback's rejection into words. Default {@link ShareCardLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /** Look, do not touch: no form, chips for roles, no remove buttons. Copying an\n * invite link still works. */\n readOnly?: boolean;\n /** Focus the field on mount — a dialog that exists to add somebody. */\n autoFocus?: boolean;\n labels?: Partial<ShareCardLabels>;\n}\n\n// Shape only. Whether the address exists, is allowed, or is the sharer's own is the\n// app's question — the field must never become a way to probe for accounts.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** The kit's {@link RoleChip}, so a role looks the same here as in the admin roster,\n * with the role's description in a tooltip. */\nfunction ShareRoleChip({ role }: { role: ShareRole | undefined }) {\n if (!role) return null;\n const chip = <RoleChip value={role.key} roles={{ [role.key]: role }} />;\n return role.description ? <Tooltip label={role.description}>{chip}</Tooltip> : chip;\n}\n\n/**\n * A row's controls: the role toggle and the remove icon, or a pending grant's copy and\n * revoke. Held to the row and wrapping inside it (0.32.1, the 360 px Extra-large sweep;\n * docs/text-size-harmonization.md §4 \"nothing overflows\"): under a write lock on a touch\n * screen or at Large the remove icon gives its reason as a line under it, up to 20rem\n * wide, and a `shrink-0` box ran 31 px past a 390 px phone at Normal and 269 px past a\n * 360 px one at Extra large. Where the controls fit in one row, nothing changes.\n */\nconst ROW_CONTROLS = \"ms-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-1\";\n\n/**\n * A row's role choice saves on change, so it is a commit that cannot take `commit`:\n * under a write lock it is disabled, and the lock's reason is put in the kit Tooltip\n * around it — the same sentence the locked buttons beside it give.\n */\nfunction LockedRoleChoice({ reason, children }: { reason: ReactNode; children: ReactNode }) {\n return reason === undefined ? children : <Tooltip label={reason}>{children}</Tooltip>;\n}\n\n/**\n * The body both forms share: the add form, then the list. Exported for an app that\n * frames it itself (a settings section, a sheet).\n */\nexport function SharePanel({\n grantees,\n roles,\n pending = [],\n candidates,\n defaultRole,\n onAdd,\n onRoleChange,\n onRemove,\n onRevokePending,\n addWithoutEmail = false,\n caption,\n intro,\n loading = false,\n error,\n formatError,\n readOnly = false,\n autoFocus,\n labels: labelsProp,\n}: SharePanelProps) {\n // The `shareCard` namespace: prop > `<UiKitProvider labels>` > English.\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, labelsProp);\n const confirm = useConfirm();\n // Every commit below opts into the surrounding WriteLockProvider with `commit`; the\n // role choices save on change, cannot take it, and read the lock themselves.\n const lock = useWriteLock();\n const [email, setEmail] = useState(\"\");\n // A candidate taken from the completion. Typing again lets it go: the field then\n // names an address again, and a chosen team beside free text would be two answers\n // to \"who\" (Kurvenschmiede's rule).\n const [target, setTarget] = useState<ShareCandidate | null>(null);\n const [newRole, setNewRole] = useState(defaultRole ?? roles[0]?.key ?? \"\");\n const [invalid, setInvalid] = useState(false);\n // One action at a time, for the whole panel: two in flight against one record's\n // grants can answer out of order. The key says which control draws the spinner.\n const [busy, setBusy] = useState<string | null>(null);\n const [failure, setFailure] = useState<ReactNode>(null);\n\n const roleOf = (key: string | undefined) => roles.find((r) => r.key === key);\n const choosable = roles.length > 1;\n const canAdd = !readOnly && onAdd !== undefined;\n // Under a write lock the form's Share button gives the lock's sentence as a line (on\n // touch and at Large). A row's remove or revoke icon then keeps it in its tooltip, as\n // the role toggle beside it does, rather than repeat the same line under every row\n // (0.32.1, the 360 px Extra-large sweep). Without the form the rows' lines are the\n // only place it is said, so they stay — and ROW_CONTROLS wraps them.\n const rowReason: DisabledReasonDisplay = canAdd ? \"tooltip\" : \"auto\";\n\n const run = async (key: string, action: () => MaybePromise): Promise<boolean> => {\n setBusy(key);\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n setBusy(null);\n }\n };\n\n const address = email.trim();\n const empty = !target && address === \"\";\n\n const submit = async () => {\n // Enter in the field is a commit too — the button's lock does not see it.\n if (!onAdd || busy || lock.locked) return;\n if (empty && !addWithoutEmail) return;\n if (!target && address !== \"\" && !EMAIL_SHAPE.test(address)) {\n setInvalid(true);\n return;\n }\n const request: ShareAddRequest = {\n email: target ? null : address || null,\n candidate: target,\n role: newRole,\n };\n if (await run(\"add\", () => onAdd(request))) {\n setEmail(\"\");\n setTarget(null);\n }\n };\n\n const options: ComboOption<string>[] = (candidates ?? []).map((c) => ({\n value: String(c.id),\n label: c.name,\n sublabel: c.kind === \"team\" ? labels.team : (c.email ?? undefined),\n icon: c.icon ?? (c.kind === \"team\" ? <UsersRound className=\"size-4\" /> : undefined),\n }));\n\n // The completion is a convenience, not a search: an address that matches nobody is\n // the ordinary case (somebody new), so no \"No results\" box opens over the field —\n // and over its error. The same narrowing Autocomplete applies, asked first.\n const needle = (target ? \"\" : email).trim().toLowerCase();\n const offers =\n needle !== \"\" &&\n options.some((o) => o.label.toLowerCase().includes(needle) || (o.sublabel ?? \"\").toLowerCase().includes(needle));\n\n const captionNode = typeof caption === \"function\" ? caption(target) : caption;\n const shownError = error ?? failure;\n const nobody = grantees.length === 0 && pending.length === 0;\n\n return (\n <div className=\"space-y-3\">\n {intro && <div className=\"text-xs text-[var(--text-muted)]\">{intro}</div>}\n\n {canAdd && (\n <div className=\"space-y-2\">\n <div\n className={cn(\n \"grid grid-cols-1 gap-2\",\n choosable && \"sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start\",\n )}\n >\n <Autocomplete<string>\n label={addWithoutEmail ? labels.emailOptional : labels.email}\n placeholder={labels.emailPlaceholder}\n value={target ? target.name : email}\n options={options}\n open={offers ? undefined : false}\n fillOnSelect={false}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.\n autoFocus={autoFocus}\n inputMode=\"email\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n disabled={busy !== null}\n error={invalid ? labels.invalidEmail : undefined}\n onChange={(text) => {\n setTarget(null);\n setInvalid(false);\n setFailure(null);\n setEmail(text);\n }}\n onSelect={(option) => {\n const chosen = (candidates ?? []).find((c) => String(c.id) === option.value);\n if (!chosen) return;\n setInvalid(false);\n if (chosen.kind !== \"team\" && chosen.email) {\n setTarget(null);\n setEmail(chosen.email);\n } else {\n setTarget(chosen);\n setEmail(\"\");\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") void submit();\n }}\n />\n {choosable && (\n <ToggleGroup<string>\n aria-label={labels.role}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={newRole}\n onChange={setNewRole}\n disabled={busy !== null}\n caption={\n roles.some((r) => r.description) ? (key: string) => roleOf(key)?.description : undefined\n }\n // Level with the floating field beside it, as the two apps do by hand.\n optionClassName=\"sm:min-h-10\"\n />\n )}\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1 basis-48 space-y-1\">\n {target?.kind === \"team\" && <Caption>{labels.teamHint(target.name)}</Caption>}\n {captionNode && <Caption as=\"div\">{captionNode}</Caption>}\n </div>\n <Button\n variant=\"brand\"\n commit\n pending={busy === \"add\"}\n disabled={busy !== null || (empty && !addWithoutEmail)}\n onClick={() => void submit()}\n className=\"ms-auto\"\n >\n <UserPlus className=\"size-4\" aria-hidden />\n {labels.add}\n </Button>\n </div>\n </div>\n )}\n\n {shownError && (\n <AlertBanner tone=\"danger\" size=\"sm\">\n {shownError}\n </AlertBanner>\n )}\n\n {loading ? (\n <div className=\"flex justify-center py-3\">\n <Spinner label={labels.loading} />\n </div>\n ) : (\n <div className=\"space-y-2\">\n <SectionLabel as=\"h4\">{labels.whoHasAccess}</SectionLabel>\n {nobody && <Caption>{labels.nobodyYet}</Caption>}\n <ul className=\"divide-y divide-[var(--border)]\">\n {grantees.map((g) => {\n const editable = !readOnly && !g.locked;\n const changeable = editable && onRoleChange !== undefined && choosable;\n const key = `grantee:${g.id}`;\n return (\n <li key={key} className=\"space-y-1.5 py-2 first:pt-0 last:pb-0\">\n {/* Wraps rather than squeezes: on a phone the controls drop under\n the name, full row, instead of truncating it to a letter. */}\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium text-[var(--text-primary)]\">\n {g.kind === \"team\" && <UsersRound className=\"size-4 shrink-0\" aria-hidden />}\n <span data-private className=\"truncate\">\n {g.name}\n </span>\n {!changeable && <ShareRoleChip role={roleOf(g.role)} />}\n </div>\n {(g.kind === \"team\" || g.email) && (\n <div data-private className=\"truncate text-xs text-[var(--text-muted)]\">\n {g.kind === \"team\" ? labels.team : g.email}\n </div>\n )}\n </div>\n {(changeable || (editable && onRemove)) && (\n <div className={ROW_CONTROLS}>\n {changeable && (\n <LockedRoleChoice reason={lock.locked ? lock.reason : undefined}>\n <ToggleGroup<string>\n size=\"sm\"\n aria-label={labels.roleOf(g.name)}\n options={roles.map((r) => ({ value: r.key, label: r.label }))}\n value={g.role}\n disabled={busy !== null || lock.locked}\n onChange={(next) => {\n if (next !== g.role) void run(`role:${g.id}`, () => onRoleChange(g, next));\n }}\n />\n </LockedRoleChoice>\n )}\n {editable && onRemove && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n commit\n label={labels.remove}\n // The icon alone at every text size (§10.8): the row's one\n // control beside its role toggle, where \"Remove access\" as\n // text at Large would not fit a 240 px row. It carries a\n // spinner and the write lock, which a \"⋯\" menu entry\n // cannot, and its confirmation names the action in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.removeConfirm(g.name),\n confirmLabel: labels.remove,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRemove(g));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n {g.extra}\n </li>\n );\n })}\n {pending.map((p) => {\n const key = `pending:${p.id}`;\n const name = p.email || labels.openInvite;\n return (\n <li key={key} className=\"py-2 first:pt-0 last:pb-0\">\n <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5\">\n <div className=\"min-w-0 flex-1 basis-40\">\n <div\n data-private={p.email ? true : undefined}\n className={cn(\n \"truncate text-sm\",\n p.email ? \"text-[var(--text-primary)]\" : \"text-[var(--text-muted)]\",\n )}\n >\n {name}\n </div>\n <div className=\"mt-0.5 flex flex-wrap items-center gap-1\">\n <Chip size=\"sm\" shape=\"square\" tone=\"warning\" caps>\n {labels.pending}\n </Chip>\n <ShareRoleChip role={roleOf(p.role)} />\n </div>\n </div>\n {(p.link || (!readOnly && onRevokePending)) && (\n <div className={ROW_CONTROLS}>\n {p.link && <CopyButton size=\"xs\" tone=\"muted\" text={p.link} label={labels.copyLink} />}\n {!readOnly && onRevokePending && (\n <IconButton\n tone=\"danger\"\n size=\"xs\"\n commit\n label={labels.revokePending}\n // As the grantee's remove above: one action beside the\n // copy icon (which is an icon at every size), and the\n // confirmation says it in words.\n labelVisible={false}\n disabledReasonDisplay={rowReason}\n pending={busy === key}\n disabled={busy !== null}\n onClick={async () => {\n const yes = await confirm({\n title: labels.revokePendingConfirm(name),\n confirmLabel: labels.revokePending,\n tone: \"danger\",\n });\n if (yes) await run(key, () => onRevokePending(p));\n }}\n >\n <Trash2 />\n </IconButton>\n )}\n </div>\n )}\n </div>\n </li>\n );\n })}\n </ul>\n </div>\n )}\n </div>\n );\n}\n\n// ── The two frames ────────────────────────────────────────────────────────────\n\nexport interface ShareCardProps extends SharePanelProps {\n /** A heading over the card. */\n title?: ReactNode;\n /** The Card's variant. Default `outline` — a block inside a section, as keksdose's\n * card under a budget row is. */\n variant?: CardProps[\"variant\"];\n className?: string;\n}\n\n/** {@link SharePanel} in a card — keksdose's inline form, under the row it shares. */\nexport function ShareCard({ title, variant = \"outline\", className, ...panel }: ShareCardProps) {\n return (\n <Card variant={variant} padding=\"sm\" className={cn(\"space-y-3\", className)}>\n {title && <h3 className=\"text-sm font-semibold text-[var(--text-primary)]\">{title}</h3>}\n <SharePanel {...panel} />\n </Card>\n );\n}\n\nexport interface ShareDialogProps\n extends SharePanelProps,\n Pick<DialogFrameProps, \"open\" | \"onClose\" | \"size\" | \"className\"> {\n /** The heading. Default {@link ShareCardLabels.dialogTitle}. */\n title?: ReactNode;\n /** Under the heading — what is being shared (Kurvenschmiede passes its name). */\n description?: ReactNode;\n}\n\n/**\n * {@link SharePanel} in the kit's {@link DialogFrame} with a Close button —\n * Kurvenschmiede's form. Sharing commits as it goes, so there is nothing to save or\n * cancel: Close is the only action.\n */\nexport function ShareDialog({ open, onClose, size, className, title, description, ...panel }: ShareDialogProps) {\n const labels = useKitLabels(\"shareCard\", DEFAULT_SHARE_CARD_LABELS, panel.labels);\n return (\n <DialogFrame\n open={open}\n onClose={onClose}\n size={size}\n className={className}\n title={title ?? labels.dialogTitle}\n description={description}\n actions={(close) => (\n <Button variant=\"secondary\" onClick={close}>\n {labels.close}\n </Button>\n )}\n >\n <SharePanel {...panel} />\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
@@ -35,6 +35,12 @@ function statusDotColor(tone) {
35
35
  return COLOR[tone];
36
36
  }
37
37
  const SIZE = { sm: "size-2", md: "size-2.5", lg: "size-3" };
38
+ const LABEL_WRAP_AT_LARGE = "large:whitespace-normal large:break-words";
39
+ const DOT_ON_FIRST_LINE = {
40
+ sm: "large:mt-[calc((1lh_-_0.5rem)/2)]",
41
+ md: "large:mt-[calc((1lh_-_0.625rem)/2)]",
42
+ lg: "large:mt-[calc((1lh_-_0.75rem)/2)]"
43
+ };
38
44
  function StatusDot({
39
45
  tone = "neutral",
40
46
  size = "sm",
@@ -53,6 +59,7 @@ function StatusDot({
53
59
  SIZE[size],
54
60
  FILL[tone],
55
61
  ring && "ring-2 ring-[var(--bg-surface)]",
62
+ label != null && DOT_ON_FIRST_LINE[size],
56
63
  label == null && className,
57
64
  dotClassName
58
65
  ),
@@ -63,9 +70,9 @@ function StatusDot({
63
70
  return (
64
71
  // No `aria-label` here: the visible label is the text, and a name on a role-less
65
72
  // span is one ARIA forbids and screen readers ignore.
66
- /* @__PURE__ */ jsxs("span", { ...rest, className: cn("inline-flex min-w-0 items-center gap-1.5 text-sm", className), children: [
73
+ /* @__PURE__ */ jsxs("span", { ...rest, className: cn("inline-flex min-w-0 items-center gap-1.5 text-sm large:items-start", className), children: [
67
74
  dot,
68
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: label })
75
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate", LABEL_WRAP_AT_LARGE), children: label })
69
76
  ] })
70
77
  );
71
78
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/status-dot.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { ProgressBarTone } from \"./progress-bar\";\nimport type { ChipHue } from \"./chip\";\n\n/**\n * The tones are `ProgressBar`'s, name for name — which are `Chip`'s, less nothing — so a\n * dot in a legend, the bar it explains and the badge beside it can be given the same\n * word and agree. The money pair is here for the same reason it is there: a legend\n * swatch for income is money, not \"success\". Chip's categorical hues are here too\n * (0.11): a unit-status dot in a combobox (kastlan lease-unit-step.tsx:35, \"rented\"\n * is blue) must match the Chip that states the same status elsewhere.\n */\nexport type StatusDotTone = ProgressBarTone | ChipHue;\n\n/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's\n * unread dot), `lg` 12px (kastlan's occupancy legend swatch). */\nexport type StatusDotSize = \"sm\" | \"md\" | \"lg\";\n\nconst FILL: Record<StatusDotTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n blue: \"bg-[var(--hue-blue)]\",\n indigo: \"bg-[var(--hue-indigo)]\",\n purple: \"bg-[var(--hue-purple)]\",\n teal: \"bg-[var(--hue-teal)]\",\n orange: \"bg-[var(--hue-orange)]\",\n};\n\n/**\n * The colour of each tone as a CSS value — the same `var(--…)` the dot's class paints\n * with. For a mark the kit does not draw: a `SwatchPicker` option (`color`), a chart\n * series, an inline `style`. keksdose's category picker spelled `var(--hue-teal)` out by\n * hand to match the dot beside the category name; a renamed token would have parted\n * them silently. (The class table above stays literal: Tailwind finds classes by\n * reading the source, so `bg-[${…}]` would never be generated. A test keeps the two\n * tables agreeing.)\n */\nconst COLOR: Record<StatusDotTone, string> = {\n brand: \"var(--brand)\",\n neutral: \"var(--text-muted)\",\n success: \"var(--success)\",\n warning: \"var(--warning)\",\n danger: \"var(--danger)\",\n info: \"var(--info)\",\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n blue: \"var(--hue-blue)\",\n indigo: \"var(--hue-indigo)\",\n purple: \"var(--hue-purple)\",\n teal: \"var(--hue-teal)\",\n orange: \"var(--hue-orange)\",\n};\n\n/** The CSS colour a {@link StatusDot} of `tone` is filled with — `statusDotColor(\"teal\")`\n * is `\"var(--hue-teal)\"`. See the table above for when to reach for it. */\nexport function statusDotColor(tone: StatusDotTone): string {\n return COLOR[tone];\n}\n\nconst SIZE: Record<StatusDotSize, string> = { sm: \"size-2\", md: \"size-2.5\", lg: \"size-3\" };\n\nexport interface StatusDotProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n tone?: StatusDotTone;\n size?: StatusDotSize;\n /**\n * Visible text beside the dot — a legend entry (\"Rented\") or a status (\"Online\"). It\n * is also what a screen reader reads: the dot itself is decorative whenever there is\n * a label.\n *\n * Without a label the dot is hidden from assistive technology, because a coloured\n * circle says nothing to someone who cannot see it. Say the state where it IS read —\n * keksdose's avatar trigger puts \"3 unread\" in its own `aria-label` — or give the dot\n * an `aria-label` of its own, which makes it an image with that name.\n */\n label?: ReactNode;\n /**\n * A ring in the surface colour round the dot, so a dot laid over the corner of an\n * icon or an avatar reads as a separate mark and not a smudge (keksdose top-bar.tsx\n * :152, account-menu.tsx ~385, both hand-drawing `ring-2 ring-white\n * dark:ring-slate-900`). Position it with `className` (`absolute -end-0.5 -top-0.5`).\n */\n ring?: boolean;\n /** Extra classes for the dot itself; `className` goes on the outer element. */\n dotClassName?: string;\n}\n\n/**\n * A small token-coloured dot, with an optional label.\n *\n * kastlan's reporting dashboard (dashboard-tab.tsx:79) paints its occupancy legend\n * with raw `bg-green-500` / `bg-red-500` / `bg-yellow-500`, which ignores the palette\n * and the dark theme; keksdose draws two unread dots by hand in `rose-500`. Both are\n * this.\n */\nexport function StatusDot({\n tone = \"neutral\",\n size = \"sm\",\n label,\n ring = false,\n className,\n dotClassName,\n \"aria-label\": ariaLabel,\n ...rest\n}: StatusDotProps) {\n const dot = (\n <span\n className={cn(\n \"inline-block shrink-0 rounded-full\",\n SIZE[size],\n FILL[tone],\n ring && \"ring-2 ring-[var(--bg-surface)]\",\n label == null && className,\n dotClassName,\n )}\n {...(label == null\n ? ariaLabel\n ? { role: \"img\", \"aria-label\": ariaLabel, ...rest }\n : { \"aria-hidden\": true, ...rest }\n : { \"aria-hidden\": true })}\n />\n );\n if (label == null) return dot;\n return (\n // No `aria-label` here: the visible label is the text, and a name on a role-less\n // span is one ARIA forbids and screen readers ignore.\n <span {...rest} className={cn(\"inline-flex min-w-0 items-center gap-1.5 text-sm\", className)}>\n {dot}\n <span className=\"min-w-0 truncate\">{label}</span>\n </span>\n );\n}\n"],"mappings":";AAgHI,cAoBA,YApBA;AA/GJ,SAAS,UAAU;AAkBnB,MAAM,OAAsC;AAAA,EAC1C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAWA,MAAM,QAAuC;AAAA,EAC3C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAIO,SAAS,eAAe,MAA6B;AAC1D,SAAO,MAAM,IAAI;AACnB;AAEA,MAAM,OAAsC,EAAE,IAAI,UAAU,IAAI,YAAY,IAAI,SAAS;AAmClF,SAAS,UAAU;AAAA,EACxB,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAmB;AACjB,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT,KAAK,IAAI;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB;AAAA,MACF;AAAA,MACC,GAAI,SAAS,OACV,YACE,EAAE,MAAM,OAAO,cAAc,WAAW,GAAG,KAAK,IAChD,EAAE,eAAe,MAAM,GAAG,KAAK,IACjC,EAAE,eAAe,KAAK;AAAA;AAAA,EAC5B;AAEF,MAAI,SAAS,KAAM,QAAO;AAC1B;AAAA;AAAA;AAAA,IAGE,qBAAC,UAAM,GAAG,MAAM,WAAW,GAAG,oDAAoD,SAAS,GACxF;AAAA;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,iBAAM;AAAA,OAC5C;AAAA;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/status-dot.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { ProgressBarTone } from \"./progress-bar\";\nimport type { ChipHue } from \"./chip\";\n\n/**\n * The tones are `ProgressBar`'s, name for name — which are `Chip`'s, less nothing — so a\n * dot in a legend, the bar it explains and the badge beside it can be given the same\n * word and agree. The money pair is here for the same reason it is there: a legend\n * swatch for income is money, not \"success\". Chip's categorical hues are here too\n * (0.11): a unit-status dot in a combobox (kastlan lease-unit-step.tsx:35, \"rented\"\n * is blue) must match the Chip that states the same status elsewhere.\n */\nexport type StatusDotTone = ProgressBarTone | ChipHue;\n\n/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's\n * unread dot), `lg` 12px (kastlan's occupancy legend swatch). */\nexport type StatusDotSize = \"sm\" | \"md\" | \"lg\";\n\nconst FILL: Record<StatusDotTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n blue: \"bg-[var(--hue-blue)]\",\n indigo: \"bg-[var(--hue-indigo)]\",\n purple: \"bg-[var(--hue-purple)]\",\n teal: \"bg-[var(--hue-teal)]\",\n orange: \"bg-[var(--hue-orange)]\",\n};\n\n/**\n * The colour of each tone as a CSS value — the same `var(--…)` the dot's class paints\n * with. For a mark the kit does not draw: a `SwatchPicker` option (`color`), a chart\n * series, an inline `style`. keksdose's category picker spelled `var(--hue-teal)` out by\n * hand to match the dot beside the category name; a renamed token would have parted\n * them silently. (The class table above stays literal: Tailwind finds classes by\n * reading the source, so `bg-[${…}]` would never be generated. A test keeps the two\n * tables agreeing.)\n */\nconst COLOR: Record<StatusDotTone, string> = {\n brand: \"var(--brand)\",\n neutral: \"var(--text-muted)\",\n success: \"var(--success)\",\n warning: \"var(--warning)\",\n danger: \"var(--danger)\",\n info: \"var(--info)\",\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n blue: \"var(--hue-blue)\",\n indigo: \"var(--hue-indigo)\",\n purple: \"var(--hue-purple)\",\n teal: \"var(--hue-teal)\",\n orange: \"var(--hue-orange)\",\n};\n\n/** The CSS colour a {@link StatusDot} of `tone` is filled with — `statusDotColor(\"teal\")`\n * is `\"var(--hue-teal)\"`. See the table above for when to reach for it. */\nexport function statusDotColor(tone: StatusDotTone): string {\n return COLOR[tone];\n}\n\nconst SIZE: Record<StatusDotSize, string> = { sm: \"size-2\", md: \"size-2.5\", lg: \"size-3\" };\n\n/**\n * A labelled dot at Large and Extra large (0.32.1, keksdose's 0.32 report;\n * docs/text-size-harmonization.md §4 \"nothing truncates\"): the label wraps instead of\n * ending in \"…\", so a legend entry or a status keeps its last word, the one that tells\n * two states apart. The dot then sits on the label's FIRST line rather than halfway down\n * the block: the row aligns to the top and the dot moves down by half of what one line\n * (`1lh`, the label's own line height, which the dot inherits) leaves around it.\n * `large:` classes, so Normal keeps its one centred line.\n *\n * `break-words`, not `[overflow-wrap:anywhere]`: a legend often sits in a table, and\n * `anywhere` lowers the label's min-content width to one letter, so an auto-sized column\n * squeezed it to a letter per line (the showcase's tone table at 360 px, Extra large).\n * `break-word` keeps each word whole wherever the width is the content's to choose, and\n * still breaks one that cannot fit its line at all.\n */\nconst LABEL_WRAP_AT_LARGE = \"large:whitespace-normal large:break-words\";\nconst DOT_ON_FIRST_LINE: Record<StatusDotSize, string> = {\n sm: \"large:mt-[calc((1lh_-_0.5rem)/2)]\",\n md: \"large:mt-[calc((1lh_-_0.625rem)/2)]\",\n lg: \"large:mt-[calc((1lh_-_0.75rem)/2)]\",\n};\n\nexport interface StatusDotProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n tone?: StatusDotTone;\n size?: StatusDotSize;\n /**\n * Visible text beside the dot — a legend entry (\"Rented\") or a status (\"Online\"). It\n * is also what a screen reader reads: the dot itself is decorative whenever there is\n * a label.\n *\n * Without a label the dot is hidden from assistive technology, because a coloured\n * circle says nothing to someone who cannot see it. Say the state where it IS read —\n * keksdose's avatar trigger puts \"3 unread\" in its own `aria-label` — or give the dot\n * an `aria-label` of its own, which makes it an image with that name.\n */\n label?: ReactNode;\n /**\n * A ring in the surface colour round the dot, so a dot laid over the corner of an\n * icon or an avatar reads as a separate mark and not a smudge (keksdose top-bar.tsx\n * :152, account-menu.tsx ~385, both hand-drawing `ring-2 ring-white\n * dark:ring-slate-900`). Position it with `className` (`absolute -end-0.5 -top-0.5`).\n */\n ring?: boolean;\n /** Extra classes for the dot itself; `className` goes on the outer element. */\n dotClassName?: string;\n}\n\n/**\n * A small token-coloured dot, with an optional label.\n *\n * kastlan's reporting dashboard (dashboard-tab.tsx:79) paints its occupancy legend\n * with raw `bg-green-500` / `bg-red-500` / `bg-yellow-500`, which ignores the palette\n * and the dark theme; keksdose draws two unread dots by hand in `rose-500`. Both are\n * this.\n */\nexport function StatusDot({\n tone = \"neutral\",\n size = \"sm\",\n label,\n ring = false,\n className,\n dotClassName,\n \"aria-label\": ariaLabel,\n ...rest\n}: StatusDotProps) {\n const dot = (\n <span\n className={cn(\n \"inline-block shrink-0 rounded-full\",\n SIZE[size],\n FILL[tone],\n ring && \"ring-2 ring-[var(--bg-surface)]\",\n label != null && DOT_ON_FIRST_LINE[size],\n label == null && className,\n dotClassName,\n )}\n {...(label == null\n ? ariaLabel\n ? { role: \"img\", \"aria-label\": ariaLabel, ...rest }\n : { \"aria-hidden\": true, ...rest }\n : { \"aria-hidden\": true })}\n />\n );\n if (label == null) return dot;\n return (\n // No `aria-label` here: the visible label is the text, and a name on a role-less\n // span is one ARIA forbids and screen readers ignore.\n <span {...rest} className={cn(\"inline-flex min-w-0 items-center gap-1.5 text-sm large:items-start\", className)}>\n {dot}\n <span className={cn(\"min-w-0 truncate\", LABEL_WRAP_AT_LARGE)}>{label}</span>\n </span>\n );\n}\n"],"mappings":";AAsII,cAqBA,YArBA;AArIJ,SAAS,UAAU;AAkBnB,MAAM,OAAsC;AAAA,EAC1C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAWA,MAAM,QAAuC;AAAA,EAC3C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AACV;AAIO,SAAS,eAAe,MAA6B;AAC1D,SAAO,MAAM,IAAI;AACnB;AAEA,MAAM,OAAsC,EAAE,IAAI,UAAU,IAAI,YAAY,IAAI,SAAS;AAiBzF,MAAM,sBAAsB;AAC5B,MAAM,oBAAmD;AAAA,EACvD,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAmCO,SAAS,UAAU;AAAA,EACxB,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAmB;AACjB,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT,KAAK,IAAI;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,QAAQ,kBAAkB,IAAI;AAAA,QACvC,SAAS,QAAQ;AAAA,QACjB;AAAA,MACF;AAAA,MACC,GAAI,SAAS,OACV,YACE,EAAE,MAAM,OAAO,cAAc,WAAW,GAAG,KAAK,IAChD,EAAE,eAAe,MAAM,GAAG,KAAK,IACjC,EAAE,eAAe,KAAK;AAAA;AAAA,EAC5B;AAEF,MAAI,SAAS,KAAM,QAAO;AAC1B;AAAA;AAAA;AAAA,IAGE,qBAAC,UAAM,GAAG,MAAM,WAAW,GAAG,sEAAsE,SAAS,GAC1G;AAAA;AAAA,MACD,oBAAC,UAAK,WAAW,GAAG,oBAAoB,mBAAmB,GAAI,iBAAM;AAAA,OACvE;AAAA;AAEJ;","names":[]}
@@ -129,8 +129,15 @@ function SwipeableRow({
129
129
  type: "button",
130
130
  onClick: action.onCommit,
131
131
  className: cn(
132
- "sr-only whitespace-nowrap rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-sm",
132
+ "sr-only whitespace-nowrap rounded-md border-[var(--border)] bg-[var(--bg-surface)] text-xs font-medium text-[var(--text-primary)] shadow-sm",
133
133
  "focus:not-sr-only focus:pointer-events-auto focus:inline-flex focus:items-center focus:gap-1.5",
134
+ // The box's padding and border only once it shows (0.32.1, keksdose's
135
+ // 0.32 report). Unconditional, `px-2` and `border` beat `sr-only`'s
136
+ // `padding: 0` and `border-width: 0`, so the hidden button was a 1 px
137
+ // box with 2 × 0.5rem of padding round it: 26 px at Extra large, running
138
+ // past the row's end from the strip's start — invisible under its clip,
139
+ // but counted in the page's scroll width (§4: nothing overflows).
140
+ "focus:border focus:px-2 focus:py-1",
134
141
  FOCUS_RING
135
142
  ),
136
143
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/swipeable-row.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useRowSwipe, type SwipeStage } from \"../hooks/use-row-swipe\";\nimport { DEFAULT_SWIPEABLE_ROW_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * One armed swipe action: what it does, and how the reveal panel presents it.\n *\n * The two background classes are the idle and armed states. Before the first\n * threshold is crossed the panel previews the nearest action in its IDLE colour, so\n * the user can see what a little more drag will do; crossing the threshold flips it to\n * `armedClassName`. Passing the same value for both simply removes that feedback.\n */\nexport interface SwipeAction {\n onCommit: () => void;\n label: string;\n /** Rendered inside the panel. Icons stay the consumer's choice — this package\n * ships no opinion about which icon set an app uses. */\n icon?: ReactNode;\n /** Background utility class while the action is previewed but not yet armed. The\n * panel's text is white unless this sets a text colour too — and a fill that turns\n * light in dark mode needs one: pass the fill's contrast token\n * (`bg-[var(--danger)] text-[var(--danger-contrast)]`; also `--brand-contrast`,\n * `--success-contrast`), as the kit's own swipes do since 0.26. */\n className: string;\n /** Background utility class once the drag has passed this action's threshold. */\n armedClassName: string;\n}\n\n/**\n * `right`/`left` are PHYSICAL drag directions in every writing direction, and stay so on\n * purpose: a swipe is a movement of the finger across the glass, the gesture hook\n * measures it in screen pixels, and \"which way is destructive\" is an app decision that\n * RTL guidelines do not agree on. An app that wants mirrored actions in RTL swaps the\n * two arrays itself. What the component does mirror is everything it lays out: the\n * reveal panel's contents hug the physical edge being uncovered, and the keyboard\n * buttons sit at the row's logical END.\n */\nexport interface SwipeableRowProps {\n /** Actions committed by dragging (physically) RIGHT, nearest threshold first. */\n right?: SwipeAction[];\n /** Actions committed by dragging (physically) LEFT, nearest threshold first. */\n left?: SwipeAction[];\n /** Turn the gesture off — an open editor, a pending mutation, a locked row. This\n * also withdraws the keyboard buttons below: a row that must not be acted on must\n * not be actionable by ANY input. */\n enabled?: boolean;\n /** Names the revealed action buttons as a set (\"Row actions\"), for a reader that\n * reaches the group before it reaches any one button. Default:\n * `swipeableRow.actions` from the {@link UiKitProvider}, else English. */\n actionsLabel?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** How much of the row stays on screen at full drag. A row that can be pulled\n * entirely off its own track reads as already deleted, and there is nothing left to\n * drag back; a sliver keeps the gesture reversible and the row identifiable. */\nconst PEEK_PX = 40;\n\n/**\n * A row whose horizontal drag reveals actions underneath it.\n *\n * The reveal panel spans the FULL row width (Keksdose feedback #174). It used to be\n * two half-width blocks side by side, which capped the drag at half the row so the\n * opposite panel could never be exposed — and on a row bound to three actions that\n * left ~55px between stages on a phone, close enough that the wrong one committed.\n * Only the side being dragged toward is painted now, so full width costs nothing:\n * at rest the content covers both anyway, and a drag only ever reveals one side.\n *\n * Thresholds are still not fixed pixel values — they are spread evenly across the\n * available drag, which is now the row width less a {@link PEEK_PX} sliver. A fixed\n * 72/150/228px ladder overshoots a narrow phone; a proportional one keeps every\n * action reachable and roughly doubles the room between them.\n *\n * **Whether the action will fire is stated three ways, not one** (feedback #174): the\n * panel goes from dimmed to solid, the icon grows and gains a filled disc, and the\n * label turns bold — plus the existing haptic tick per newly armed stage. A colour\n * shift alone is easy to miss mid-gesture, and it is invisible to anyone who cannot\n * distinguish the two tones.\n *\n * Clicks that follow a drag are swallowed in the capture phase: without that, a swipe\n * would also fire whatever click handler the row content carries (expanding it, opening\n * a dialog) on top of the action it just committed.\n *\n * ## The keyboard path is buttons, and deliberately not a typed gesture\n *\n * Every action here used to live exclusively inside a pointer drag — `onCommit` had no\n * other caller — so with a keyboard, with a screen reader, or on any input that cannot\n * express 120px of horizontal travel, the row's actions did not exist at all. That is\n * not a rough edge on a phone control: {@link DataTable} mounts this for every row of\n * its mobile layout, and the actions an app puts here are its destructive ones.\n *\n * The rejected fix was to map the gesture onto keys — arrows nudging `dx`, a chord that\n * \"swipes\". That invents a private idiom with nothing to discover it by, and it makes\n * the keyboard imitate a touchscreen instead of doing the job the touchscreen is doing.\n * So each action is ALSO a real button, `sr-only` until it takes focus and then shown at\n * the row's trailing edge, firing the identical `onCommit`: one definition of what an\n * action is, two ways to reach it, and nothing to drift apart. `sr-only` hides from\n * the eye and not from the accessibility tree, so the row also ANNOUNCES what can be\n * done to it rather than keeping it behind a gesture nobody can hear.\n */\nexport function SwipeableRow({\n right,\n left,\n enabled = true,\n actionsLabel,\n className,\n children,\n}: SwipeableRowProps) {\n const labels = useKitLabels(\"swipeableRow\", DEFAULT_SWIPEABLE_ROW_LABELS, {\n actions: actionsLabel,\n });\n const rowRef = useRef<HTMLDivElement>(null);\n const [rowWidth, setRowWidth] = useState(0);\n useEffect(() => {\n const el = rowRef.current;\n if (!el) return;\n setRowWidth(el.clientWidth);\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver((entries) => {\n for (const e of entries) setRowWidth(e.contentRect.width);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const rightActions = right ?? [];\n const leftActions = left ?? [];\n const active = enabled && (rightActions.length > 0 || leftActions.length > 0);\n // The whole row less a sliver, not half of it (feedback #174). `Math.max` keeps a\n // very narrow row (or one measured before layout) from producing a zero or\n // negative drag, which would arm every stage at once.\n const maxDrag = rowWidth > 0 ? Math.max(96, Math.round(rowWidth - PEEK_PX)) : 200;\n const spread = (actions: SwipeAction[]): SwipeStage[] =>\n actions.map((a, i) => ({\n threshold: Math.round((maxDrag * (i + 1)) / (actions.length + 1)),\n onCommit: a.onCommit,\n }));\n\n const swipe = useRowSwipe({\n enabled: active,\n rightStages: spread(rightActions),\n leftStages: spread(leftActions),\n maxDrag,\n });\n\n // A haptic tick per newly armed threshold, so each stage is felt and not only seen.\n const armedNow = swipe.dx >= 0 ? swipe.armedRightIndex : swipe.armedLeftIndex;\n const prevArmed = useRef(-1);\n useEffect(() => {\n if (armedNow > prevArmed.current && armedNow >= 0) navigator.vibrate?.(12);\n prevArmed.current = armedNow;\n }, [armedNow]);\n\n // Index -1 (nothing armed yet) still previews the nearest action, idle-coloured.\n const dx = swipe.dx;\n // Only the side being dragged toward is painted, which is what makes a full-width\n // panel safe.\n //\n // The side has to SURVIVE the release, though, and that is what this ref is for\n // (Keksdose feedback dev#467): *\"Swiping a category to the left with no option — if\n // letting it go it gets, for the way back, the background color of the opposite\n // swipe direction where there should be no color change at all.\"* On release dx\n // snaps to 0 while the row itself slides back over 150ms, so a plain `dx < 0` read\n // \"right\" for exactly the frames where the left side was still uncovered — and\n // painted the RIGHT action's colour into the gap. With no left action at all that\n // colour was the only thing the gesture ever showed.\n //\n // Adjusted during render rather than in an effect: an effect would repaint a frame\n // later (visibly, at 150ms). State set while rendering re-renders this component\n // before anything is committed, so the released frame already has the right side.\n const [lastSide, setLastSide] = useState<\"left\" | \"right\">(\"right\");\n const side = dx === 0 ? null : dx < 0 ? \"left\" : \"right\";\n if (side && side !== lastSide) setLastSide(side);\n const draggingLeft = side ? side === \"left\" : lastSide === \"left\";\n const shown = draggingLeft\n ? leftActions[Math.max(0, swipe.armedLeftIndex)]\n : rightActions[Math.max(0, swipe.armedRightIndex)];\n const armed = draggingLeft ? swipe.armedLeftIndex >= 0 : swipe.armedRightIndex >= 0;\n\n return (\n <div ref={rowRef} className={cn(\"relative overflow-hidden\", className)}>\n {active && shown && (\n <div\n className={cn(\n \"pointer-events-none absolute inset-0 flex items-center gap-2 px-4\",\n \"text-xs font-medium text-white transition-all\",\n // Dimmed until the drag has actually passed a threshold: \"nothing will\n // happen yet\" has to look different from \"let go and this fires\", and on\n // a moving row a one-step colour change was too quiet to notice.\n armed ? \"opacity-100\" : \"opacity-60\",\n armed ? shown.armedClassName : shown.className,\n // The panel is anchored to the edge the row is uncovering, so the label\n // sits where the eye already is rather than across the screen. That edge is\n // physical, while a flex row starts at the INLINE start — so in RTL the two\n // directions swap, or the label sat under the row on the covered side.\n draggingLeft ? \"flex-row-reverse rtl:flex-row\" : \"rtl:flex-row-reverse\",\n )}\n >\n {shown.icon && (\n <span\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-full transition-all\",\n // Armed, the icon gets a disc of its own and grows — a second,\n // non-colour signal, and the one that stays readable at a glance.\n armed ? \"size-8 scale-110 bg-white/25\" : \"size-7 bg-white/10\",\n )}\n >\n {shown.icon}\n </span>\n )}\n <span className={cn(\"truncate transition-all\", armed && \"text-sm font-semibold\")}>\n {shown.label}\n </span>\n </div>\n )}\n <div\n // Capture phase: the row's own click handler lives on a DESCENDANT, so a\n // bubble-phase listener here would run after it — too late to suppress.\n onClickCapture={(e) => {\n if (!swipe.consumeClick()) return;\n e.preventDefault();\n e.stopPropagation();\n }}\n className={cn(\n \"relative bg-[var(--bg-surface)]\",\n // A mouse drag is a supported way to swipe (use-row-swipe gates only on\n // the button, not the pointer type), and a horizontal mouse drag over\n // text also SELECTS that text — so committing an action left the row\n // highlighted blue. Suppressed only while dragging, so text in a row at\n // rest stays selectable.\n swipe.dragging ? \"select-none\" : \"transition-transform duration-150 motion-reduce:transition-none\",\n )}\n style={{\n transform: dx !== 0 ? `translate3d(${dx}px,0,0)` : undefined,\n touchAction: active ? \"pan-y\" : undefined,\n }}\n {...(active ? swipe.handlers : {})}\n >\n {children}\n </div>\n {active && (\n <div\n role=\"group\"\n aria-label={labels.actions}\n // OUTSIDE the sliding div on purpose: an action that travels with the row it\n // acts on is a moving target, and the drag transform would carry it off the\n // screen mid-gesture.\n //\n // `pointer-events-none` here, restored on a button only once it HAS focus: a\n // sr-only button is clipped to nothing but is still a hit target, and this\n // strip sits at the row's trailing edge — exactly where a drag toward the\n // start begins.\n //\n // `end-0`/`pe-2`, not `right-0`/`pr-2`: in RTL the right edge is where the\n // row's title starts, and a focused button there covered it.\n // A gesture that silently failed to start because it landed on an invisible\n // button is not a bug anyone would find by looking at the screen.\n className=\"pointer-events-none absolute inset-y-0 end-0 z-10 flex items-center gap-1 pe-2\"\n >\n {/* Both sides, each still nearest-threshold first, so the tab order reads in\n the order the gesture arms them. */}\n {[...rightActions, ...leftActions].map((action, i) => (\n <button\n key={`${action.label}-${i}`}\n type=\"button\"\n onClick={action.onCommit}\n className={cn(\n \"sr-only whitespace-nowrap rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-sm\",\n \"focus:not-sr-only focus:pointer-events-auto focus:inline-flex focus:items-center focus:gap-1.5\",\n FOCUS_RING,\n )}\n >\n {/* The label is the button's accessible name, so the caller's icon is\n decoration on top of it rather than a second reading of it. */}\n {action.icon && (\n <span aria-hidden=\"true\" className=\"[&_svg]:size-4\">\n {action.icon}\n </span>\n )}\n {action.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/swipeable-row.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { useRowSwipe, type SwipeStage } from \"../hooks/use-row-swipe\";\nimport { DEFAULT_SWIPEABLE_ROW_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/**\n * One armed swipe action: what it does, and how the reveal panel presents it.\n *\n * The two background classes are the idle and armed states. Before the first\n * threshold is crossed the panel previews the nearest action in its IDLE colour, so\n * the user can see what a little more drag will do; crossing the threshold flips it to\n * `armedClassName`. Passing the same value for both simply removes that feedback.\n */\nexport interface SwipeAction {\n onCommit: () => void;\n label: string;\n /** Rendered inside the panel. Icons stay the consumer's choice — this package\n * ships no opinion about which icon set an app uses. */\n icon?: ReactNode;\n /** Background utility class while the action is previewed but not yet armed. The\n * panel's text is white unless this sets a text colour too — and a fill that turns\n * light in dark mode needs one: pass the fill's contrast token\n * (`bg-[var(--danger)] text-[var(--danger-contrast)]`; also `--brand-contrast`,\n * `--success-contrast`), as the kit's own swipes do since 0.26. */\n className: string;\n /** Background utility class once the drag has passed this action's threshold. */\n armedClassName: string;\n}\n\n/**\n * `right`/`left` are PHYSICAL drag directions in every writing direction, and stay so on\n * purpose: a swipe is a movement of the finger across the glass, the gesture hook\n * measures it in screen pixels, and \"which way is destructive\" is an app decision that\n * RTL guidelines do not agree on. An app that wants mirrored actions in RTL swaps the\n * two arrays itself. What the component does mirror is everything it lays out: the\n * reveal panel's contents hug the physical edge being uncovered, and the keyboard\n * buttons sit at the row's logical END.\n */\nexport interface SwipeableRowProps {\n /** Actions committed by dragging (physically) RIGHT, nearest threshold first. */\n right?: SwipeAction[];\n /** Actions committed by dragging (physically) LEFT, nearest threshold first. */\n left?: SwipeAction[];\n /** Turn the gesture off — an open editor, a pending mutation, a locked row. This\n * also withdraws the keyboard buttons below: a row that must not be acted on must\n * not be actionable by ANY input. */\n enabled?: boolean;\n /** Names the revealed action buttons as a set (\"Row actions\"), for a reader that\n * reaches the group before it reaches any one button. Default:\n * `swipeableRow.actions` from the {@link UiKitProvider}, else English. */\n actionsLabel?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** How much of the row stays on screen at full drag. A row that can be pulled\n * entirely off its own track reads as already deleted, and there is nothing left to\n * drag back; a sliver keeps the gesture reversible and the row identifiable. */\nconst PEEK_PX = 40;\n\n/**\n * A row whose horizontal drag reveals actions underneath it.\n *\n * The reveal panel spans the FULL row width (Keksdose feedback #174). It used to be\n * two half-width blocks side by side, which capped the drag at half the row so the\n * opposite panel could never be exposed — and on a row bound to three actions that\n * left ~55px between stages on a phone, close enough that the wrong one committed.\n * Only the side being dragged toward is painted now, so full width costs nothing:\n * at rest the content covers both anyway, and a drag only ever reveals one side.\n *\n * Thresholds are still not fixed pixel values — they are spread evenly across the\n * available drag, which is now the row width less a {@link PEEK_PX} sliver. A fixed\n * 72/150/228px ladder overshoots a narrow phone; a proportional one keeps every\n * action reachable and roughly doubles the room between them.\n *\n * **Whether the action will fire is stated three ways, not one** (feedback #174): the\n * panel goes from dimmed to solid, the icon grows and gains a filled disc, and the\n * label turns bold — plus the existing haptic tick per newly armed stage. A colour\n * shift alone is easy to miss mid-gesture, and it is invisible to anyone who cannot\n * distinguish the two tones.\n *\n * Clicks that follow a drag are swallowed in the capture phase: without that, a swipe\n * would also fire whatever click handler the row content carries (expanding it, opening\n * a dialog) on top of the action it just committed.\n *\n * ## The keyboard path is buttons, and deliberately not a typed gesture\n *\n * Every action here used to live exclusively inside a pointer drag — `onCommit` had no\n * other caller — so with a keyboard, with a screen reader, or on any input that cannot\n * express 120px of horizontal travel, the row's actions did not exist at all. That is\n * not a rough edge on a phone control: {@link DataTable} mounts this for every row of\n * its mobile layout, and the actions an app puts here are its destructive ones.\n *\n * The rejected fix was to map the gesture onto keys — arrows nudging `dx`, a chord that\n * \"swipes\". That invents a private idiom with nothing to discover it by, and it makes\n * the keyboard imitate a touchscreen instead of doing the job the touchscreen is doing.\n * So each action is ALSO a real button, `sr-only` until it takes focus and then shown at\n * the row's trailing edge, firing the identical `onCommit`: one definition of what an\n * action is, two ways to reach it, and nothing to drift apart. `sr-only` hides from\n * the eye and not from the accessibility tree, so the row also ANNOUNCES what can be\n * done to it rather than keeping it behind a gesture nobody can hear.\n */\nexport function SwipeableRow({\n right,\n left,\n enabled = true,\n actionsLabel,\n className,\n children,\n}: SwipeableRowProps) {\n const labels = useKitLabels(\"swipeableRow\", DEFAULT_SWIPEABLE_ROW_LABELS, {\n actions: actionsLabel,\n });\n const rowRef = useRef<HTMLDivElement>(null);\n const [rowWidth, setRowWidth] = useState(0);\n useEffect(() => {\n const el = rowRef.current;\n if (!el) return;\n setRowWidth(el.clientWidth);\n if (typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver((entries) => {\n for (const e of entries) setRowWidth(e.contentRect.width);\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const rightActions = right ?? [];\n const leftActions = left ?? [];\n const active = enabled && (rightActions.length > 0 || leftActions.length > 0);\n // The whole row less a sliver, not half of it (feedback #174). `Math.max` keeps a\n // very narrow row (or one measured before layout) from producing a zero or\n // negative drag, which would arm every stage at once.\n const maxDrag = rowWidth > 0 ? Math.max(96, Math.round(rowWidth - PEEK_PX)) : 200;\n const spread = (actions: SwipeAction[]): SwipeStage[] =>\n actions.map((a, i) => ({\n threshold: Math.round((maxDrag * (i + 1)) / (actions.length + 1)),\n onCommit: a.onCommit,\n }));\n\n const swipe = useRowSwipe({\n enabled: active,\n rightStages: spread(rightActions),\n leftStages: spread(leftActions),\n maxDrag,\n });\n\n // A haptic tick per newly armed threshold, so each stage is felt and not only seen.\n const armedNow = swipe.dx >= 0 ? swipe.armedRightIndex : swipe.armedLeftIndex;\n const prevArmed = useRef(-1);\n useEffect(() => {\n if (armedNow > prevArmed.current && armedNow >= 0) navigator.vibrate?.(12);\n prevArmed.current = armedNow;\n }, [armedNow]);\n\n // Index -1 (nothing armed yet) still previews the nearest action, idle-coloured.\n const dx = swipe.dx;\n // Only the side being dragged toward is painted, which is what makes a full-width\n // panel safe.\n //\n // The side has to SURVIVE the release, though, and that is what this ref is for\n // (Keksdose feedback dev#467): *\"Swiping a category to the left with no option — if\n // letting it go it gets, for the way back, the background color of the opposite\n // swipe direction where there should be no color change at all.\"* On release dx\n // snaps to 0 while the row itself slides back over 150ms, so a plain `dx < 0` read\n // \"right\" for exactly the frames where the left side was still uncovered — and\n // painted the RIGHT action's colour into the gap. With no left action at all that\n // colour was the only thing the gesture ever showed.\n //\n // Adjusted during render rather than in an effect: an effect would repaint a frame\n // later (visibly, at 150ms). State set while rendering re-renders this component\n // before anything is committed, so the released frame already has the right side.\n const [lastSide, setLastSide] = useState<\"left\" | \"right\">(\"right\");\n const side = dx === 0 ? null : dx < 0 ? \"left\" : \"right\";\n if (side && side !== lastSide) setLastSide(side);\n const draggingLeft = side ? side === \"left\" : lastSide === \"left\";\n const shown = draggingLeft\n ? leftActions[Math.max(0, swipe.armedLeftIndex)]\n : rightActions[Math.max(0, swipe.armedRightIndex)];\n const armed = draggingLeft ? swipe.armedLeftIndex >= 0 : swipe.armedRightIndex >= 0;\n\n return (\n <div ref={rowRef} className={cn(\"relative overflow-hidden\", className)}>\n {active && shown && (\n <div\n className={cn(\n \"pointer-events-none absolute inset-0 flex items-center gap-2 px-4\",\n \"text-xs font-medium text-white transition-all\",\n // Dimmed until the drag has actually passed a threshold: \"nothing will\n // happen yet\" has to look different from \"let go and this fires\", and on\n // a moving row a one-step colour change was too quiet to notice.\n armed ? \"opacity-100\" : \"opacity-60\",\n armed ? shown.armedClassName : shown.className,\n // The panel is anchored to the edge the row is uncovering, so the label\n // sits where the eye already is rather than across the screen. That edge is\n // physical, while a flex row starts at the INLINE start — so in RTL the two\n // directions swap, or the label sat under the row on the covered side.\n draggingLeft ? \"flex-row-reverse rtl:flex-row\" : \"rtl:flex-row-reverse\",\n )}\n >\n {shown.icon && (\n <span\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-full transition-all\",\n // Armed, the icon gets a disc of its own and grows — a second,\n // non-colour signal, and the one that stays readable at a glance.\n armed ? \"size-8 scale-110 bg-white/25\" : \"size-7 bg-white/10\",\n )}\n >\n {shown.icon}\n </span>\n )}\n <span className={cn(\"truncate transition-all\", armed && \"text-sm font-semibold\")}>\n {shown.label}\n </span>\n </div>\n )}\n <div\n // Capture phase: the row's own click handler lives on a DESCENDANT, so a\n // bubble-phase listener here would run after it — too late to suppress.\n onClickCapture={(e) => {\n if (!swipe.consumeClick()) return;\n e.preventDefault();\n e.stopPropagation();\n }}\n className={cn(\n \"relative bg-[var(--bg-surface)]\",\n // A mouse drag is a supported way to swipe (use-row-swipe gates only on\n // the button, not the pointer type), and a horizontal mouse drag over\n // text also SELECTS that text — so committing an action left the row\n // highlighted blue. Suppressed only while dragging, so text in a row at\n // rest stays selectable.\n swipe.dragging ? \"select-none\" : \"transition-transform duration-150 motion-reduce:transition-none\",\n )}\n style={{\n transform: dx !== 0 ? `translate3d(${dx}px,0,0)` : undefined,\n touchAction: active ? \"pan-y\" : undefined,\n }}\n {...(active ? swipe.handlers : {})}\n >\n {children}\n </div>\n {active && (\n <div\n role=\"group\"\n aria-label={labels.actions}\n // OUTSIDE the sliding div on purpose: an action that travels with the row it\n // acts on is a moving target, and the drag transform would carry it off the\n // screen mid-gesture.\n //\n // `pointer-events-none` here, restored on a button only once it HAS focus: a\n // sr-only button is clipped to nothing but is still a hit target, and this\n // strip sits at the row's trailing edge — exactly where a drag toward the\n // start begins.\n //\n // `end-0`/`pe-2`, not `right-0`/`pr-2`: in RTL the right edge is where the\n // row's title starts, and a focused button there covered it.\n // A gesture that silently failed to start because it landed on an invisible\n // button is not a bug anyone would find by looking at the screen.\n className=\"pointer-events-none absolute inset-y-0 end-0 z-10 flex items-center gap-1 pe-2\"\n >\n {/* Both sides, each still nearest-threshold first, so the tab order reads in\n the order the gesture arms them. */}\n {[...rightActions, ...leftActions].map((action, i) => (\n <button\n key={`${action.label}-${i}`}\n type=\"button\"\n onClick={action.onCommit}\n className={cn(\n \"sr-only whitespace-nowrap rounded-md border-[var(--border)] bg-[var(--bg-surface)] text-xs font-medium text-[var(--text-primary)] shadow-sm\",\n \"focus:not-sr-only focus:pointer-events-auto focus:inline-flex focus:items-center focus:gap-1.5\",\n // The box's padding and border only once it shows (0.32.1, keksdose's\n // 0.32 report). Unconditional, `px-2` and `border` beat `sr-only`'s\n // `padding: 0` and `border-width: 0`, so the hidden button was a 1 px\n // box with 2 × 0.5rem of padding round it: 26 px at Extra large, running\n // past the row's end from the strip's start — invisible under its clip,\n // but counted in the page's scroll width (§4: nothing overflows).\n \"focus:border focus:px-2 focus:py-1\",\n FOCUS_RING,\n )}\n >\n {/* The label is the button's accessible name, so the caller's icon is\n decoration on top of it rather than a second reading of it. */}\n {action.icon && (\n <span aria-hidden=\"true\" className=\"[&_svg]:size-4\">\n {action.icon}\n </span>\n )}\n {action.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactElement, ComponentPropsWithoutRef, ReactNode, Ref, MouseEvent, KeyboardEvent } from 'react';
3
3
  import { LucideIcon } from 'lucide-react';
4
- import { e0 as KitLinkComponent } from '../delete-account-setting-BvdQdbv3.js';
4
+ import { e0 as KitLinkComponent } from '../delete-account-setting-Dy4kJgUm.js';
5
5
  import './tooltip.js';
6
6
  import '../hooks/use-anchored-rect.js';
7
7
  import '../lib/clipping.js';
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { dO as InputProps } from '../delete-account-setting-BvdQdbv3.js';
2
+ import { dO as InputProps } from '../delete-account-setting-Dy4kJgUm.js';
3
3
  import './tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
@@ -116,8 +116,15 @@ interface ToggleGroupBaseProps<T extends string> extends Omit<ComponentPropsWith
116
116
  * two stack and both keep their corners.
117
117
  */
118
118
  chromeClassName?: string;
119
- /** A {@link FieldHint} on the label line, as on a labelled {@link Select}. Only with
120
- * `label`. */
119
+ /**
120
+ * As on a labelled {@link Select}: a {@link FieldHint} "?" on the label line, and plain
121
+ * text (a string) as a caption under the group. Only with `label`.
122
+ *
123
+ * 0.32.1 (keksdose's 0.32 report): text went on the label line in every placement, so
124
+ * `TextSizeSetting`'s hint with `labelPlacement="above"` sat beside the label in body
125
+ * type, and at Extra large became a column one word wide — on the setting a reader who
126
+ * needs large text opens first. The label line holds the "?" only (`isTextHint`).
127
+ */
121
128
  hint?: ReactNode;
122
129
  /** The message under the field when it is wrong: paints the field's border with
123
130
  * `--danger`, marks the group `aria-invalid` and describes it with the message, as