@eifi1/ui-kit 0.4.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 (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
@@ -0,0 +1,171 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useAnchoredRect } from "../hooks/use-anchored-rect.js";
6
+ const TOOLTIP_SURFACE = "w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-lg";
7
+ const sidePositionClass = {
8
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-1",
9
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-1",
10
+ left: "right-full top-1/2 -translate-y-1/2 mr-1",
11
+ right: "left-full top-1/2 -translate-y-1/2 ml-1"
12
+ };
13
+ function Tooltip({
14
+ label,
15
+ side = "top",
16
+ className,
17
+ portal = false,
18
+ redact = false,
19
+ children
20
+ }) {
21
+ if (isEmptyLabel(label)) return /* @__PURE__ */ jsx(Fragment, { children });
22
+ if (portal) {
23
+ return /* @__PURE__ */ jsx(
24
+ PortalTooltip,
25
+ {
26
+ label,
27
+ side,
28
+ className,
29
+ redact,
30
+ children
31
+ }
32
+ );
33
+ }
34
+ return /* @__PURE__ */ jsxs("span", { className: cn("relative inline-flex group/tooltip", className), children: [
35
+ children,
36
+ /* @__PURE__ */ jsx(
37
+ "span",
38
+ {
39
+ role: "tooltip",
40
+ "data-private": redact ? "" : void 0,
41
+ className: cn(
42
+ TOOLTIP_SURFACE,
43
+ "pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100",
44
+ sidePositionClass[side]
45
+ ),
46
+ children: label
47
+ }
48
+ )
49
+ ] });
50
+ }
51
+ function isEmptyLabel(label) {
52
+ return label == null || label === false || typeof label === "string" && label.trim() === "";
53
+ }
54
+ const TOOLTIP_GAP = 4;
55
+ const TOOLTIP_MARGIN = 4;
56
+ const portalTransformBySide = {
57
+ right: "translate(0, -50%)",
58
+ left: "translate(-100%, -50%)",
59
+ top: "translate(-50%, -100%)",
60
+ bottom: "translate(-50%, 0)"
61
+ };
62
+ function tooltipAnchor(r, side) {
63
+ switch (side) {
64
+ case "right":
65
+ return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };
66
+ case "left":
67
+ return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };
68
+ case "top":
69
+ return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };
70
+ case "bottom":
71
+ return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };
72
+ }
73
+ }
74
+ const opposite = {
75
+ left: "right",
76
+ right: "left",
77
+ top: "bottom",
78
+ bottom: "top"
79
+ };
80
+ function roomOn(r, side, size, viewport) {
81
+ switch (side) {
82
+ case "left":
83
+ return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;
84
+ case "right":
85
+ return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;
86
+ case "top":
87
+ return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;
88
+ case "bottom":
89
+ return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;
90
+ }
91
+ }
92
+ function sameRoom(a, b) {
93
+ return a.size.width === b.size.width && a.size.height === b.size.height && a.viewport.width === b.viewport.width && a.viewport.height === b.viewport.height;
94
+ }
95
+ function clamp(value, low, high) {
96
+ return Math.max(low, Math.min(value, high));
97
+ }
98
+ function placeTooltip(r, side, size, viewport) {
99
+ const chosen = roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport) ? side : opposite[side];
100
+ const point = tooltipAnchor(r, chosen);
101
+ const box = chosen === "left" ? { left: point.left - size.width, top: point.top - size.height / 2 } : chosen === "right" ? { left: point.left, top: point.top - size.height / 2 } : chosen === "top" ? { left: point.left - size.width / 2, top: point.top - size.height } : { left: point.left - size.width / 2, top: point.top };
102
+ return {
103
+ left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),
104
+ top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),
105
+ side: chosen
106
+ };
107
+ }
108
+ function PortalTooltip({
109
+ label,
110
+ side,
111
+ className,
112
+ redact,
113
+ children
114
+ }) {
115
+ const triggerRef = useRef(null);
116
+ const bubbleRef = useRef(null);
117
+ const [visible, setVisible] = useState(false);
118
+ const rect = useAnchoredRect(triggerRef, visible);
119
+ const [room, setRoom] = useState(null);
120
+ useLayoutEffect(() => {
121
+ const measured = visible ? bubbleRef.current?.getBoundingClientRect() : void 0;
122
+ setRoom((previous) => {
123
+ if (!measured) return null;
124
+ const next = {
125
+ size: { width: measured.width, height: measured.height },
126
+ viewport: { width: window.innerWidth, height: window.innerHeight }
127
+ };
128
+ return previous && sameRoom(previous, next) ? previous : next;
129
+ });
130
+ }, [visible, rect, label]);
131
+ const point = rect ? tooltipAnchor(rect, side) : null;
132
+ const placed = rect && room ? placeTooltip(rect, side, room.size, room.viewport) : null;
133
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
134
+ /* @__PURE__ */ jsx(
135
+ "span",
136
+ {
137
+ ref: triggerRef,
138
+ className: cn("relative inline-flex", className),
139
+ onMouseEnter: () => setVisible(true),
140
+ onMouseLeave: () => setVisible(false),
141
+ onFocus: () => setVisible(true),
142
+ onBlur: () => setVisible(false),
143
+ children
144
+ }
145
+ ),
146
+ visible && point && typeof document !== "undefined" && createPortal(
147
+ /* @__PURE__ */ jsx(
148
+ "span",
149
+ {
150
+ ref: bubbleRef,
151
+ role: "tooltip",
152
+ "data-private": redact ? "" : void 0,
153
+ style: placed ? { position: "fixed", left: placed.left, top: placed.top } : {
154
+ position: "fixed",
155
+ left: point.left,
156
+ top: point.top,
157
+ transform: portalTransformBySide[side]
158
+ },
159
+ className: cn(TOOLTIP_SURFACE, "pointer-events-none z-50"),
160
+ children: label
161
+ }
162
+ ),
163
+ document.body
164
+ )
165
+ ] });
166
+ }
167
+ export {
168
+ Tooltip,
169
+ placeTooltip
170
+ };
171
+ //# sourceMappingURL=tooltip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/tooltip.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { useAnchoredRect, type AnchorRect } from \"../hooks/use-anchored-rect\";\n\ntype TooltipSide = \"top\" | \"bottom\" | \"left\" | \"right\";\n\n/** The floating bubble itself. Uses the shared surface/border/text tokens so it\n * reads as part of the app's chrome (like the top bar and cards) rather than the\n * cold slate pill it used to be.\n *\n * `w-max` keeps a short label on one line — the old `whitespace-nowrap` did that\n * too, but it also let a sentence-length label grow without bound, and a bubble\n * wider than the space beside its trigger gets clipped by whatever overflow\n * container it sits in. So cap it and let long text wrap instead. The cap tracks\n * the viewport as well, for narrow screens where 20rem is already most of it.\n * `side` is a preference rather than an instruction for the PORTALLED variant,\n * which measures the bubble and turns it round when it would not fit\n * (Steering Design feedback #126). The CSS-only one never learns its own size,\n * so there `side` is still the whole of the placement. */\nconst TOOLTIP_SURFACE =\n \"w-max max-w-[min(20rem,calc(100vw-1rem))] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs font-medium text-[var(--text-primary)] shadow-lg\";\n\nconst sidePositionClass: Record<TooltipSide, string> = {\n top: \"bottom-full left-1/2 -translate-x-1/2 mb-1\",\n bottom: \"top-full left-1/2 -translate-x-1/2 mt-1\",\n left: \"right-full top-1/2 -translate-y-1/2 mr-1\",\n right: \"left-full top-1/2 -translate-y-1/2 ml-1\",\n};\n\n/**\n * Hover/focus label for a control.\n *\n * Two implementations, and the choice matters more than it looks. The default is\n * CSS-only: the bubble is always mounted next to the trigger and fades in on\n * `:hover`, which costs no state and works in a plain render test. The `portal`\n * variant mounts the bubble in `document.body` only while hovered, positioned by\n * measurement.\n *\n * ⚠️ **A bubble that repeats a value has to be redactable.** The consuming app blurs\n * `[data-private]` under a `demo-mode` class on `<html>` — and the portalled bubble is\n * mounted on `document.body`, which is INSIDE that class, so the rule reaches it as\n * long as the bubble is tagged. It is not tagged by default, because most labels are\n * UI strings; pass `redact` on the ones that repeat the user's own data (a truncated\n * payee, an account name, a memo). Getting this wrong is silent: the trigger blurs,\n * the bubble spells the value out on hover.\n *\n * ⚠️ **An empty label renders nothing at all.** `title={payee ?? \"\"}` is an ordinary\n * shape at a call site that reveals truncated text, and the native attribute answers\n * it by showing no tooltip. A component that faithfully rendered an empty bubble\n * would be a worse `title`, so the emptiness check is here rather than at every call\n * site that could forget it.\n *\n * ⚠️ **Inside a scroll container, use `portal`.** An always-mounted bubble is\n * absolutely positioned, but an absolutely positioned descendant still counts\n * towards its scroll-container ancestor's scrollable overflow — so an invisible\n * bubble on a control near the right edge makes the container scroll sideways\n * with nothing to reveal. That is what Keksdose feedback dev#488 reported on the\n * admin roster: 66px of horizontal scroll on a table that fit, 44px of it owed to\n * tooltips nobody could see. The portalled bubble is `position: fixed` and absent\n * until hovered, so it adds no width — and, being outside the container, it also\n * cannot be clipped by it.\n */\nexport function Tooltip({\n label,\n side = \"top\",\n className,\n portal = false,\n redact = false,\n children,\n}: {\n label: ReactNode;\n side?: TooltipSide;\n className?: string;\n portal?: boolean;\n /** Tag the bubble `data-private`, for a label that repeats the user's own data. */\n redact?: boolean;\n children: ReactNode;\n}) {\n // No label, no bubble — and no wrapper either, so a conditional tooltip costs the\n // layout nothing on the branch where it does not apply.\n if (isEmptyLabel(label)) return <>{children}</>;\n if (portal) {\n return (\n <PortalTooltip\n label={label}\n side={side}\n className={className}\n redact={redact}\n >\n {children}\n </PortalTooltip>\n );\n }\n return (\n <span className={cn(\"relative inline-flex group/tooltip\", className)}>\n {children}\n <span\n role=\"tooltip\"\n data-private={redact ? \"\" : undefined}\n className={cn(\n TOOLTIP_SURFACE,\n \"pointer-events-none absolute z-50 opacity-0 group-hover/tooltip:opacity-100 group-focus-within/tooltip:opacity-100\",\n sidePositionClass[side],\n )}\n >\n {label}\n </span>\n </span>\n );\n}\n\n/** \"Would this bubble be blank.\" Only the values a call site actually produces when\n * it has nothing to say — `\"\"`, `null`, `undefined`, `false` from a `&&` guard. A\n * numeric `0` is a real label and stays one. */\nfunction isEmptyLabel(label: ReactNode): boolean {\n return (\n label == null ||\n label === false ||\n (typeof label === \"string\" && label.trim() === \"\")\n );\n}\n\nconst TOOLTIP_GAP = 4;\n\n/** How close to the viewport edge a bubble may sit. Not zero: a label flush\n * against the glass reads as clipped even when every character is on screen. */\nconst TOOLTIP_MARGIN = 4;\n\nconst portalTransformBySide: Record<TooltipSide, string> = {\n right: \"translate(0, -50%)\",\n left: \"translate(-100%, -50%)\",\n top: \"translate(-50%, -100%)\",\n bottom: \"translate(-50%, 0)\",\n};\n\n/** Anchor point (viewport px) for the tooltip on the given side of `r`. Paired\n * with {@link portalTransformBySide}, which shifts the box onto that point. */\nfunction tooltipAnchor(\n r: AnchorRect,\n side: TooltipSide,\n): { left: number; top: number } {\n switch (side) {\n case \"right\":\n return { left: r.right + TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"left\":\n return { left: r.left - TOOLTIP_GAP, top: r.top + r.height / 2 };\n case \"top\":\n return { left: r.left + r.width / 2, top: r.top - TOOLTIP_GAP };\n case \"bottom\":\n return { left: r.left + r.width / 2, top: r.bottom + TOOLTIP_GAP };\n }\n}\n\nexport interface TooltipSize {\n width: number;\n height: number;\n}\n\nexport interface TooltipViewport {\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n left: number;\n top: number;\n /** Which side it ended up on, which need not be the one that was asked for. */\n side: TooltipSide;\n}\n\nconst opposite: Record<TooltipSide, TooltipSide> = {\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n bottom: \"top\",\n};\n\n/** Whether the bubble clears the viewport edge on `side` of the trigger. */\nfunction roomOn(\n r: AnchorRect,\n side: TooltipSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): boolean {\n switch (side) {\n case \"left\":\n return r.left - TOOLTIP_GAP - size.width >= TOOLTIP_MARGIN;\n case \"right\":\n return r.right + TOOLTIP_GAP + size.width <= viewport.width - TOOLTIP_MARGIN;\n case \"top\":\n return r.top - TOOLTIP_GAP - size.height >= TOOLTIP_MARGIN;\n case \"bottom\":\n return r.bottom + TOOLTIP_GAP + size.height <= viewport.height - TOOLTIP_MARGIN;\n }\n}\n\nfunction sameRoom(\n a: { size: TooltipSize; viewport: TooltipViewport },\n b: { size: TooltipSize; viewport: TooltipViewport },\n): boolean {\n return (\n a.size.width === b.size.width &&\n a.size.height === b.size.height &&\n a.viewport.width === b.viewport.width &&\n a.viewport.height === b.viewport.height\n );\n}\n\nfunction clamp(value: number, low: number, high: number): number {\n // `high` first, so a bubble taller or wider than the viewport is pinned to the\n // top-left corner rather than to the bottom-right one — the start of a label\n // is the half worth keeping.\n return Math.max(low, Math.min(value, high));\n}\n\n/**\n * Where the bubble actually goes, given how big it turned out to be.\n *\n * Two rules, and they are separate because they fix separate failures.\n *\n * **Turn round when the preferred side has no room.** `side` says which side of\n * the trigger the label reads best on, and on a form near the left edge of the\n * window that side is off the screen — the capped bubble can only wrap, not\n * move, so what the reader gets is a sentence with its first half outside the\n * glass. Flipped only when the *other* side is genuinely better: a trigger in a\n * viewport too narrow for the bubble either way keeps the side it asked for, and\n * the clamp below does what it can.\n *\n * **Then clamp both axes.** The cross axis is the one that needs it — a `top`\n * bubble is centred on the trigger, so a trigger near the left edge pushes half\n * the label off even though the side it is on is right — and clamping the main\n * axis too costs nothing and covers the flip having nowhere to land.\n *\n * Pure, and measured in viewport pixels throughout, so it can be tested without\n * a layout: the caller supplies the trigger's rect, the bubble's own size and\n * the window.\n */\nexport function placeTooltip(\n r: AnchorRect,\n side: TooltipSide,\n size: TooltipSize,\n viewport: TooltipViewport,\n): TooltipPlacement {\n const chosen =\n roomOn(r, side, size, viewport) || !roomOn(r, opposite[side], size, viewport)\n ? side\n : opposite[side];\n const point = tooltipAnchor(r, chosen);\n const box =\n chosen === \"left\"\n ? { left: point.left - size.width, top: point.top - size.height / 2 }\n : chosen === \"right\"\n ? { left: point.left, top: point.top - size.height / 2 }\n : chosen === \"top\"\n ? { left: point.left - size.width / 2, top: point.top - size.height }\n : { left: point.left - size.width / 2, top: point.top };\n return {\n left: clamp(box.left, TOOLTIP_MARGIN, viewport.width - size.width - TOOLTIP_MARGIN),\n top: clamp(box.top, TOOLTIP_MARGIN, viewport.height - size.height - TOOLTIP_MARGIN),\n side: chosen,\n };\n}\n\nfunction PortalTooltip({\n label,\n side,\n className,\n redact,\n children,\n}: {\n label: ReactNode;\n side: TooltipSide;\n className?: string;\n redact?: boolean;\n children: ReactNode;\n}) {\n const triggerRef = useRef<HTMLSpanElement | null>(null);\n const bubbleRef = useRef<HTMLSpanElement | null>(null);\n const [visible, setVisible] = useState(false);\n // The measure + scroll/resize-tracking lifecycle is owned by useAnchoredRect;\n // here we only map the rect to a side-specific anchor point.\n const rect = useAnchoredRect(triggerRef, visible);\n // The bubble's own size and the window it has to fit in — neither of which is\n // knowable in render: the width is whatever the label wrapped to inside the\n // cap, and reading `window` while rendering is not a pure thing to do. Both\n // are taken in a LAYOUT effect, so the correction lands before the browser\n // paints and there is no frame in which the label sits off the screen.\n const [room, setRoom] = useState<{ size: TooltipSize; viewport: TooltipViewport } | null>(null);\n useLayoutEffect(() => {\n const measured = visible ? bubbleRef.current?.getBoundingClientRect() : undefined;\n // eslint-disable-next-line react-hooks/set-state-in-effect -- a measurement is the one thing a layout effect is for\n setRoom((previous) => {\n if (!measured) return null;\n const next = {\n size: { width: measured.width, height: measured.height },\n viewport: { width: window.innerWidth, height: window.innerHeight },\n };\n // Only publish what actually CHANGED: every re-measure allocates a fresh\n // object, and a new object on every scroll event would re-render the\n // bubble forever.\n return previous && sameRoom(previous, next) ? previous : next;\n });\n }, [visible, rect, label]);\n\n const point = rect ? tooltipAnchor(rect, side) : null;\n // Unmeasured on the very first pass, where the anchor point plus the side's\n // own transform is exactly what this always did. One layout effect later the\n // size is known and the placement is decided properly.\n const placed = rect && room ? placeTooltip(rect, side, room.size, room.viewport) : null;\n\n return (\n <>\n <span\n ref={triggerRef}\n className={cn(\"relative inline-flex\", className)}\n onMouseEnter={() => setVisible(true)}\n onMouseLeave={() => setVisible(false)}\n onFocus={() => setVisible(true)}\n onBlur={() => setVisible(false)}\n >\n {children}\n </span>\n {visible &&\n point &&\n typeof document !== \"undefined\" &&\n createPortal(\n <span\n ref={bubbleRef}\n role=\"tooltip\"\n data-private={redact ? \"\" : undefined}\n style={\n placed\n ? { position: \"fixed\", left: placed.left, top: placed.top }\n : {\n position: \"fixed\",\n left: point.left,\n top: point.top,\n transform: portalTransformBySide[side],\n }\n }\n className={cn(TOOLTIP_SURFACE, \"pointer-events-none z-50\")}\n >\n {label}\n </span>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"AAiFkC,wBAc9B,YAd8B;AAjFlC,SAAS,iBAAiB,QAAQ,gBAAgC;AAClE,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,uBAAwC;AAiBjD,MAAM,kBACJ;AAEF,MAAM,oBAAiD;AAAA,EACrD,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AACT;AAmCO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,SAAS;AAAA,EACT;AACF,GAQG;AAGD,MAAI,aAAa,KAAK,EAAG,QAAO,gCAAG,UAAS;AAC5C,MAAI,QAAQ;AACV,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE,qBAAC,UAAK,WAAW,GAAG,sCAAsC,SAAS,GAChE;AAAA;AAAA,IACD;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,gBAAc,SAAS,KAAK;AAAA,QAC5B,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,kBAAkB,IAAI;AAAA,QACxB;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAKA,SAAS,aAAa,OAA2B;AAC/C,SACE,SAAS,QACT,UAAU,SACT,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM;AAEnD;AAEA,MAAM,cAAc;AAIpB,MAAM,iBAAiB;AAEvB,MAAM,wBAAqD;AAAA,EACzD,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,QAAQ;AACV;AAIA,SAAS,cACP,GACA,MAC+B;AAC/B,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,EAAE,MAAM,EAAE,QAAQ,aAAa,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;AAAA,IAClE,KAAK;AACH,aAAO,EAAE,MAAM,EAAE,OAAO,aAAa,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;AAAA,IACjE,KAAK;AACH,aAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,KAAK,EAAE,MAAM,YAAY;AAAA,IAChE,KAAK;AACH,aAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,KAAK,EAAE,SAAS,YAAY;AAAA,EACrE;AACF;AAmBA,MAAM,WAA6C;AAAA,EACjD,MAAM;AAAA,EACN,OAAO;AAAA,EACP,KAAK;AAAA,EACL,QAAQ;AACV;AAGA,SAAS,OACP,GACA,MACA,MACA,UACS;AACT,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,EAAE,OAAO,cAAc,KAAK,SAAS;AAAA,IAC9C,KAAK;AACH,aAAO,EAAE,QAAQ,cAAc,KAAK,SAAS,SAAS,QAAQ;AAAA,IAChE,KAAK;AACH,aAAO,EAAE,MAAM,cAAc,KAAK,UAAU;AAAA,IAC9C,KAAK;AACH,aAAO,EAAE,SAAS,cAAc,KAAK,UAAU,SAAS,SAAS;AAAA,EACrE;AACF;AAEA,SAAS,SACP,GACA,GACS;AACT,SACE,EAAE,KAAK,UAAU,EAAE,KAAK,SACxB,EAAE,KAAK,WAAW,EAAE,KAAK,UACzB,EAAE,SAAS,UAAU,EAAE,SAAS,SAChC,EAAE,SAAS,WAAW,EAAE,SAAS;AAErC;AAEA,SAAS,MAAM,OAAe,KAAa,MAAsB;AAI/D,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,OAAO,IAAI,CAAC;AAC5C;AAwBO,SAAS,aACd,GACA,MACA,MACA,UACkB;AAClB,QAAM,SACJ,OAAO,GAAG,MAAM,MAAM,QAAQ,KAAK,CAAC,OAAO,GAAG,SAAS,IAAI,GAAG,MAAM,QAAQ,IACxE,OACA,SAAS,IAAI;AACnB,QAAM,QAAQ,cAAc,GAAG,MAAM;AACrC,QAAM,MACJ,WAAW,SACP,EAAE,MAAM,MAAM,OAAO,KAAK,OAAO,KAAK,MAAM,MAAM,KAAK,SAAS,EAAE,IAClE,WAAW,UACT,EAAE,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,KAAK,SAAS,EAAE,IACrD,WAAW,QACT,EAAE,MAAM,MAAM,OAAO,KAAK,QAAQ,GAAG,KAAK,MAAM,MAAM,KAAK,OAAO,IAClE,EAAE,MAAM,MAAM,OAAO,KAAK,QAAQ,GAAG,KAAK,MAAM,IAAI;AAC9D,SAAO;AAAA,IACL,MAAM,MAAM,IAAI,MAAM,gBAAgB,SAAS,QAAQ,KAAK,QAAQ,cAAc;AAAA,IAClF,KAAK,MAAM,IAAI,KAAK,gBAAgB,SAAS,SAAS,KAAK,SAAS,cAAc;AAAA,IAClF,MAAM;AAAA,EACR;AACF;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,aAAa,OAA+B,IAAI;AACtD,QAAM,YAAY,OAA+B,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAG5C,QAAM,OAAO,gBAAgB,YAAY,OAAO;AAMhD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAkE,IAAI;AAC9F,kBAAgB,MAAM;AACpB,UAAM,WAAW,UAAU,UAAU,SAAS,sBAAsB,IAAI;AAExE,YAAQ,CAAC,aAAa;AACpB,UAAI,CAAC,SAAU,QAAO;AACtB,YAAM,OAAO;AAAA,QACX,MAAM,EAAE,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO;AAAA,QACvD,UAAU,EAAE,OAAO,OAAO,YAAY,QAAQ,OAAO,YAAY;AAAA,MACnE;AAIA,aAAO,YAAY,SAAS,UAAU,IAAI,IAAI,WAAW;AAAA,IAC3D,CAAC;AAAA,EACH,GAAG,CAAC,SAAS,MAAM,KAAK,CAAC;AAEzB,QAAM,QAAQ,OAAO,cAAc,MAAM,IAAI,IAAI;AAIjD,QAAM,SAAS,QAAQ,OAAO,aAAa,MAAM,MAAM,KAAK,MAAM,KAAK,QAAQ,IAAI;AAEnF,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAW,GAAG,wBAAwB,SAAS;AAAA,QAC/C,cAAc,MAAM,WAAW,IAAI;AAAA,QACnC,cAAc,MAAM,WAAW,KAAK;AAAA,QACpC,SAAS,MAAM,WAAW,IAAI;AAAA,QAC9B,QAAQ,MAAM,WAAW,KAAK;AAAA,QAE7B;AAAA;AAAA,IACH;AAAA,IACC,WACC,SACA,OAAO,aAAa,eACpB;AAAA,MACE;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,gBAAc,SAAS,KAAK;AAAA,UAC5B,OACE,SACI,EAAE,UAAU,SAAS,MAAM,OAAO,MAAM,KAAK,OAAO,IAAI,IACxD;AAAA,YACE,UAAU;AAAA,YACV,MAAM,MAAM;AAAA,YACZ,KAAK,MAAM;AAAA,YACX,WAAW,sBAAsB,IAAI;AAAA,UACvC;AAAA,UAEN,WAAW,GAAG,iBAAiB,0BAA0B;AAAA,UAExD;AAAA;AAAA,MACH;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
@@ -0,0 +1,239 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode, ComponentPropsWithoutRef, InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
3
+
4
+ type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand";
5
+ /**
6
+ * Button classes for the rare case where the styling must land on a non-`<button>`
7
+ * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix
8
+ * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real
9
+ * button works, prefer `<Button>`. Draws from the same base + variant maps as
10
+ * `<Button>`, so the two stay in lockstep.
11
+ */
12
+ declare function buttonClasses(variant?: ButtonVariant, className?: string): string;
13
+ declare function Button({ variant, stretch, className, ...rest }: ButtonHTMLAttributes<HTMLButtonElement> & {
14
+ variant?: ButtonVariant;
15
+ stretch?: boolean;
16
+ }): react.JSX.Element;
17
+ declare const IconButton: react.ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
18
+ variant?: ButtonVariant;
19
+ /** Box size: md = 36px (matches the top bar), sm = 32px. The icon stays 20px. */
20
+ size?: "sm" | "md";
21
+ } & react.RefAttributes<HTMLButtonElement>>;
22
+ declare const FIELD_BASE: string;
23
+ /** Cancels FIELD_BASE's read-only treatment for a field that is `readOnly` for a
24
+ * reason other than "you may not edit this". Attribute-scoped for the same reason
25
+ * FIELD_BASE is — the two have to cancel on the identical selector or twMerge
26
+ * cannot make the later one win. */
27
+ declare const FIELD_WRITABLE_LOOK = "[&[readonly]]:bg-white [&[readonly]]:text-slate-900 dark:[&[readonly]]:bg-slate-900 dark:[&[readonly]]:text-slate-100";
28
+ declare const FIELD_FLOATING_PAD = "pt-4 pb-1";
29
+ declare const FIELD_INVALID = "border-rose-400 ring-1 ring-rose-400 focus:border-rose-500 focus:ring-rose-500 dark:border-rose-500/80 dark:ring-rose-500/80 dark:focus:border-rose-400";
30
+ declare const FLOATING_INPUT_CLASS: string;
31
+ /** The phone breakpoint the display treatment below keys off — the same one the
32
+ * numpad sheet uses, kept in one place so the two can't drift apart. */
33
+ declare const PHONE_QUERY = "(max-width: 767px)";
34
+ /**
35
+ * The PHONE display treatment (Keksdose feedback #176, carried across the entry
36
+ * forms by #179): the field chrome removed so the ONE input a form is actually
37
+ * about reads as the thing itself, not as another boxed row in a stack.
38
+ *
39
+ * Every control that takes `variant="display"` — {@link Input}, `NumberInput`,
40
+ * `AmountInput` — means exactly the same thing by it: the treatment applies below
41
+ * {@link PHONE_QUERY} and the field is untouched above it, so a caller never has
42
+ * to ask the viewport, and a form can't end up half-treated across breakpoints.
43
+ *
44
+ * What stays, deliberately:
45
+ * - A hairline baseline. With the box gone something still has to say "you can
46
+ * type here"; it takes the brand colour on focus, where a bordered field would
47
+ * light its whole outline. Each edge is set exactly once (`border-x-0
48
+ * border-t-0 border-b`) so the rule can't hinge on utility order.
49
+ * - The muted placeholder colour, because a display field is usually empty at
50
+ * the moment it matters most and has nothing else to show.
51
+ * - The label, moved to `sr-only` rather than dropped: display type is legible
52
+ * to the eye, not to a screen reader.
53
+ *
54
+ * Size and weight are NOT here — an amount wants display type, a subject line
55
+ * wants a heading — so each control adds its own on top.
56
+ */
57
+ declare const FIELD_DISPLAY = "block w-full border-x-0 border-t-0 border-b border-[var(--border)] bg-transparent px-0 pt-0 pb-1 text-[var(--text-primary)] shadow-none placeholder:text-slate-400 focus:border-[var(--brand)] focus:outline-none focus:ring-0 disabled:opacity-60 dark:placeholder:text-slate-500";
58
+ declare const FIELD_TRIGGER: string;
59
+ /** The dropdown chevron, shared by the native {@link Select} and every custom
60
+ * {@link FIELD_TRIGGER} control.
61
+ *
62
+ * Absolutely positioned and centred on the FIELD box. As an ordinary flex child
63
+ * it centres on the *content* box instead, and `FIELD_FLOATING_PAD` (pt-4 pb-1)
64
+ * pushes that box's midline down — so a labelled currency/multi-select chevron
65
+ * sat visibly lower than the native select's right beside it (feedback #400).
66
+ * Pair it with `pr-9` on the trigger so the value can't run underneath. */
67
+ declare function FieldChevron({ className }: {
68
+ className?: string;
69
+ }): react.JSX.Element;
70
+ declare const FLOATING_LABEL_CLASS: string;
71
+ declare const FLOATING_LABEL_STATIC: string;
72
+ /**
73
+ * The one place that assembles a labelled field: a `relative` wrapper around the
74
+ * control (`children`) plus a floating label inside the top strip. Pass `staticLabel`
75
+ * for controls that always have a value (selects); omit it for free-text fields whose
76
+ * label animates from centred→up. Used by Input/Select/Textarea/NumberInput; the
77
+ * dropdown controls that need a ref + menu keep their own wrapper but the same label
78
+ * (via {@link FieldLabel}) and trigger ({@link FIELD_TRIGGER}) styles.
79
+ */
80
+ declare function FloatingField({ className, htmlFor, label, staticLabel, srOnlyLabel, hint, children, }: {
81
+ className?: string;
82
+ htmlFor?: string;
83
+ label?: ReactNode;
84
+ staticLabel?: boolean;
85
+ /** Keep the label in the accessibility tree but out of the layout — what
86
+ * {@link FIELD_DISPLAY} needs, since a floating label inside a field with no
87
+ * field left would have nothing to float in. */
88
+ srOnlyLabel?: boolean;
89
+ /** Something interactive that belongs to the LABEL rather than to the value —
90
+ * in practice a {@link FieldHint} "?" (dev#468). It is rendered in a flex row
91
+ * with the label, so it is centred on the label's line by the layout instead
92
+ * of by a hand-tuned `top-…`, and it can never drift when the type changes.
93
+ *
94
+ * It rides an ANIMATED label too, and the two cases place it differently on
95
+ * purpose. A static label has a field-wide strip to itself, so the hint sits
96
+ * at the far end of it and a long label truncates into the gap. An animated
97
+ * one belongs to a control with something at the right edge of the field — a
98
+ * calculator, a stepper — so the hint follows the label instead, and it is
99
+ * the label that gives way (steering-design feedback #48). */
100
+ hint?: ReactNode;
101
+ children: ReactNode;
102
+ }): react.JSX.Element;
103
+ /**
104
+ * The "?" that explains a field, on the field's own label line (dev#468).
105
+ *
106
+ * Pass it to a labelled {@link Select} / {@link Input} as `hint`. It exists as a
107
+ * component rather than as a snippet each form repeats because the previous
108
+ * version was exactly that snippet — an absolutely-positioned button whose
109
+ * `top-1.5` was one guess at where an 11px label sits — and the reporter's
110
+ * follow-up was *"question mark is not centered. Is it part of the hoc?
111
+ * positioning problems seem quite frequently."* It was not part of the HOC. Now
112
+ * it is, and there is one place left where the answer can be wrong.
113
+ *
114
+ * A `<button>` rather than a bare icon: hover alone puts the explanation out of
115
+ * reach of a keyboard and of every touch device, and the tooltip shows on focus
116
+ * too. The text is also its accessible name, so a screen reader gets it without
117
+ * the bubble ever opening.
118
+ */
119
+ declare function FieldHint({ label, side }: {
120
+ label: string;
121
+ side?: "left" | "right" | "top" | "bottom";
122
+ }): react.JSX.Element;
123
+ /**
124
+ * The floating label for a custom-dropdown trigger (a `<span>`, since the trigger is a
125
+ * button not a labelable input). Same placement as {@link FloatingField}'s static label,
126
+ * so every labelled field lines up. Render inside a `relative` wrapper, before the trigger.
127
+ */
128
+ declare function FieldLabel({ children, className }: {
129
+ children: ReactNode;
130
+ className?: string;
131
+ }): react.JSX.Element;
132
+ declare const Input: react.ForwardRefExoticComponent<InputHTMLAttributes<HTMLInputElement> & {
133
+ label?: ReactNode;
134
+ /** Classes for the `<input>` itself, as distinct from `className`, which
135
+ * styles the field WRAPPER once a `label` turns this into a FloatingField.
136
+ * Without it a labelled Input had no way to reach its own element — so
137
+ * `tabular-nums` on a numeric text field, which NumberInput has supported
138
+ * all along through the identically named prop, was simply unavailable. */
139
+ inputClassName?: string;
140
+ /** {@link FIELD_DISPLAY} — on a phone, drop the chrome and set the value as a
141
+ * heading. For the one field a form is about (a feedback subject, an account
142
+ * name), never for a stack of them. Labelled fields only: the label is what
143
+ * the placeholder falls back to once it goes `sr-only`. */
144
+ variant?: "field" | "display";
145
+ /** The field is required and unanswered, or holds something that cannot be
146
+ * saved — {@link FIELD_INVALID}, the same rose border/ring `Select` has worn
147
+ * since feedback #235.
148
+ *
149
+ * It exists here because writing `aria-invalid` by hand did NOT do this. The
150
+ * attribute spreads onto the element and nothing styles it — there is no
151
+ * `[aria-invalid]` rule in this package or in either consumer's stylesheet —
152
+ * so three Keksdose dialogs flagged a mismatched passphrase to a screen reader
153
+ * and painted the field exactly as if it were fine. Setting the prop sets the
154
+ * attribute too, so the two can no longer be spelled separately. */
155
+ invalid?: boolean;
156
+ } & react.RefAttributes<HTMLInputElement>>;
157
+ declare const Select: react.ForwardRefExoticComponent<SelectHTMLAttributes<HTMLSelectElement> & {
158
+ label?: ReactNode;
159
+ invalid?: boolean;
160
+ /** A {@link FieldHint} for the label line — see {@link FloatingField}. */
161
+ hint?: ReactNode;
162
+ } & react.RefAttributes<HTMLSelectElement>>;
163
+ declare const Textarea: react.ForwardRefExoticComponent<TextareaHTMLAttributes<HTMLTextAreaElement> & {
164
+ label?: ReactNode;
165
+ /** See {@link Input}'s `invalid`. */
166
+ invalid?: boolean;
167
+ } & react.RefAttributes<HTMLTextAreaElement>>;
168
+ declare function Card({ className, children, flush, }: {
169
+ className?: string;
170
+ children: ReactNode;
171
+ /**
172
+ * When true, drop the card chrome (border, rounded corners, shadow) on mobile so the
173
+ * card spans edge-to-edge. Chrome reappears at the `md` breakpoint. Use for primary
174
+ * content cards on data pages; leave off for centered dialog/panel cards.
175
+ */
176
+ flush?: boolean;
177
+ }): react.JSX.Element;
178
+ declare function CardHeader({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
179
+ declare function CardTitle({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
180
+ declare function CardDescription({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
181
+ declare function CardAction({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
182
+ declare function CardContent({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
183
+ declare function CardFooter({ className, ...props }: ComponentPropsWithoutRef<"div">): react.JSX.Element;
184
+ declare function Spinner({ className }: {
185
+ className?: string;
186
+ }): react.JSX.Element;
187
+ declare function EmptyState({ title, hint, className, }: {
188
+ title: string;
189
+ hint?: string;
190
+ className?: string;
191
+ }): react.JSX.Element;
192
+ interface TabsProps<T extends string> {
193
+ /** `label` is a ReactNode so tabs can pair an icon with text; `badge` is an
194
+ * optional trailing node (e.g. a count pill). `href` marks a tab that IS a route:
195
+ * it renders as an anchor so it can be middle-/⌘-clicked into a new tab (Keksdose
196
+ * feedback #451), while a plain click still goes through `onChange` and stays
197
+ * client-side. Tabs that only flip local state leave it unset — a link to a URL
198
+ * that does not select the tab would be worse than no link. */
199
+ tabs: {
200
+ id: T;
201
+ label: ReactNode;
202
+ badge?: ReactNode;
203
+ href?: string;
204
+ }[];
205
+ active: T;
206
+ onChange: (id: T) => void;
207
+ className?: string;
208
+ /** Let the strip WRAP onto as many rows as it needs below `md`, instead of
209
+ * scrolling sideways — for a strip carrying more tabs than fit on a phone row.
210
+ * Keksdose live #262: ten reports on a 406px screen showed about three, and the
211
+ * round-2 answer (a <select> below `md`) cost a tap to open and a tap to choose.
212
+ * *"I would rather keep the tabs but have multirow tabs depending on the screen
213
+ * size, I will not loose the function to see all reports at once and navigate by
214
+ * single click rather by double click."*
215
+ *
216
+ * The active MARKER has to change with the layout, which is why this cannot be
217
+ * done from a call site with a `className`: the default marker is a `border-b-2`
218
+ * underline riding the container's own bottom rule, and a tab sitting in a row
219
+ * that does not touch that rule cannot wear it — every row but the last would
220
+ * show a stray line floating mid-strip, and `-mb-px` would pull each chip a
221
+ * pixel into the row beneath it. A wrapped strip marks the active tab with a
222
+ * filled brand chip instead, which is also the only thing findable at a glance
223
+ * among ten same-weight labels on three rows.
224
+ *
225
+ * From `md` up NOTHING changes: same single-line underline strip, same paddings,
226
+ * same colours as every other strip in the app.
227
+ *
228
+ * Opt-in on purpose. The three-tab strips (invoices, statements, bank imports)
229
+ * already fit a phone row, and turning those into chips would be an unrequested
230
+ * redesign of three other pages. */
231
+ wrap?: boolean;
232
+ /** Accessible name for the `role="tablist"` container. Every tab carries its own
233
+ * text, so this names the GROUP, not the tabs; leave it unset where a visible
234
+ * heading immediately above already does that job. */
235
+ label?: string;
236
+ }
237
+ declare function Tabs<T extends string>({ tabs, active, onChange, className, wrap, label }: TabsProps<T>): react.JSX.Element;
238
+
239
+ export { Button, type ButtonVariant, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, EmptyState, FIELD_BASE, FIELD_DISPLAY, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FLOATING_LABEL_CLASS, FLOATING_LABEL_STATIC, FieldChevron, FieldHint, FieldLabel, FloatingField, IconButton, Input, PHONE_QUERY, Select, Spinner, Tabs, type TabsProps, Textarea, buttonClasses };