@eifi1/ui-kit 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +37 -23
  2. package/dist/components/alert-banner.d.ts +57 -5
  3. package/dist/components/alert-banner.js +63 -17
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +112 -0
  10. package/dist/components/bulk-action-bar.js +196 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/button-group.d.ts +58 -3
  13. package/dist/components/button-group.js +56 -5
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/calculator.d.ts +6 -0
  16. package/dist/components/calendar-heatmap.d.ts +124 -0
  17. package/dist/components/calendar-heatmap.js +295 -0
  18. package/dist/components/calendar-heatmap.js.map +1 -0
  19. package/dist/components/chip.d.ts +31 -5
  20. package/dist/components/chip.js +85 -5
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +34 -2
  23. package/dist/components/choice-card.js +49 -0
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/copy-button.d.ts +14 -4
  26. package/dist/components/copy-button.js +17 -4
  27. package/dist/components/copy-button.js.map +1 -1
  28. package/dist/components/data-table.js +2 -0
  29. package/dist/components/data-table.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +22 -2
  31. package/dist/components/date-picker.js +11 -3
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/description-list.d.ts +43 -6
  34. package/dist/components/description-list.js +91 -11
  35. package/dist/components/description-list.js.map +1 -1
  36. package/dist/components/dialog-frame.d.ts +7 -0
  37. package/dist/components/dialog-frame.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +57 -2
  39. package/dist/components/disclosure.js +24 -12
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/field.d.ts +43 -0
  42. package/dist/components/field.js +52 -0
  43. package/dist/components/field.js.map +1 -0
  44. package/dist/components/file-button.d.ts +1 -0
  45. package/dist/components/file-dropzone.d.ts +4 -4
  46. package/dist/components/file-dropzone.js +2 -1
  47. package/dist/components/file-dropzone.js.map +1 -1
  48. package/dist/components/floating-panel.d.ts +147 -4
  49. package/dist/components/floating-panel.js +188 -28
  50. package/dist/components/floating-panel.js.map +1 -1
  51. package/dist/components/full-bleed-dialog.d.ts +23 -4
  52. package/dist/components/full-bleed-dialog.js +11 -2
  53. package/dist/components/full-bleed-dialog.js.map +1 -1
  54. package/dist/components/list.d.ts +187 -0
  55. package/dist/components/list.js +221 -0
  56. package/dist/components/list.js.map +1 -0
  57. package/dist/components/menu-item.d.ts +109 -0
  58. package/dist/components/menu-item.js +88 -0
  59. package/dist/components/menu-item.js.map +1 -0
  60. package/dist/components/mini-calendar.d.ts +70 -5
  61. package/dist/components/mini-calendar.js +166 -62
  62. package/dist/components/mini-calendar.js.map +1 -1
  63. package/dist/components/modal.d.ts +23 -1
  64. package/dist/components/modal.js +36 -10
  65. package/dist/components/modal.js.map +1 -1
  66. package/dist/components/nav-pills.d.ts +66 -0
  67. package/dist/components/nav-pills.js +65 -0
  68. package/dist/components/nav-pills.js.map +1 -0
  69. package/dist/components/number-field.d.ts +6 -0
  70. package/dist/components/number-input.d.ts +6 -0
  71. package/dist/components/numpad-sheet.d.ts +6 -0
  72. package/dist/components/page-contents.js +1 -2
  73. package/dist/components/page-contents.js.map +1 -1
  74. package/dist/components/page-header.d.ts +37 -0
  75. package/dist/components/page-header.js +36 -0
  76. package/dist/components/page-header.js.map +1 -0
  77. package/dist/components/progress-bar.d.ts +31 -2
  78. package/dist/components/progress-bar.js +73 -7
  79. package/dist/components/progress-bar.js.map +1 -1
  80. package/dist/components/series-chart-labels.d.ts +3 -0
  81. package/dist/components/series-chart-labels.js +2 -1
  82. package/dist/components/series-chart-labels.js.map +1 -1
  83. package/dist/components/series-chart.d.ts +32 -0
  84. package/dist/components/series-chart.js +141 -4
  85. package/dist/components/series-chart.js.map +1 -1
  86. package/dist/components/settings-fields.d.ts +1 -0
  87. package/dist/components/status-dot.d.ts +53 -0
  88. package/dist/components/status-dot.js +57 -0
  89. package/dist/components/status-dot.js.map +1 -0
  90. package/dist/components/table.d.ts +78 -13
  91. package/dist/components/table.js +84 -11
  92. package/dist/components/table.js.map +1 -1
  93. package/dist/components/text.d.ts +70 -0
  94. package/dist/components/text.js +24 -0
  95. package/dist/components/text.js.map +1 -0
  96. package/dist/components/time-input.d.ts +1 -0
  97. package/dist/components/toast.d.ts +167 -0
  98. package/dist/components/toast.js +226 -0
  99. package/dist/components/toast.js.map +1 -0
  100. package/dist/components/toggle-group.d.ts +44 -1
  101. package/dist/components/toggle-group.js +73 -4
  102. package/dist/components/toggle-group.js.map +1 -1
  103. package/dist/components/tooltip.d.ts +46 -15
  104. package/dist/components/tooltip.js +87 -47
  105. package/dist/components/tooltip.js.map +1 -1
  106. package/dist/components/ui.d.ts +158 -7
  107. package/dist/components/ui.js +191 -10
  108. package/dist/components/ui.js.map +1 -1
  109. package/dist/components/user-avatar.d.ts +25 -3
  110. package/dist/components/user-avatar.js +32 -3
  111. package/dist/components/user-avatar.js.map +1 -1
  112. package/dist/hooks/use-close-transition.d.ts +7 -1
  113. package/dist/hooks/use-close-transition.js +5 -2
  114. package/dist/hooks/use-close-transition.js.map +1 -1
  115. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  116. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  117. package/dist/hooks/use-file-drop.d.ts +1 -0
  118. package/dist/i18n/defaults.d.ts +6 -0
  119. package/dist/i18n/defaults.js +11 -1
  120. package/dist/i18n/defaults.js.map +1 -1
  121. package/dist/i18n/kit-labels.d.ts +11 -0
  122. package/dist/i18n/kit-labels.js.map +1 -1
  123. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  124. package/dist/i18n/locales/de-CH.d.ts +6 -0
  125. package/dist/i18n/locales/de-informal.d.ts +6 -0
  126. package/dist/i18n/locales/de.d.ts +6 -0
  127. package/dist/i18n/locales/de.js +30 -2
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +6 -0
  130. package/dist/i18n/locales/es.js +30 -2
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +6 -0
  133. package/dist/i18n/locales/fr.js +30 -2
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +6 -0
  136. package/dist/i18n/locales/hu.js +30 -2
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +6 -0
  139. package/dist/i18n/locales/it.js +30 -2
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +6 -0
  142. package/dist/i18n/locales/zh.js +30 -2
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/index.d.ts +27 -15
  145. package/dist/index.js +15 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/clipping.d.ts +25 -0
  148. package/dist/lib/clipping.js +17 -0
  149. package/dist/lib/clipping.js.map +1 -0
  150. package/dist/lib/dates.d.ts +17 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/rhf/form.d.ts +1 -0
  154. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  155. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  156. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  157. package/dist/rhf.d.ts +2 -0
  158. package/dist/rhf.js +1 -0
  159. package/dist/rhf.js.map +1 -1
  160. package/dist/search/command-palette.d.ts +19 -2
  161. package/dist/search/command-palette.js +16 -5
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/search/global-search.d.ts +26 -1
  164. package/dist/search/global-search.js +29 -13
  165. package/dist/search/global-search.js.map +1 -1
  166. package/dist/search.d.ts +1 -1
  167. package/dist/shell/topbar-action-menu.d.ts +73 -3
  168. package/dist/shell/topbar-action-menu.js +100 -27
  169. package/dist/shell/topbar-action-menu.js.map +1 -1
  170. package/dist/shell.d.ts +2 -1
  171. package/dist/wizard/stepper-nav.d.ts +1 -0
  172. package/dist/wizard/stepper-nav.js +1 -1
  173. package/dist/wizard/stepper-nav.js.map +1 -1
  174. package/dist/wizard/types.d.ts +2 -2
  175. package/dist/wizard/types.js.map +1 -1
  176. package/dist/wizard/use-wizard.js +1 -1
  177. package/dist/wizard/use-wizard.js.map +1 -1
  178. package/dist/wizard/wizard-context.d.ts +7 -1
  179. package/dist/wizard/wizard-context.js +4 -0
  180. package/dist/wizard/wizard-context.js.map +1 -1
  181. package/dist/wizard/wizard-summary.js +3 -3
  182. package/dist/wizard/wizard-summary.js.map +1 -1
  183. package/dist/wizard.d.ts +2 -1
  184. package/package.json +16 -12
  185. package/src/components/alert-banner.tsx +149 -21
  186. package/src/components/breadcrumbs.tsx +168 -0
  187. package/src/components/bulk-action-bar.tsx +362 -0
  188. package/src/components/button-group.tsx +124 -4
  189. package/src/components/calendar-heatmap.tsx +504 -0
  190. package/src/components/chip.tsx +128 -7
  191. package/src/components/choice-card.tsx +97 -1
  192. package/src/components/copy-button.tsx +29 -7
  193. package/src/components/data-table.tsx +7 -0
  194. package/src/components/date-picker.tsx +49 -1
  195. package/src/components/description-list.tsx +168 -15
  196. package/src/components/dialog-frame.tsx +7 -0
  197. package/src/components/disclosure.tsx +93 -21
  198. package/src/components/field.tsx +137 -0
  199. package/src/components/file-dropzone.tsx +9 -10
  200. package/src/components/floating-panel.tsx +366 -15
  201. package/src/components/full-bleed-dialog.tsx +42 -5
  202. package/src/components/list.tsx +443 -0
  203. package/src/components/menu-item.tsx +235 -0
  204. package/src/components/mini-calendar.tsx +245 -54
  205. package/src/components/modal.tsx +80 -17
  206. package/src/components/nav-pills.tsx +145 -0
  207. package/src/components/page-contents.tsx +4 -4
  208. package/src/components/page-header.tsx +68 -0
  209. package/src/components/progress-bar.tsx +129 -11
  210. package/src/components/series-chart-labels.ts +4 -0
  211. package/src/components/series-chart.tsx +274 -3
  212. package/src/components/status-dot.tsx +107 -0
  213. package/src/components/table.tsx +176 -17
  214. package/src/components/text.tsx +97 -0
  215. package/src/components/toast.tsx +441 -0
  216. package/src/components/toggle-group.tsx +128 -4
  217. package/src/components/tooltip.tsx +200 -100
  218. package/src/components/ui.tsx +381 -14
  219. package/src/components/user-avatar.tsx +57 -3
  220. package/src/hooks/use-close-transition.ts +14 -5
  221. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  222. package/src/i18n/defaults.ts +10 -0
  223. package/src/i18n/kit-labels.tsx +10 -0
  224. package/src/i18n/locales/de.ts +29 -0
  225. package/src/i18n/locales/es.ts +29 -0
  226. package/src/i18n/locales/fr.ts +29 -0
  227. package/src/i18n/locales/hu.ts +29 -0
  228. package/src/i18n/locales/it.ts +29 -0
  229. package/src/i18n/locales/zh.ts +29 -0
  230. package/src/index.ts +31 -0
  231. package/src/lib/clipping.ts +34 -0
  232. package/src/lib/dates.ts +37 -0
  233. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  234. package/src/rhf.ts +2 -0
  235. package/src/search/command-palette.tsx +42 -5
  236. package/src/search/global-search.tsx +59 -18
  237. package/src/shell/topbar-action-menu.tsx +239 -49
  238. package/src/wizard/stepper-nav.tsx +2 -1
  239. package/src/wizard/types.ts +2 -2
  240. package/src/wizard/use-wizard.ts +3 -3
  241. package/src/wizard/wizard-context.tsx +9 -0
  242. package/src/wizard/wizard-summary.tsx +14 -10
  243. package/tokens.css +97 -0
