@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.
- package/README.md +219 -0
- package/dist/components/account-settings.d.ts +71 -0
- package/dist/components/account-settings.js +159 -0
- package/dist/components/account-settings.js.map +1 -0
- package/dist/components/alert-banner.d.ts +20 -0
- package/dist/components/alert-banner.js +39 -0
- package/dist/components/alert-banner.js.map +1 -0
- package/dist/components/amount-input.d.ts +117 -0
- package/dist/components/amount-input.js +243 -0
- package/dist/components/amount-input.js.map +1 -0
- package/dist/components/calculator.d.ts +42 -0
- package/dist/components/calculator.js +161 -0
- package/dist/components/calculator.js.map +1 -0
- package/dist/components/chart.d.ts +58 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/combobox-core.d.ts +77 -0
- package/dist/components/combobox-core.js +263 -0
- package/dist/components/combobox-core.js.map +1 -0
- package/dist/components/combobox.d.ts +141 -0
- package/dist/components/combobox.js +550 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/currency-select.d.ts +37 -0
- package/dist/components/currency-select.js +131 -0
- package/dist/components/currency-select.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +28 -0
- package/dist/components/data-table-filter-popover.js +201 -0
- package/dist/components/data-table-filter-popover.js.map +1 -0
- package/dist/components/data-table-filters.d.ts +5 -0
- package/dist/components/data-table-filters.js +118 -0
- package/dist/components/data-table-filters.js.map +1 -0
- package/dist/components/data-table-labels.d.ts +33 -0
- package/dist/components/data-table-labels.js +53 -0
- package/dist/components/data-table-labels.js.map +1 -0
- package/dist/components/data-table-pagination.d.ts +18 -0
- package/dist/components/data-table-pagination.js +92 -0
- package/dist/components/data-table-pagination.js.map +1 -0
- package/dist/components/data-table-sort.d.ts +23 -0
- package/dist/components/data-table-sort.js +55 -0
- package/dist/components/data-table-sort.js.map +1 -0
- package/dist/components/data-table.d.ts +5 -0
- package/dist/components/data-table.js +926 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/date-picker.d.ts +85 -0
- package/dist/components/date-picker.js +253 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dropdown.d.ts +98 -0
- package/dist/components/dropdown.js +122 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +49 -0
- package/dist/components/entity-combobox.js +115 -0
- package/dist/components/entity-combobox.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +18 -0
- package/dist/components/file-dropzone.js +101 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +57 -0
- package/dist/components/full-bleed-dialog.js +68 -0
- package/dist/components/full-bleed-dialog.js.map +1 -0
- package/dist/components/grouped-picker.d.ts +37 -0
- package/dist/components/grouped-picker.js +87 -0
- package/dist/components/grouped-picker.js.map +1 -0
- package/dist/components/hover-menu.d.ts +16 -0
- package/dist/components/hover-menu.js +113 -0
- package/dist/components/hover-menu.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +33 -0
- package/dist/components/mini-calendar.js +133 -0
- package/dist/components/mini-calendar.js.map +1 -0
- package/dist/components/modal.d.ts +76 -0
- package/dist/components/modal.js +156 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +47 -0
- package/dist/components/multi-entity-combobox.js +116 -0
- package/dist/components/multi-entity-combobox.js.map +1 -0
- package/dist/components/multi-select.d.ts +39 -0
- package/dist/components/multi-select.js +132 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/number-input.d.ts +74 -0
- package/dist/components/number-input.js +114 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/numpad-sheet.d.ts +45 -0
- package/dist/components/numpad-sheet.js +102 -0
- package/dist/components/numpad-sheet.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +44 -0
- package/dist/components/picker-sheet.js +78 -0
- package/dist/components/picker-sheet.js.map +1 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/popover.js +46 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/search-field.d.ts +52 -0
- package/dist/components/search-field.js +64 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/settings-fields.d.ts +39 -0
- package/dist/components/settings-fields.js +17 -0
- package/dist/components/settings-fields.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +60 -0
- package/dist/components/swipeable-row.js +115 -0
- package/dist/components/swipeable-row.js.map +1 -0
- package/dist/components/toggle-group.d.ts +34 -0
- package/dist/components/toggle-group.js +90 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +86 -0
- package/dist/components/tooltip.js +171 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/ui.d.ts +239 -0
- package/dist/components/ui.js +512 -0
- package/dist/components/ui.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +31 -0
- package/dist/components/use-mobile-reveal.js +33 -0
- package/dist/components/use-mobile-reveal.js.map +1 -0
- package/dist/components/use-table-state.d.ts +54 -0
- package/dist/components/use-table-state.js +153 -0
- package/dist/components/use-table-state.js.map +1 -0
- package/dist/components/user-avatar.d.ts +23 -0
- package/dist/components/user-avatar.js +33 -0
- package/dist/components/user-avatar.js.map +1 -0
- package/dist/components/wizard-stepper.d.ts +14 -0
- package/dist/components/wizard-stepper.js +27 -0
- package/dist/components/wizard-stepper.js.map +1 -0
- package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
- package/dist/feedback/feedback-attachment.d.ts +63 -0
- package/dist/feedback/feedback-attachment.js +160 -0
- package/dist/feedback/feedback-attachment.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +72 -0
- package/dist/feedback/feedback-dialog.js +109 -0
- package/dist/feedback/feedback-dialog.js.map +1 -0
- package/dist/feedback/feedback-inbox.d.ts +226 -0
- package/dist/feedback/feedback-inbox.js +315 -0
- package/dist/feedback/feedback-inbox.js.map +1 -0
- package/dist/hooks/use-anchored-panel.d.ts +80 -0
- package/dist/hooks/use-anchored-panel.js +71 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -0
- package/dist/hooks/use-anchored-rect.d.ts +24 -0
- package/dist/hooks/use-anchored-rect.js +40 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
- package/dist/hooks/use-body-scroll-lock.js +41 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -0
- package/dist/hooks/use-close-transition.d.ts +41 -0
- package/dist/hooks/use-close-transition.js +36 -0
- package/dist/hooks/use-close-transition.js.map +1 -0
- package/dist/hooks/use-dismiss.d.ts +34 -0
- package/dist/hooks/use-dismiss.js +41 -0
- package/dist/hooks/use-dismiss.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +8 -0
- package/dist/hooks/use-media-query.js +18 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +10 -0
- package/dist/hooks/use-overlay-history.js +99 -0
- package/dist/hooks/use-overlay-history.js.map +1 -0
- package/dist/hooks/use-row-swipe.d.ts +30 -0
- package/dist/hooks/use-row-swipe.js +128 -0
- package/dist/hooks/use-row-swipe.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/calc.d.ts +89 -0
- package/dist/lib/calc.js +180 -0
- package/dist/lib/calc.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/dates.d.ts +84 -0
- package/dist/lib/dates.js +108 -0
- package/dist/lib/dates.js.map +1 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +23 -0
- package/dist/lib/safe-storage.js +20 -0
- package/dist/lib/safe-storage.js.map +1 -0
- package/dist/search/command-palette.d.ts +71 -0
- package/dist/search/command-palette.js +198 -0
- package/dist/search/command-palette.js.map +1 -0
- package/dist/shell/app-shell.d.ts +64 -0
- package/dist/shell/app-shell.js +237 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/option-switcher-menu.d.ts +24 -0
- package/dist/shell/option-switcher-menu.js +44 -0
- package/dist/shell/option-switcher-menu.js.map +1 -0
- package/dist/shell/role-switcher.d.ts +19 -0
- package/dist/shell/role-switcher.js +28 -0
- package/dist/shell/role-switcher.js.map +1 -0
- package/dist/shell/top-bar.d.ts +15 -0
- package/dist/shell/top-bar.js +29 -0
- package/dist/shell/top-bar.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +44 -0
- package/dist/shell/topbar-action-menu.js +49 -0
- package/dist/shell/topbar-action-menu.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +54 -0
- package/dist/shell/topbar-controls.js +152 -0
- package/dist/shell/topbar-controls.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +60 -0
- package/dist/theme/chart-palette.js +79 -0
- package/dist/theme/chart-palette.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +39 -0
- package/dist/theme/palette-presets.js +440 -0
- package/dist/theme/palette-presets.js.map +1 -0
- package/dist/theme/palette-store.d.ts +45 -0
- package/dist/theme/palette-store.js +52 -0
- package/dist/theme/palette-store.js.map +1 -0
- package/dist/theme/theme-store.d.ts +57 -0
- package/dist/theme/theme-store.js +96 -0
- package/dist/theme/theme-store.js.map +1 -0
- package/dist/tour/tour.d.ts +104 -0
- package/dist/tour/tour.js +351 -0
- package/dist/tour/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +30 -0
- package/dist/wizard/stepper-nav.js +146 -0
- package/dist/wizard/stepper-nav.js.map +1 -0
- package/dist/wizard/types.d.ts +124 -0
- package/dist/wizard/types.js +24 -0
- package/dist/wizard/types.js.map +1 -0
- package/dist/wizard/use-wizard.d.ts +6 -0
- package/dist/wizard/use-wizard.js +222 -0
- package/dist/wizard/use-wizard.js.map +1 -0
- package/dist/wizard/validation.d.ts +23 -0
- package/dist/wizard/validation.js +13 -0
- package/dist/wizard/validation.js.map +1 -0
- package/dist/wizard/wizard-context.d.ts +19 -0
- package/dist/wizard/wizard-context.js +15 -0
- package/dist/wizard/wizard-context.js.map +1 -0
- package/dist/wizard/wizard-step.d.ts +11 -0
- package/dist/wizard/wizard-step.js +12 -0
- package/dist/wizard/wizard-step.js.map +1 -0
- package/dist/wizard/wizard-summary.d.ts +18 -0
- package/dist/wizard/wizard-summary.js +41 -0
- package/dist/wizard/wizard-summary.js.map +1 -0
- package/package.json +87 -0
- 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":"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","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 };
|