@@ -0,0 +1,221 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { createContext, forwardRef, useContext } from "react";
4
+ import { ExternalLink } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Spinner } from "./ui.js";
7
+ import { StatusDot } from "./status-dot.js";
8
+ import { useKitLabels } from "../i18n/kit-labels.js";
9
+ const DEFAULT_LIST_LABELS = {
10
+ unread: "Unread",
11
+ opensInNewTab: "opens in a new tab"
12
+ };
13
+ const ListContext = createContext({
14
+ density: "default",
15
+ separator: "gap"
16
+ });
17
+ const List = forwardRef(function List2({ as = "ul", density = "default", separator = "gap", className, children, ...rest }, ref) {
18
+ const Tag = as;
19
+ return /* @__PURE__ */ jsx(ListContext.Provider, { value: { density, separator }, children: /* @__PURE__ */ jsx(
20
+ Tag,
21
+ {
22
+ ...rest,
23
+ ref,
24
+ role: "list",
25
+ className: cn(
26
+ "m-0 flex list-none flex-col p-0",
27
+ separator === "gap" && (density === "compact" ? "gap-0.5" : "gap-1"),
28
+ className
29
+ ),
30
+ children
31
+ }
32
+ ) });
33
+ });
34
+ const PAD = {
35
+ compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5" },
36
+ default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1" },
37
+ comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1" }
38
+ };
39
+ const TARGET_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
40
+ const ListItem = forwardRef(function ListItem2({
41
+ title,
42
+ subtitle,
43
+ subtitleLines = 1,
44
+ meta,
45
+ icon: Icon,
46
+ leading,
47
+ trailing,
48
+ actions,
49
+ unread = false,
50
+ status,
51
+ selected = false,
52
+ disabled = false,
53
+ loading = false,
54
+ density: densityProp,
55
+ align = "center",
56
+ className,
57
+ targetClassName,
58
+ targetProps,
59
+ bordered = false,
60
+ as = "li",
61
+ href,
62
+ renderLink,
63
+ external = false,
64
+ onClick,
65
+ onAuxClick,
66
+ ...rest
67
+ }, ref) {
68
+ const list = useContext(ListContext);
69
+ const labels = useKitLabels("list", DEFAULT_LIST_LABELS);
70
+ const density = densityProp ?? list.density;
71
+ const pad = PAD[density];
72
+ const isLink = href !== void 0 && !disabled;
73
+ const isButton = !isLink && onClick !== void 0 && href === void 0;
74
+ const interactive = isLink || isButton;
75
+ const inert = disabled || loading;
76
+ const mark = status ?? (unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "brand", size: "sm", className: align === "start" ? "mt-1.5" : void 0 }) : null);
77
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
78
+ Icon && /* @__PURE__ */ jsx(
79
+ Icon,
80
+ {
81
+ className: cn("size-4 shrink-0 text-[var(--text-muted)]", align === "start" && "mt-0.5"),
82
+ "aria-hidden": true
83
+ }
84
+ ),
85
+ leading,
86
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
87
+ /* @__PURE__ */ jsxs(
88
+ "span",
89
+ {
90
+ className: cn(
91
+ "block truncate text-sm text-[var(--text-primary)]",
92
+ unread ? "font-semibold" : "font-medium"
93
+ ),
94
+ children: [
95
+ title,
96
+ unread && /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
97
+ " (",
98
+ labels.unread,
99
+ ")"
100
+ ] }),
101
+ external && href !== void 0 && /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
102
+ " (",
103
+ labels.opensInNewTab,
104
+ ")"
105
+ ] })
106
+ ]
107
+ }
108
+ ),
109
+ subtitle != null && /* @__PURE__ */ jsx(
110
+ "span",
111
+ {
112
+ className: cn(
113
+ "block text-xs text-[var(--text-muted)]",
114
+ subtitleLines === 2 ? "line-clamp-2" : "truncate"
115
+ ),
116
+ children: subtitle
117
+ }
118
+ ),
119
+ meta != null && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]", children: meta })
120
+ ] }),
121
+ trailing != null && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]", children: trailing }),
122
+ loading ? /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-3.5 shrink-0" }) : external && href !== void 0 && /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100", "aria-hidden": true }),
123
+ mark
124
+ ] });
125
+ const target = cn(
126
+ "flex min-w-0 flex-1 text-start",
127
+ align === "start" ? "items-start" : "items-center",
128
+ pad.target,
129
+ "rounded-[inherit]",
130
+ interactive && TARGET_RING,
131
+ interactive && !inert && "cursor-pointer",
132
+ loading && "cursor-progress",
133
+ targetClassName
134
+ );
135
+ const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};
136
+ let main;
137
+ if (isLink) {
138
+ const linkProps = {
139
+ ...targetExtras,
140
+ ...rest,
141
+ ref,
142
+ href,
143
+ className: target,
144
+ "aria-current": selected ? "true" : void 0,
145
+ "aria-busy": loading || void 0,
146
+ onClick: loading ? (e) => e.preventDefault() : onClick,
147
+ onAuxClick,
148
+ children: body
149
+ };
150
+ const { children: linkBody, ...anchorProps } = linkProps;
151
+ main = renderLink && !external ? /* @__PURE__ */ jsx(RenderedLink, { render: renderLink, ...linkProps }) : /* @__PURE__ */ jsx("a", { ...anchorProps, ...external ? { target: "_blank", rel: "noopener noreferrer" } : null, children: linkBody });
152
+ } else if (isButton) {
153
+ main = /* @__PURE__ */ jsx(
154
+ "button",
155
+ {
156
+ ...targetExtras,
157
+ ...rest,
158
+ ref,
159
+ type: "button",
160
+ disabled,
161
+ "aria-disabled": loading || void 0,
162
+ "aria-busy": loading || void 0,
163
+ "aria-current": selected ? "true" : void 0,
164
+ onClick: loading ? void 0 : onClick,
165
+ className: target,
166
+ children: body
167
+ }
168
+ );
169
+ } else {
170
+ main = /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ ...targetExtras,
174
+ ...rest,
175
+ ref,
176
+ "aria-disabled": disabled || void 0,
177
+ "aria-busy": loading || void 0,
178
+ className: target,
179
+ children: body
180
+ }
181
+ );
182
+ }
183
+ const Tag = as;
184
+ return /* @__PURE__ */ jsx(
185
+ Tag,
186
+ {
187
+ className: cn(
188
+ list.separator === "divider" && "border-b border-[var(--border)] last:border-b-0",
189
+ list.separator === "divider" && as === "li" && "py-0.5"
190
+ ),
191
+ children: /* @__PURE__ */ jsxs(
192
+ "div",
193
+ {
194
+ className: cn(
195
+ "flex min-w-0 items-center rounded-md border transition-colors",
196
+ selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : bordered ? "border-[var(--border)]" : "border-transparent",
197
+ interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
198
+ disabled && "opacity-50",
199
+ className
200
+ ),
201
+ children: [
202
+ main,
203
+ actions != null && /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.actions), children: actions })
204
+ ]
205
+ }
206
+ )
207
+ }
208
+ );
209
+ });
210
+ function RenderedLink({
211
+ render,
212
+ ...props
213
+ }) {
214
+ return render(props);
215
+ }
216
+ export {
217
+ DEFAULT_LIST_LABELS,
218
+ List,
219
+ ListItem
220
+ };
221
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n});\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; gap: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", gap: \"gap-0.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", gap: \"gap-1\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated. */\n title: ReactNode;\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | {\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n | {\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default `<a>`. The API `Chip`,\n * `StatTile` and `MenuItem` share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n );\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n className={cn(\n \"block truncate text-sm text-[var(--text-primary)]\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n \"rounded-[inherit]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n main = renderLink && !external ? (\n <RenderedLink render={renderLink} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\n );\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 items-center rounded-md border transition-colors\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {main}\n {actions != null && (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div>\n )}\n </div>\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["List","ListItem"]}
@@ -0,0 +1,109 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, MouseEvent, Ref, ReactElement } from 'react';
3
+ import { LucideIcon } from 'lucide-react';
4
+
5
+ type MenuItemTone = "default" | "danger";
6
+ /** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,
7
+ * mapping `href` to what the link calls it —
8
+ * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */
9
+ interface MenuItemLinkProps {
10
+ href: string;
11
+ className: string;
12
+ children: ReactNode;
13
+ role: "menuitem";
14
+ ref?: Ref<HTMLAnchorElement>;
15
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
16
+ "aria-current"?: "page";
17
+ id?: string;
18
+ [key: `aria-${string}`]: string | boolean | number | undefined;
19
+ [key: `data-${string}`]: unknown;
20
+ }
21
+ interface MenuItemBaseProps {
22
+ /** The row's text. */
23
+ children: ReactNode;
24
+ /** An icon before the label, drawn at the menu's 16px. */
25
+ icon?: LucideIcon;
26
+ /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */
27
+ leading?: ReactNode;
28
+ /**
29
+ * Right after the label text, before the gap that pushes {@link trailing} to the
30
+ * row's end: keksdose's budget switcher tags a budget "Guest" or "Shared" with a
31
+ * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end
32
+ * where a count or a check mark sits. Hand-placed inside `children` it was truncated
33
+ * with the name — a long budget name ate the chip first. Here the label truncates
34
+ * and the badge never shrinks, so it stays visible beside what is left of the name.
35
+ * It is part of the row's accessible name ("Household Shared"), so keep it text.
36
+ */
37
+ badge?: ReactNode;
38
+ /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */
39
+ trailing?: ReactNode;
40
+ /** `danger` for the row that destroys or signs out. Colour is not the whole message:
41
+ * the label must still say what the row does. */
42
+ tone?: MenuItemTone;
43
+ disabled?: boolean;
44
+ className?: string;
45
+ id?: string;
46
+ [key: `aria-${string}`]: string | boolean | number | undefined;
47
+ [key: `data-${string}`]: unknown;
48
+ }
49
+ /**
50
+ * A menu row is a button OR a link, and only the button can be CHECKED: a checked link
51
+ * is a link to where you already are, which is `current`, not a selection.
52
+ */
53
+ type MenuItemProps = MenuItemBaseProps & ({
54
+ href?: never;
55
+ renderLink?: never;
56
+ current?: never;
57
+ /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to
58
+ * the render prop that draws the rows. */
59
+ onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
60
+ /**
61
+ * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a
62
+ * plain command; `false` is a choice that is not chosen, which is different.
63
+ * A check mark is drawn at the end when it is on.
64
+ */
65
+ checked?: boolean;
66
+ /**
67
+ * What kind of choice, when `checked` is given:
68
+ *
69
+ * - `radio` (default): one of a set — keksdose's budget switcher
70
+ * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.
71
+ * - `checkbox`: an independent on/off — "Show archived". `menuitemcheckbox`.
72
+ *
73
+ * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a
74
+ * `role="menu"` a screen reader expects the menu roles and reads "checked"
75
+ * only from them. A button with `aria-pressed` in a menu is announced as a
76
+ * button in a menu, which is two widgets in one.
77
+ */
78
+ checkable?: "radio" | "checkbox";
79
+ } | {
80
+ /** Makes the row a link — keksdose's "Manage budgets" and account-menu rows. */
81
+ href: string;
82
+ /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads
83
+ * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */
84
+ renderLink?: (props: MenuItemLinkProps) => ReactElement;
85
+ /** Runs on the way (close the menu). */
86
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
87
+ /** The link to the page already open: `aria-current="page"` and a check mark. */
88
+ current?: boolean;
89
+ checked?: never;
90
+ checkable?: never;
91
+ });
92
+ /**
93
+ * One row of a `HoverMenu` / `TopBarActionMenu` panel.
94
+ *
95
+ * keksdose's account menu and budget switcher each rebuild this row by hand — the
96
+ * shell's class, an icon, a label, a badge, a check mark on the active budget — and the
97
+ * check mark was the only thing saying which budget was active, so a screen reader
98
+ * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).
99
+ *
100
+ * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less
101
+ * buttons and links to `menuitem`: a checked row must be `menuitemradio` or
102
+ * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that
103
+ * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up
104
+ * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`
105
+ * turns the list markup into `role="none"`.
106
+ */
107
+ declare const MenuItem: react.ForwardRefExoticComponent<MenuItemProps & react.RefAttributes<HTMLElement>>;
108
+
109
+ export { MenuItem, type MenuItemLinkProps, type MenuItemProps, type MenuItemTone };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { forwardRef } from "react";
4
+ import { Check } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ const MENU_ITEM_ROW = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
7
+ const MENU_ITEM_FOCUS = "focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
8
+ const MenuItem = forwardRef(function MenuItem2({
9
+ children,
10
+ icon: Icon,
11
+ leading,
12
+ badge,
13
+ trailing,
14
+ tone = "default",
15
+ disabled = false,
16
+ className,
17
+ href,
18
+ renderLink,
19
+ onClick,
20
+ checked,
21
+ checkable,
22
+ current,
23
+ ...rest
24
+ }, ref) {
25
+ const on = href !== void 0 ? !!current : !!checked;
26
+ const look = cn(
27
+ MENU_ITEM_ROW,
28
+ MENU_ITEM_FOCUS,
29
+ tone === "danger" && "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]",
30
+ disabled && "cursor-default opacity-50 hover:bg-transparent",
31
+ className
32
+ );
33
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
34
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
35
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0", "aria-hidden": true }),
36
+ leading,
37
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children }),
38
+ badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment, { children: [
39
+ " ",
40
+ /* @__PURE__ */ jsx("span", { "data-menu-item-badge": "", className: "flex shrink-0 items-center", children: badge })
41
+ ] })
42
+ ] }),
43
+ (trailing != null || on) && /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [
44
+ trailing,
45
+ on && /* @__PURE__ */ jsx(Check, { className: "size-4 text-[var(--text-secondary)]", "aria-hidden": true })
46
+ ] })
47
+ ] });
48
+ if (href !== void 0 && !disabled) {
49
+ const linkProps = {
50
+ ...rest,
51
+ ref,
52
+ href,
53
+ role: "menuitem",
54
+ className: look,
55
+ onClick,
56
+ "aria-current": current ? "page" : void 0,
57
+ children: body
58
+ };
59
+ if (renderLink) return /* @__PURE__ */ jsx(RenderedLink, { render: renderLink, ...linkProps });
60
+ const { children: linkBody, ...anchorProps } = linkProps;
61
+ return /* @__PURE__ */ jsx("a", { ...anchorProps, children: linkBody });
62
+ }
63
+ const role = href === void 0 && checked !== void 0 ? checkable === "checkbox" ? "menuitemcheckbox" : "menuitemradio" : "menuitem";
64
+ return /* @__PURE__ */ jsx(
65
+ "button",
66
+ {
67
+ ...rest,
68
+ ref,
69
+ type: "button",
70
+ role,
71
+ "aria-checked": role === "menuitem" ? void 0 : !!checked,
72
+ "aria-disabled": disabled || void 0,
73
+ onClick: disabled ? (e) => e.preventDefault() : onClick,
74
+ className: look,
75
+ children: body
76
+ }
77
+ );
78
+ });
79
+ function RenderedLink({
80
+ render,
81
+ ...props
82
+ }) {
83
+ return render(props);
84
+ }
85
+ export {
86
+ MenuItem
87
+ };
88
+ //# sourceMappingURL=menu-item.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/menu-item.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does\n * not reach up into the shell (the same call `Disclosure`'s menu variant made). A test\n * holds the two to the same classes, so a menu mixing `MenuItem`s with the shell's own\n * rows (`TopBarActionMenu`, `OptionSwitcherMenu`) cannot drift into two looks.\n */\nconst MENU_ITEM_ROW =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** HoverMenu moves focus onto items with the arrow keys; a pointer user never sees the\n * ring, a keyboard user always does. Inset, because the panel clips its children. */\nconst MENU_ITEM_FOCUS =\n \"focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\nexport type MenuItemTone = \"default\" | \"danger\";\n\n/** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what the link calls it —\n * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface MenuItemLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n role: \"menuitem\";\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"page\";\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\ninterface MenuItemBaseProps {\n /** The row's text. */\n children: ReactNode;\n /** An icon before the label, drawn at the menu's 16px. */\n icon?: LucideIcon;\n /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */\n leading?: ReactNode;\n /**\n * Right after the label text, before the gap that pushes {@link trailing} to the\n * row's end: keksdose's budget switcher tags a budget \"Guest\" or \"Shared\" with a\n * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end\n * where a count or a check mark sits. Hand-placed inside `children` it was truncated\n * with the name — a long budget name ate the chip first. Here the label truncates\n * and the badge never shrinks, so it stays visible beside what is left of the name.\n * It is part of the row's accessible name (\"Household Shared\"), so keep it text.\n */\n badge?: ReactNode;\n /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */\n trailing?: ReactNode;\n /** `danger` for the row that destroys or signs out. Colour is not the whole message:\n * the label must still say what the row does. */\n tone?: MenuItemTone;\n disabled?: boolean;\n className?: string;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * A menu row is a button OR a link, and only the button can be CHECKED: a checked link\n * is a link to where you already are, which is `current`, not a selection.\n */\nexport type MenuItemProps = MenuItemBaseProps &\n (\n | {\n href?: never;\n renderLink?: never;\n current?: never;\n /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to\n * the render prop that draws the rows. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a\n * plain command; `false` is a choice that is not chosen, which is different.\n * A check mark is drawn at the end when it is on.\n */\n checked?: boolean;\n /**\n * What kind of choice, when `checked` is given:\n *\n * - `radio` (default): one of a set — keksdose's budget switcher\n * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.\n * - `checkbox`: an independent on/off — \"Show archived\". `menuitemcheckbox`.\n *\n * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a\n * `role=\"menu\"` a screen reader expects the menu roles and reads \"checked\"\n * only from them. A button with `aria-pressed` in a menu is announced as a\n * button in a menu, which is two widgets in one.\n */\n checkable?: \"radio\" | \"checkbox\";\n }\n | {\n /** Makes the row a link — keksdose's \"Manage budgets\" and account-menu rows. */\n href: string;\n /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads\n * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */\n renderLink?: (props: MenuItemLinkProps) => ReactElement;\n /** Runs on the way (close the menu). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n checked?: never;\n checkable?: never;\n }\n );\n\n/**\n * One row of a `HoverMenu` / `TopBarActionMenu` panel.\n *\n * keksdose's account menu and budget switcher each rebuild this row by hand — the\n * shell's class, an icon, a label, a badge, a check mark on the active budget — and the\n * check mark was the only thing saying which budget was active, so a screen reader\n * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).\n *\n * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less\n * buttons and links to `menuitem`: a checked row must be `menuitemradio` or\n * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that\n * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up\n * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`\n * turns the list markup into `role=\"none\"`.\n */\nexport const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem(\n {\n children,\n icon: Icon,\n leading,\n badge,\n trailing,\n tone = \"default\",\n disabled = false,\n className,\n href,\n renderLink,\n onClick,\n checked,\n checkable,\n current,\n ...rest\n },\n ref,\n) {\n const on = href !== undefined ? !!current : !!checked;\n const look = cn(\n MENU_ITEM_ROW,\n MENU_ITEM_FOCUS,\n tone === \"danger\" && \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]\",\n disabled && \"cursor-default opacity-50 hover:bg-transparent\",\n className,\n );\n const body = (\n <>\n <span className=\"flex min-w-0 items-center gap-2\">\n {Icon && <Icon className=\"size-4 shrink-0\" aria-hidden />}\n {leading}\n <span className=\"min-w-0 truncate\">{children}</span>\n {badge != null && badge !== false && (\n <>\n {/* A space between the name and the badge in the accessible name (\"Household\n Shared\", not \"HouseholdShared\"); whitespace between flex items is not\n laid out, so it costs nothing on screen. */}{\" \"}\n <span data-menu-item-badge=\"\" className=\"flex shrink-0 items-center\">\n {badge}\n </span>\n </>\n )}\n </span>\n {(trailing != null || on) && (\n <span className=\"flex shrink-0 items-center gap-2\">\n {trailing}\n {on && <Check className=\"size-4 text-[var(--text-secondary)]\" aria-hidden />}\n </span>\n )}\n </>\n );\n\n if (href !== undefined && !disabled) {\n const linkProps: MenuItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n role: \"menuitem\",\n className: look,\n onClick: onClick as MenuItemLinkProps[\"onClick\"],\n \"aria-current\": current ? \"page\" : undefined,\n children: body,\n };\n if (renderLink) return <RenderedLink render={renderLink} {...linkProps} />;\n const { children: linkBody, ...anchorProps } = linkProps;\n return <a {...anchorProps}>{linkBody}</a>;\n }\n\n const role =\n href === undefined && checked !== undefined\n ? checkable === \"checkbox\"\n ? \"menuitemcheckbox\"\n : \"menuitemradio\"\n : \"menuitem\";\n return (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n role={role}\n aria-checked={role === \"menuitem\" ? undefined : !!checked}\n // `aria-disabled` rather than `disabled`: HoverMenu's roving focus skips either,\n // and the ARIA menu pattern keeps an unavailable item perceivable (\"Delete,\n // dimmed\") rather than greyed to the point some browsers stop exposing it.\n aria-disabled={disabled || undefined}\n onClick={\n disabled\n ? (e) => e.preventDefault()\n : (onClick as ((event: MouseEvent<HTMLButtonElement>) => void) | undefined)\n }\n className={look}\n >\n {body}\n </button>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: MenuItemLinkProps & { render: (props: MenuItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAiKiB,SAIP,UAJO,KAIP,YAJO;AAjKjB,SAAS,kBAAkB;AAE3B,SAAS,aAAa;AAEtB,SAAS,UAAU;AAQnB,MAAM,gBACJ;AAIF,MAAM,kBACJ;AAgHK,MAAM,WAAW,WAAuC,SAASA,UACtE;AAAA,EACE;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,KAAK,SAAS,SAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACE;AAAA,yBAAC,UAAK,WAAU,mCACb;AAAA,cAAQ,oBAAC,QAAK,WAAU,mBAAkB,eAAW,MAAC;AAAA,MACtD;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,MAC5C,SAAS,QAAQ,UAAU,SAC1B,iCAGmD;AAAA;AAAA,QACjD,oBAAC,UAAK,wBAAqB,IAAG,WAAU,8BACrC,iBACH;AAAA,SACF;AAAA,OAEJ;AAAA,KACE,YAAY,QAAQ,OACpB,qBAAC,UAAK,WAAU,oCACb;AAAA;AAAA,MACA,MAAM,oBAAC,SAAM,WAAU,uCAAsC,eAAW,MAAC;AAAA,OAC5E;AAAA,KAEJ;AAGF,MAAI,SAAS,UAAa,CAAC,UAAU;AACnC,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,MACA,gBAAgB,UAAU,SAAS;AAAA,MACnC,UAAU;AAAA,IACZ;AACA,QAAI,WAAY,QAAO,oBAAC,gBAAa,QAAQ,YAAa,GAAG,WAAW;AACxE,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,oBAAC,OAAG,GAAG,aAAc,oBAAS;AAAA,EACvC;AAEA,QAAM,OACJ,SAAS,UAAa,YAAY,SAC9B,cAAc,aACZ,qBACA,kBACF;AACN,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc,SAAS,aAAa,SAAY,CAAC,CAAC;AAAA,MAIlD,iBAAe,YAAY;AAAA,MAC3B,SACE,WACI,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MAEP,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["MenuItem"]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
4
  /**
5
5
  * Every string this calendar can speak — the `miniCalendar` namespace of
@@ -52,8 +52,10 @@ declare const DEFAULT_MINI_CALENDAR_LABELS: MiniCalendarLabels;
52
52
  * before this, a closed prop list dropped both (audit §api-design).
53
53
  */
54
54
  interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSelect"> {
55
- from: string;
56
- to: string;
55
+ /** The selection's start as "YYYY-MM-DD"; `""` (the default) for none. */
56
+ from?: string;
57
+ /** The selection's end; `""` (the default) for none. */
58
+ to?: string;
57
59
  /**
58
60
  * BCP 47 tag for the month caption, the weekday heads, the day numbers and the
59
61
  * spoken dates. Optional since the kit grew `<UiKitProvider locale>`: a calendar
@@ -94,9 +96,72 @@ interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSel
94
96
  * merely by existing.
95
97
  */
96
98
  focusOnOpen?: boolean;
99
+ /**
100
+ * `"sm"` (default) is the compact picker: 32px round days, the size the date
101
+ * pickers and the data-table filter draw. `"lg"` is a MONTH VIEW for a page — tall
102
+ * cells in a ruled grid, the day number in the top corner and {@link renderDay}'s
103
+ * content under it — the calendar a planner page hand-built before this existed.
104
+ * Both are the same grid underneath: the same roles, roving tabindex, keys, RTL
105
+ * mirroring and week start.
106
+ */
107
+ size?: "sm" | "lg";
108
+ /**
109
+ * Custom content for one day: events, an amount, a dot. Called for every day of the
110
+ * month on show; return `null` for none.
111
+ *
112
+ * ⚠️ **It renders INSIDE the day's button** — a dot under the number at `"sm"`, a
113
+ * column under it at `"lg"` — so it must not be interactive: no links, no buttons,
114
+ * no inputs. A control inside a control is invalid HTML, and a screen reader lands
115
+ * on the day and never reaches what is inside it. Events that are themselves
116
+ * clickable are shown here as plain text, and the DAY is what is clicked: pass
117
+ * `mode="single"` and let `onSelect` open a day panel (a drawer, a popover) that
118
+ * lists that day's events as real links.
119
+ *
120
+ * The day's accessible name stays the full date; this content is attached to it as
121
+ * its DESCRIPTION (`aria-describedby`), so "Monday, 14 September 2026" is read
122
+ * first and "Rent due · Plumber" after it. Anything decorative (a coloured dot)
123
+ * should be `aria-hidden` or carry an `sr-only` word for what it means.
124
+ */
125
+ renderDay?: (day: Date, state: MiniCalendarDayState) => ReactNode;
126
+ /**
127
+ * The month on show, as "YYYY-MM". FOLLOWED rather than controlled, exactly like
128
+ * `from`/`to`: when it changes the grid moves there, and in between the user pages
129
+ * freely — every move to another month is reported through {@link onMonthChange}.
130
+ * For a month page whose own header (a month picker, a Today button) drives the
131
+ * calendar. Left out, the grid opens on the selection or today.
132
+ */
133
+ month?: string;
134
+ /** Called with "YYYY-MM" whenever the USER moves the grid to another month (the
135
+ * arrows, PageUp/PageDown, arrowing off the edge). */
136
+ onMonthChange?: (month: string) => void;
137
+ /**
138
+ * Hide the built-in previous/next arrows and month caption, for a page that draws
139
+ * its own. The caption is kept for assistive technology (visually hidden), because
140
+ * it is what names the grid.
141
+ */
142
+ hideNavigation?: boolean;
97
143
  /** Extra classes for the calendar's root. */
98
144
  className?: string;
99
145
  }
146
+ /** What {@link MiniCalendarProps.renderDay} is told about the day it draws. */
147
+ interface MiniCalendarDayState {
148
+ /** The day as "YYYY-MM-DD". */
149
+ iso: string;
150
+ /** The day number as drawn, in the calendar's locale. */
151
+ label: string;
152
+ today: boolean;
153
+ /** Outside `min`/`max`. */
154
+ disabled: boolean;
155
+ /** Part of the selection — an end or a day between. */
156
+ selected: boolean;
157
+ rangeStart: boolean;
158
+ rangeEnd: boolean;
159
+ /** Strictly between the two ends of a range. */
160
+ inRange: boolean;
161
+ /** The day that holds the tab stop. */
162
+ active: boolean;
163
+ size: "sm" | "lg";
164
+ }
100
165
  type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
101
166
  /**
102
167
  * Month-grid range picker used by the data-table date filter. Click once to set
@@ -119,6 +184,6 @@ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
119
184
  * month would have holes that the roving tabindex falls into. They stay reachable and
120
185
  * readable, and the click handler is what refuses them.
121
186
  */
122
- declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, className, ...rest }: MiniCalendarProps): react.JSX.Element;
187
+ declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, size, renderDay, month, onMonthChange, hideNavigation, className, ...rest }: MiniCalendarProps): react.JSX.Element;
123
188
 
124
- export { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarLabels, type MiniCalendarProps, type WeekDay };
189
+ export { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarDayState, type MiniCalendarLabels, type MiniCalendarProps, type WeekDay };