@instruments/materia-ui 0.1.0 → 0.2.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/agent-docs.md +4 -3
- package/dist/components/button/button-variants.js +53 -8
- package/dist/components/button-group/button-group-root.js +8 -6
- package/dist/components/calendar/calendar-root.js +39 -13
- package/dist/components/chart/chart-tooltip.js +7 -5
- package/dist/components/empty/empty-media.js +5 -5
- package/dist/components/input-group/input-group-textarea.d.ts +3 -2
- package/dist/components/input-group/input-group-textarea.js +8 -4
- package/dist/components/pill/pill-variants.js +21 -10
- package/dist/components/progress/progress-root.d.ts +7 -5
- package/dist/components/progress/progress-root.js +26 -5
- package/dist/components/radio-group/radio-group-root.d.ts +7 -3
- package/dist/components/radio-group/radio-group-root.js +14 -4
- package/dist/components/textarea/textarea-root.d.ts +1 -1
- package/dist/components/textarea/textarea-root.js +6 -9
- package/dist/components/textarea/use-textarea-rows.d.ts +22 -0
- package/dist/components/textarea/use-textarea-rows.js +43 -0
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.d.ts +8 -6
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.js +11 -20
- package/dist/compositions/product-card/product-card-color-count.js +26 -26
- package/dist/compositions/product-card/product-card-compact-arm.js +2 -2
- package/dist/compositions/product-card/product-card-content-types.d.ts +5 -0
- package/dist/compositions/product-card/product-card-full-arm.js +2 -2
- package/dist/compositions/product-card/product-card-provider-types.d.ts +10 -0
- package/dist/compositions/product-card/product-card-provider.js +1 -1
- package/dist/compositions/product-card/product-card-swatch-selector.js +4 -3
- package/dist/data/docs-manifest.js +24 -24
- package/dist/styles/index.css +17 -6
- package/lint/tokens.mjs +1 -0
- package/package.json +1 -1
package/agent-docs.md
CHANGED
|
@@ -1259,10 +1259,11 @@ The component renders only the field stack - no card, heading, or CTA chrome. Co
|
|
|
1259
1259
|
|
|
1260
1260
|
`@instruments/materia-ui/components/progress`
|
|
1261
1261
|
|
|
1262
|
-
Linear progress bar showing completion from 0 to 100
|
|
1262
|
+
Linear progress bar showing completion from 0 to 100. Pass `value={null}` when the amount of work is unknown: a short bar slides across the track on a loop, and under reduced motion it holds still in the middle. Built on Base UI Progress for accessible semantics.
|
|
1263
1263
|
|
|
1264
|
-
- Use `Progress` for determinate 0–100 completion
|
|
1264
|
+
- Use `Progress` for determinate 0–100 completion and `value={null}` when the total is unknown; position in a stepped flow is `ProgressDots`.
|
|
1265
1265
|
- For a ring instead of a bar use `ProgressCircle` - it needs an explicit `label` for its native `<progress>` accessible name.
|
|
1266
|
+
- Mount `ReducedMotionProvider` at the app root - the indeterminate bar slides via `m.*` (LazyMotion).
|
|
1266
1267
|
|
|
1267
1268
|
## ProgressDots
|
|
1268
1269
|
|
|
@@ -1945,7 +1946,7 @@ The typography primitive for body copy - renders a `<p>` (or any element via `as
|
|
|
1945
1946
|
Renders a textarea element for multi-line text input (Base UI `Field.Control` rendering a `<textarea>`). Inside a Field it is the field's control: the FieldLabel targets it, FieldDescription/FieldError describe it, and it inherits the field's invalid state.
|
|
1946
1947
|
|
|
1947
1948
|
- Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
|
|
1948
|
-
- It auto-grows with content (`field-sizing-content`); set `rows` for
|
|
1949
|
+
- It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
|
|
1949
1950
|
- Give it a label - inside a `Field` a `FieldLabel` targets it with no ids to wire; standalone, point a `Label`'s `htmlFor` at its `id`. A placeholder is not an accessible name.
|
|
1950
1951
|
|
|
1951
1952
|
## ThinkingLoader
|
|
@@ -114,18 +114,37 @@ const NEUTRAL_SOFT = "border-border bg-secondary text-secondary-foreground";
|
|
|
114
114
|
const NEUTRAL_SOFT_GRADIENT = "before:bg-sheen-bright hover:before:bg-sheen-hover active:before:bg-none disabled:before:bg-none data-disabled:before:bg-none data-[hovered=true]:before:bg-sheen-hover data-[pressed=true]:before:bg-none dark:before:bg-sheen-dark";
|
|
115
115
|
const NEUTRAL_OUTLINE = "border-border bg-background text-foreground hover:bg-muted hover:text-accent-foreground data-[hovered=true]:bg-muted data-[hovered=true]:text-accent-foreground";
|
|
116
116
|
const NEUTRAL_GHOST = "border-transparent bg-transparent text-foreground shadow-none hover:bg-muted hover:text-accent-foreground data-[hovered=true]:bg-muted data-[hovered=true]:text-accent-foreground";
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
/* ── Link, shared by every tone: no fill at rest or on hover, so each tone
|
|
118
|
+
* only picks a text colour that reads on the page. ─────────────────── */
|
|
119
|
+
const LINK = "border-transparent bg-transparent underline-offset-4 shadow-none hover:bg-transparent hover:underline data-[hovered=true]:bg-transparent data-[hovered=true]:underline";
|
|
120
|
+
const LINK_FLAT = "shadow-none before:hidden active:scale-100 active:underline-offset-5 data-[pressed=true]:scale-100 data-[pressed=true]:underline-offset-5";
|
|
121
|
+
const NEUTRAL_LINK = `${LINK} text-foreground`;
|
|
119
122
|
const NEUTRAL_TRANSPARENT = "transform-gpu border-transparent bg-primary/50 backdrop-blur-md will-change-[filter,background-color] hover:bg-primary/60 data-[hovered=true]:bg-primary/60";
|
|
120
123
|
/* ── Danger tone (was `variant="destructive"`) modifiers ────────────── */
|
|
124
|
+
// Built like neutral soft. The hover pins the fill, since the danger tone
|
|
125
|
+
// darkens its solid fill on hover.
|
|
126
|
+
const DANGER_SOFT = "border-error-border bg-error-soft text-error-foreground hover:bg-error-soft data-[hovered=true]:bg-error-soft";
|
|
121
127
|
const DANGER_OUTLINE = "border-destructive text-destructive hover:bg-destructive/5 data-[hovered=true]:bg-destructive/5";
|
|
122
128
|
const DANGER_GHOST = "text-destructive hover:bg-destructive/10 hover:text-destructive data-[hovered=true]:bg-destructive/10 data-[hovered=true]:text-destructive";
|
|
123
|
-
const
|
|
129
|
+
const DANGER_LINK = `${LINK} text-destructive`;
|
|
130
|
+
// Half the danger fill over a light page is a pale red, so light mode takes
|
|
131
|
+
// dark text; over graphite it is mid-toned, so dark mode takes white, as
|
|
132
|
+
// Pill's status tones do.
|
|
133
|
+
const DANGER_TRANSPARENT = "transform-gpu border-transparent bg-destructive/50 text-foreground backdrop-blur-md will-change-[filter,background-color] hover:bg-destructive/60 data-[hovered=true]:bg-destructive/60 dark:text-white/90";
|
|
124
134
|
/* ── Brand tone (was `variant="brand"`) modifiers ───────────────────── */
|
|
125
135
|
const BRAND_SOLID_GRADIENT = "shadow-button-sheen before:bg-sheen-strong hover:before:bg-sheen-hover active:shadow-none active:before:bg-none disabled:shadow-none disabled:before:bg-none data-disabled:shadow-none data-disabled:before:bg-none data-[hovered=true]:before:bg-sheen-hover data-[pressed=true]:shadow-none data-[pressed=true]:before:bg-none";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
136
|
+
// Built like neutral soft, with Pill's brand soft colours. Dark brand soft is
|
|
137
|
+
// graphite, so the text there takes the brand accent.
|
|
138
|
+
const BRAND_SOFT = "bg-brand-soft text-brand-foreground dark:text-brand-accent";
|
|
139
|
+
// Without a fill, brand yellow is unreadable on the page, so the text takes
|
|
140
|
+
// `brand-strong`; the outline border can stay yellow.
|
|
141
|
+
const BRAND_OUTLINE = "border-brand text-brand-strong hover:bg-brand/5 data-[hovered=true]:bg-brand/5";
|
|
142
|
+
const BRAND_GHOST = "text-brand-strong hover:bg-brand/10 hover:text-brand-strong data-[hovered=true]:bg-brand/10 data-[hovered=true]:text-brand-strong";
|
|
143
|
+
const BRAND_LINK = `${LINK} text-brand-strong`;
|
|
144
|
+
// Over graphite, half the brand fill turns olive and the brand foreground
|
|
145
|
+
// stops reading, so dark mode thickens the fill rather than swapping the text,
|
|
146
|
+
// which keeps the brand and its foreground overridable as a pair.
|
|
147
|
+
const BRAND_TRANSPARENT = "transform-gpu border-transparent bg-brand/50 backdrop-blur-md will-change-[filter,background-color] hover:bg-brand/60 data-[hovered=true]:bg-brand/60 dark:bg-brand/70 dark:hover:bg-brand/80 dark:data-[hovered=true]:bg-brand/80";
|
|
129
148
|
/**
|
|
130
149
|
* buttonVariants - CVA helper for Button styling.
|
|
131
150
|
*
|
|
@@ -191,7 +210,7 @@ const buttonVariants = cva([
|
|
|
191
210
|
},
|
|
192
211
|
{
|
|
193
212
|
asInput: false,
|
|
194
|
-
class: `${NEUTRAL_LINK} ${SOLID_SURFACE} ${
|
|
213
|
+
class: `${NEUTRAL_LINK} ${SOLID_SURFACE} ${LINK_FLAT}`,
|
|
195
214
|
emphasis: "link",
|
|
196
215
|
tone: "neutral",
|
|
197
216
|
},
|
|
@@ -208,6 +227,12 @@ const buttonVariants = cva([
|
|
|
208
227
|
emphasis: "solid",
|
|
209
228
|
tone: "danger",
|
|
210
229
|
},
|
|
230
|
+
{
|
|
231
|
+
asInput: false,
|
|
232
|
+
class: `${DANGER_SOFT} ${SOLID_SURFACE} ${SOLID_SHADOW} ${NEUTRAL_SOFT_GRADIENT}`,
|
|
233
|
+
emphasis: "soft",
|
|
234
|
+
tone: "danger",
|
|
235
|
+
},
|
|
211
236
|
{
|
|
212
237
|
asInput: false,
|
|
213
238
|
class: `${SOLID_SHADOW} ${DANGER_OUTLINE}`,
|
|
@@ -226,6 +251,12 @@ const buttonVariants = cva([
|
|
|
226
251
|
emphasis: "transparent",
|
|
227
252
|
tone: "danger",
|
|
228
253
|
},
|
|
254
|
+
{
|
|
255
|
+
asInput: false,
|
|
256
|
+
class: `${DANGER_LINK} ${LINK_FLAT}`,
|
|
257
|
+
emphasis: "link",
|
|
258
|
+
tone: "danger",
|
|
259
|
+
},
|
|
229
260
|
/* ── Brand tone (base colours from variants.tone) ─────── */
|
|
230
261
|
{
|
|
231
262
|
asInput: false,
|
|
@@ -233,6 +264,12 @@ const buttonVariants = cva([
|
|
|
233
264
|
emphasis: "solid",
|
|
234
265
|
tone: "brand",
|
|
235
266
|
},
|
|
267
|
+
{
|
|
268
|
+
asInput: false,
|
|
269
|
+
class: `${BRAND_SOFT} ${SOLID_SURFACE} ${SOLID_SHADOW} ${NEUTRAL_SOFT_GRADIENT}`,
|
|
270
|
+
emphasis: "soft",
|
|
271
|
+
tone: "brand",
|
|
272
|
+
},
|
|
236
273
|
{
|
|
237
274
|
asInput: false,
|
|
238
275
|
class: `${SOLID_SHADOW} ${BRAND_OUTLINE}`,
|
|
@@ -251,6 +288,12 @@ const buttonVariants = cva([
|
|
|
251
288
|
emphasis: "transparent",
|
|
252
289
|
tone: "brand",
|
|
253
290
|
},
|
|
291
|
+
{
|
|
292
|
+
asInput: false,
|
|
293
|
+
class: `${BRAND_LINK} ${LINK_FLAT}`,
|
|
294
|
+
emphasis: "link",
|
|
295
|
+
tone: "brand",
|
|
296
|
+
},
|
|
254
297
|
// Input chips carry the solid drop-shadow (was `variant="default"`).
|
|
255
298
|
// asInput replaces every tone/emphasis surface (gated `asInput: false`).
|
|
256
299
|
{ asInput: true, class: SOLID_SHADOW },
|
|
@@ -281,10 +324,12 @@ const buttonVariants = cva([
|
|
|
281
324
|
// brand|danger>, emphasis is the full Emphasis plus Button's `link`
|
|
282
325
|
// extension. The `satisfies Record<…>` fails to compile if these keys
|
|
283
326
|
// drift from ButtonTone/ButtonEmphasis (declared below).
|
|
327
|
+
// Danger fades only to /95 on hover: a lighter red under the light
|
|
328
|
+
// foreground and the sheen drops below 4.5:1 at /90.
|
|
284
329
|
tone: {
|
|
285
330
|
neutral: "",
|
|
286
331
|
brand: "border-brand-accent/40 bg-brand text-brand-foreground focus-visible:ring-brand/25 data-[focused=true]:ring-brand/25",
|
|
287
|
-
danger: "border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/
|
|
332
|
+
danger: "border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/95 data-[hovered=true]:bg-destructive/95 focus-visible:ring-red-400/50 data-[focused=true]:ring-red-400/50",
|
|
288
333
|
},
|
|
289
334
|
emphasis: {
|
|
290
335
|
solid: "",
|
|
@@ -40,16 +40,18 @@ const buttonGroupVariants = cva("flex w-fit items-stretch *:focus-visible:relati
|
|
|
40
40
|
dividers: false,
|
|
41
41
|
variant: "segmented",
|
|
42
42
|
},
|
|
43
|
-
//
|
|
43
|
+
// A joined group keeps each child's own border and drops every trailing
|
|
44
|
+
// edge but the last, so neighbours share one line and an outline group
|
|
45
|
+
// reads as one continuous outline. Dividers recolour that shared edge.
|
|
44
46
|
{
|
|
45
|
-
class: "[
|
|
46
|
-
dividers: false,
|
|
47
|
+
class: "[&>*:not(:last-child)]:border-r-0",
|
|
47
48
|
orientation: "horizontal",
|
|
49
|
+
variant: "joined",
|
|
48
50
|
},
|
|
49
51
|
{
|
|
50
|
-
class: "[
|
|
51
|
-
dividers: false,
|
|
52
|
+
class: "[&>*:not(:last-child)]:border-b-0",
|
|
52
53
|
orientation: "vertical",
|
|
54
|
+
variant: "joined",
|
|
53
55
|
},
|
|
54
56
|
// Optional radius overrides (applies to non-separator items)
|
|
55
57
|
{
|
|
@@ -90,7 +92,7 @@ const buttonGroupVariants = cva("flex w-fit items-stretch *:focus-visible:relati
|
|
|
90
92
|
variant: {
|
|
91
93
|
/**
|
|
92
94
|
* Default "joined" behaviour - relies on each child for borders/background,
|
|
93
|
-
* and
|
|
95
|
+
* and collapses the borders and inner radii where neighbours meet.
|
|
94
96
|
*/
|
|
95
97
|
joined: "",
|
|
96
98
|
/**
|
|
@@ -2,16 +2,42 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
4
4
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
-
import { DayButton, DayPicker } from "react-day-picker";
|
|
5
|
+
import { Day, DayButton, DayPicker } from "react-day-picker";
|
|
6
6
|
import { buttonVariants } from "../button.js";
|
|
7
7
|
import { Icon } from "../icon.js";
|
|
8
8
|
const CalendarChevron = ({ orientation }) => (_jsx(Icon, { icon: orientation === "left" ? ChevronLeft : ChevronRight, size: "xs" }));
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The accent band behind a selected range, painted on the grid cell because
|
|
11
|
+
* only the cells meet edge to edge. Middle days fill their cell, rounded where
|
|
12
|
+
* a week starts or ends; the first and last days fill the half that faces the
|
|
13
|
+
* range, so the band runs into their filled button. A range of one day has no
|
|
14
|
+
* band.
|
|
13
15
|
*/
|
|
14
|
-
const
|
|
16
|
+
const rangeBandClasses = (modifiers) => {
|
|
17
|
+
const start = modifiers.range_start === true;
|
|
18
|
+
const end = modifiers.range_end === true;
|
|
19
|
+
if (modifiers.range_middle === true) {
|
|
20
|
+
return "bg-accent text-accent-foreground first:rounded-l-md last:rounded-r-md";
|
|
21
|
+
}
|
|
22
|
+
if (start && !end) {
|
|
23
|
+
return "bg-linear-to-r from-transparent from-50% to-accent to-50% last:rounded-r-md";
|
|
24
|
+
}
|
|
25
|
+
if (end && !start) {
|
|
26
|
+
return "bg-linear-to-l from-transparent from-50% to-accent to-50% first:rounded-l-md";
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
const CalendarDay = ({ className, modifiers, ...props }) => (_jsx(Day, { className: cn(className, rangeBandClasses(modifiers)), modifiers: modifiers, ...props }));
|
|
31
|
+
/**
|
|
32
|
+
* DayPicker marks state on the grid cell, not the button inside it, so the
|
|
33
|
+
* button carries its own copy to paint the day. A range's middle days are
|
|
34
|
+
* selected too, but sit on the cell's band instead of a fill; today is tinted
|
|
35
|
+
* unless it is filled.
|
|
36
|
+
*/
|
|
37
|
+
const CalendarDayButton = ({ modifiers, ...props }) => {
|
|
38
|
+
const filled = modifiers.selected === true && modifiers.range_middle !== true;
|
|
39
|
+
return (_jsx(DayButton, { "data-selected": filled, "data-today": modifiers.today === true && !filled, modifiers: modifiers, ...props }));
|
|
40
|
+
};
|
|
15
41
|
/**
|
|
16
42
|
* Date picker built on react-day-picker, styled with the shared button variants;
|
|
17
43
|
* supports `single`, `multiple`, and `range` selection via the `mode` prop.
|
|
@@ -26,9 +52,8 @@ const Calendar = ({ className, classNames, showOutsideDays = true, ...props }) =
|
|
|
26
52
|
button_next: cn(buttonVariants({ emphasis: "ghost", tone: "neutral" }), "text-muted-foreground hover:bg-accent hover:text-foreground size-8 p-0"),
|
|
27
53
|
button_previous: cn(buttonVariants({ emphasis: "ghost", tone: "neutral" }), "text-muted-foreground hover:bg-accent hover:text-foreground size-8 p-0"),
|
|
28
54
|
caption_label: "text-paragraph-sm font-medium",
|
|
29
|
-
day:
|
|
30
|
-
|
|
31
|
-
day_button: cn(buttonVariants({ emphasis: "ghost" }), "data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[selected=true]:hover:bg-primary data-[selected=true]:hover:text-primary-foreground size-8 rounded-md p-0 font-normal"),
|
|
55
|
+
day: "text-paragraph-sm relative w-9 p-0 text-center focus-within:relative focus-within:z-20",
|
|
56
|
+
day_button: cn(buttonVariants({ emphasis: "ghost" }), "data-[today=true]:bg-accent data-[today=true]:text-accent-foreground data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[selected=true]:hover:bg-primary data-[selected=true]:hover:text-primary-foreground size-8 rounded-md p-0 font-normal"),
|
|
32
57
|
disabled: "text-muted-foreground opacity-50",
|
|
33
58
|
hidden: "invisible",
|
|
34
59
|
month: "flex flex-col gap-4",
|
|
@@ -36,18 +61,19 @@ const Calendar = ({ className, classNames, showOutsideDays = true, ...props }) =
|
|
|
36
61
|
month_grid: "w-full border-collapse space-y-1",
|
|
37
62
|
months: "relative flex flex-col gap-2 sm:flex-row",
|
|
38
63
|
nav: "absolute inset-x-0 top-0 z-10 flex h-9 items-center justify-between px-1",
|
|
39
|
-
outside: "
|
|
40
|
-
range_end: "
|
|
41
|
-
range_middle: "
|
|
42
|
-
range_start: "
|
|
64
|
+
outside: "text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
|
65
|
+
range_end: "",
|
|
66
|
+
range_middle: "",
|
|
67
|
+
range_start: "",
|
|
43
68
|
selected: "",
|
|
44
|
-
today: "
|
|
69
|
+
today: "",
|
|
45
70
|
week: "mt-2 flex w-full",
|
|
46
71
|
weekday: "w-9 rounded-md text-calendar font-normal text-muted-foreground",
|
|
47
72
|
weekdays: "flex",
|
|
48
73
|
...classNames,
|
|
49
74
|
}, components: {
|
|
50
75
|
Chevron: CalendarChevron,
|
|
76
|
+
Day: CalendarDay,
|
|
51
77
|
DayButton: CalendarDayButton,
|
|
52
78
|
}, showOutsideDays: showOutsideDays, ...props }));
|
|
53
79
|
Calendar.displayName = "Calendar";
|
|
@@ -4,14 +4,13 @@ import { cn } from "@instruments/materia-ui/utils/cn";
|
|
|
4
4
|
import { isNumber, isString } from "../../utils/type-guards.js";
|
|
5
5
|
import { Flex } from "../flex.js";
|
|
6
6
|
import { Grid } from "../grid.js";
|
|
7
|
-
import { getPayloadConfigFromPayload, readDatumStringField, resolveConfigKey, useChart, } from "./chart-context.js";
|
|
7
|
+
import { getPayloadConfigFromPayload, readDatumStringField, resolveConfigKey, resolveEntryKey, useChart, } from "./chart-context.js";
|
|
8
8
|
/** recharts `Tooltip` - pair it with {@link ChartTooltipContent} via `content`. */
|
|
9
9
|
export { Tooltip as ChartTooltip } from "recharts";
|
|
10
10
|
/** One value cell inside the tooltip: swatch (or config icon) + label + value. */
|
|
11
|
-
const TooltipRow = ({ item, indicator, hideIndicator, nestLabel, nestedLabel, color, }) => {
|
|
11
|
+
const TooltipRow = ({ item, indicator, hideIndicator, nestLabel, nestedLabel, color, configKey, }) => {
|
|
12
12
|
const { config } = useChart();
|
|
13
|
-
const
|
|
14
|
-
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
13
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, configKey);
|
|
15
14
|
const swatchColor = color ?? readDatumStringField(item, "fill") ?? item.color;
|
|
16
15
|
const swatchStyle = {
|
|
17
16
|
"--color-bg": swatchColor,
|
|
@@ -58,5 +57,8 @@ export const ChartTooltipContent = ({ active, payload, className, indicator = "d
|
|
|
58
57
|
const items = payload.filter((item) => item.type !== "none");
|
|
59
58
|
const tooltipLabel = hideLabel ? null : (_jsx(TooltipLabel, { config: config, label: label, labelClassName: labelClassName, labelFormatter: labelFormatter, labelKey: labelKey, payload: payload }));
|
|
60
59
|
const nestLabel = items.length === 1 && indicator !== "dot";
|
|
61
|
-
return (_jsxs(Grid, { className: cn("bg-popover text-popover-foreground text-caption min-w-32 items-start gap-1.5 rounded-xl border px-2.5 py-1.5 shadow-md dark:shadow-none", className), columns: "none", "data-component": "chart-tooltip-content", "data-slot": "chart-tooltip-content", gap: "none", children: [nestLabel ? null : tooltipLabel, _jsx(Grid, { className: "gap-1.5", columns: "none", gap: "none", children: items.map((item) =>
|
|
60
|
+
return (_jsxs(Grid, { className: cn("bg-popover text-popover-foreground text-caption min-w-32 items-start gap-1.5 rounded-xl border px-2.5 py-1.5 shadow-md dark:shadow-none", className), columns: "none", "data-component": "chart-tooltip-content", "data-slot": "chart-tooltip-content", gap: "none", children: [nestLabel ? null : tooltipLabel, _jsx(Grid, { className: "gap-1.5", columns: "none", gap: "none", children: items.map((item) => {
|
|
61
|
+
const key = resolveConfigKey(nameKey, item.name, item.dataKey);
|
|
62
|
+
return (_jsx(TooltipRow, { color: color, configKey: key, hideIndicator: hideIndicator, indicator: indicator, item: item, nestedLabel: tooltipLabel, nestLabel: nestLabel }, resolveEntryKey(item, key)));
|
|
63
|
+
}) })] }));
|
|
62
64
|
};
|
|
@@ -16,17 +16,17 @@ const SIZE_MEDIA_MB = {
|
|
|
16
16
|
sm: "mb-1",
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Tone → text colour for the bare (plain) icon
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Tone → text colour for the bare (plain) icon, each at least 3:1 on the page.
|
|
20
|
+
* Brand and warning hues are too light on a light page, so they take
|
|
21
|
+
* `brand-strong` and the warning foreground.
|
|
22
22
|
*/
|
|
23
23
|
const PLAIN_ICON_TONE = {
|
|
24
|
-
brand: "text-brand-
|
|
24
|
+
brand: "text-brand-strong",
|
|
25
25
|
danger: "text-destructive",
|
|
26
26
|
info: "text-info-accent",
|
|
27
27
|
neutral: "text-muted-foreground",
|
|
28
28
|
success: "text-affirmative-accent",
|
|
29
|
-
warning: "text-
|
|
29
|
+
warning: "text-warning-foreground",
|
|
30
30
|
};
|
|
31
31
|
/** Bare-icon size (one step up from the boxed icon so it reads on its own). */
|
|
32
32
|
const PLAIN_ICON_SIZE = {
|
|
@@ -7,7 +7,8 @@ export type InputGroupTextareaProps = Omit<React.ComponentProps<"textarea">, "si
|
|
|
7
7
|
* Textarea element for use inside InputGroup (Base UI `Field.Control`
|
|
8
8
|
* rendering a `<textarea>`). Inherits disabled state from InputGroup context;
|
|
9
9
|
* inside a Field it is the field's control, and it carries `aria-invalid`
|
|
10
|
-
* when the group has an error.
|
|
10
|
+
* when the group has an error. It grows with its text, and `rows` sets its
|
|
11
|
+
* minimum height in lines.
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* ```tsx
|
|
@@ -19,4 +20,4 @@ export type InputGroupTextareaProps = Omit<React.ComponentProps<"textarea">, "si
|
|
|
19
20
|
* </InputGroup>
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
22
|
-
export declare const InputGroupTextarea: ({ className, testId, disabled: disabledProp, id, name, ...props }: InputGroupTextareaProps) => React.JSX.Element;
|
|
23
|
+
export declare const InputGroupTextarea: ({ className, testId, disabled: disabledProp, id, name, rows, style, ...props }: InputGroupTextareaProps) => React.JSX.Element;
|
|
@@ -4,11 +4,13 @@ import { Field as FieldPrimitive } from "@base-ui/react/field";
|
|
|
4
4
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
5
5
|
import { INPUT_CONTROL_RESET } from "../../tokens/input-shell.js";
|
|
6
6
|
import { useInputGroup } from "../input-group/input-group-context.js";
|
|
7
|
+
import { useTextareaRows } from "../textarea/use-textarea-rows.js";
|
|
7
8
|
/**
|
|
8
9
|
* Textarea element for use inside InputGroup (Base UI `Field.Control`
|
|
9
10
|
* rendering a `<textarea>`). Inherits disabled state from InputGroup context;
|
|
10
11
|
* inside a Field it is the field's control, and it carries `aria-invalid`
|
|
11
|
-
* when the group has an error.
|
|
12
|
+
* when the group has an error. It grows with its text, and `rows` sets its
|
|
13
|
+
* minimum height in lines.
|
|
12
14
|
*
|
|
13
15
|
* @example
|
|
14
16
|
* ```tsx
|
|
@@ -20,10 +22,12 @@ import { useInputGroup } from "../input-group/input-group-context.js";
|
|
|
20
22
|
* </InputGroup>
|
|
21
23
|
* ```
|
|
22
24
|
*/
|
|
23
|
-
export const InputGroupTextarea = ({ className, testId, disabled: disabledProp, id, name, ...props }) => {
|
|
25
|
+
export const InputGroupTextarea = ({ className, testId, disabled: disabledProp, id, name, rows, style, ...props }) => {
|
|
24
26
|
const { disabled: contextDisabled, hasError } = useInputGroup();
|
|
25
27
|
const disabled = disabledProp ?? contextDisabled;
|
|
28
|
+
const { className: sizingClassName, ref: sizingRef, style: sizingStyle, } = useTextareaRows(rows);
|
|
26
29
|
// The control owns id/name/disabled so Field can label and validate it;
|
|
27
|
-
// textarea-only props ride on the rendered element.
|
|
28
|
-
|
|
30
|
+
// textarea-only props ride on the rendered element. The sizing ref sits on
|
|
31
|
+
// the control so a caller's own `ref` on the textarea still lands.
|
|
32
|
+
return (_jsx(FieldPrimitive.Control, { disabled: disabled, ref: sizingRef, render: _jsx("textarea", { className: cn(INPUT_CONTROL_RESET, "text-paragraph flex resize-none px-0 py-2", sizingClassName, className), "data-component": "input-group-textarea", "data-slot": "input-group-control", rows: rows, style: { ...sizingStyle, ...style }, ...(hasError ? { "aria-invalid": true } : {}), ...(testId === undefined ? {} : { "data-testid": testId }), ...props }), ...(id === undefined ? {} : { id }), ...(name === undefined ? {} : { name }) }));
|
|
29
33
|
};
|
|
@@ -60,15 +60,19 @@ export const pillVariants = cva(cn("inline-flex items-center justify-center bord
|
|
|
60
60
|
emphasis: "solid",
|
|
61
61
|
tone: "success",
|
|
62
62
|
},
|
|
63
|
+
// The soft token is pale in light and graphite in dark, the reverse of
|
|
64
|
+
// the accent fill, so one class reads on it in both themes.
|
|
63
65
|
{
|
|
64
|
-
className: "text-
|
|
66
|
+
className: "text-info-soft",
|
|
65
67
|
emphasis: "solid",
|
|
66
68
|
tone: "info",
|
|
67
69
|
},
|
|
70
|
+
// Dark mode gives the warning foreground the accent's own colour, as text
|
|
71
|
+
// on graphite, so the solid takes the graphite soft token instead.
|
|
68
72
|
{
|
|
69
|
-
className: "text-
|
|
73
|
+
className: "dark:text-warning-soft",
|
|
70
74
|
emphasis: "solid",
|
|
71
|
-
tone: "
|
|
75
|
+
tone: "warning",
|
|
72
76
|
},
|
|
73
77
|
/* ── Soft (was `light`) ───────────────────────────── */
|
|
74
78
|
/* neutral+soft reproduces the old secondary/solid render */
|
|
@@ -152,8 +156,10 @@ export const pillVariants = cva(cn("inline-flex items-center justify-center bord
|
|
|
152
156
|
emphasis: "ghost",
|
|
153
157
|
tone: "success",
|
|
154
158
|
},
|
|
159
|
+
// Amber is too light to read on a light page, so light mode takes the
|
|
160
|
+
// warning foreground.
|
|
155
161
|
{
|
|
156
|
-
className: "text-warning-
|
|
162
|
+
className: "text-warning-foreground dark:text-warning-accent",
|
|
157
163
|
emphasis: "ghost",
|
|
158
164
|
tone: "warning",
|
|
159
165
|
},
|
|
@@ -167,8 +173,9 @@ export const pillVariants = cva(cn("inline-flex items-center justify-center bord
|
|
|
167
173
|
emphasis: "ghost",
|
|
168
174
|
tone: "danger",
|
|
169
175
|
},
|
|
176
|
+
// Brand accent fails on a light page; graphite keeps the accent.
|
|
170
177
|
{
|
|
171
|
-
className: "text-brand-
|
|
178
|
+
className: "text-brand-strong dark:text-brand-accent",
|
|
172
179
|
emphasis: "ghost",
|
|
173
180
|
tone: "brand",
|
|
174
181
|
},
|
|
@@ -178,28 +185,32 @@ export const pillVariants = cva(cn("inline-flex items-center justify-center bord
|
|
|
178
185
|
emphasis: "transparent",
|
|
179
186
|
tone: "neutral",
|
|
180
187
|
},
|
|
188
|
+
// A half status fill is pale over a light page and mid-toned over
|
|
189
|
+
// graphite, so status tones take dark text in light and white in dark.
|
|
181
190
|
{
|
|
182
|
-
className: "bg-affirmative-accent/50 transform-gpu border-transparent
|
|
191
|
+
className: "bg-affirmative-accent/50 text-foreground transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color] dark:text-white/90",
|
|
183
192
|
emphasis: "transparent",
|
|
184
193
|
tone: "success",
|
|
185
194
|
},
|
|
186
195
|
{
|
|
187
|
-
className: "bg-warning-accent/50 transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color]",
|
|
196
|
+
className: "bg-warning-accent/50 transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color] dark:text-white/90",
|
|
188
197
|
emphasis: "transparent",
|
|
189
198
|
tone: "warning",
|
|
190
199
|
},
|
|
191
200
|
{
|
|
192
|
-
className: "bg-info-accent/50 transform-gpu border-transparent
|
|
201
|
+
className: "bg-info-accent/50 text-foreground transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color] dark:text-white/90",
|
|
193
202
|
emphasis: "transparent",
|
|
194
203
|
tone: "info",
|
|
195
204
|
},
|
|
196
205
|
{
|
|
197
|
-
className: "bg-destructive/50 transform-gpu border-transparent
|
|
206
|
+
className: "bg-destructive/50 text-foreground transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color] dark:text-white/90",
|
|
198
207
|
emphasis: "transparent",
|
|
199
208
|
tone: "danger",
|
|
200
209
|
},
|
|
210
|
+
// Over graphite a half brand fill turns olive, so dark mode thickens it
|
|
211
|
+
// and keeps the brand foreground.
|
|
201
212
|
{
|
|
202
|
-
className: "bg-brand/50 transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color]",
|
|
213
|
+
className: "bg-brand/50 dark:bg-brand/70 transform-gpu border-transparent backdrop-blur-md will-change-[filter,background-color]",
|
|
203
214
|
emphasis: "transparent",
|
|
204
215
|
tone: "brand",
|
|
205
216
|
},
|
|
@@ -21,15 +21,17 @@ interface ProgressCircleProps {
|
|
|
21
21
|
valueFormatter?: (value: number, max: number) => string;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Linear progress bar showing completion from 0 to 100
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Linear progress bar showing completion from 0 to 100. Pass `value={null}`
|
|
25
|
+
* when the amount of work is unknown: a short bar slides across the track on a
|
|
26
|
+
* loop, and under reduced motion it holds still in the middle. Built on Base
|
|
27
|
+
* UI Progress for accessible semantics.
|
|
27
28
|
*
|
|
28
|
-
* @bestPractice Use `Progress` for determinate 0–100 completion
|
|
29
|
+
* @bestPractice Use `Progress` for determinate 0–100 completion and `value={null}` when the total is unknown; position in a stepped flow is `ProgressDots`.
|
|
29
30
|
* @bestPractice For a ring instead of a bar use `ProgressCircle` - it needs an explicit `label` for its native `<progress>` accessible name.
|
|
31
|
+
* @bestPractice Mount `ReducedMotionProvider` at the app root - the indeterminate bar slides via `m.*` (LazyMotion).
|
|
30
32
|
* @category feedback
|
|
31
33
|
*/
|
|
32
|
-
declare const Progress: ({ className, ...props }: ProgressProps) => React.JSX.Element;
|
|
34
|
+
declare const Progress: ({ className, value, ...props }: ProgressProps) => React.JSX.Element;
|
|
33
35
|
/**
|
|
34
36
|
* Circular progress ring with an optional centred content slot; the value is
|
|
35
37
|
* clamped to `0..max` and exposed through a visually hidden native `<progress>`.
|
|
@@ -2,17 +2,38 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
|
|
4
4
|
import { Center } from "@instruments/materia-ui/components/center";
|
|
5
|
+
import { durations } from "@instruments/materia-ui/tokens/motion";
|
|
5
6
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
7
|
+
import { useReducedMotion } from "motion/react";
|
|
8
|
+
import * as m from "motion/react-m";
|
|
9
|
+
// The indeterminate bar is a third of the track, so translating it by its own
|
|
10
|
+
// width runs it from just off the left edge (-100%) to just off the right
|
|
11
|
+
// (300%).
|
|
12
|
+
const SLIDE = { x: ["-100%", "300%"] };
|
|
13
|
+
const SLIDE_TRANSITION = {
|
|
14
|
+
duration: durations.slow,
|
|
15
|
+
ease: "linear",
|
|
16
|
+
repeat: Number.POSITIVE_INFINITY,
|
|
17
|
+
};
|
|
6
18
|
/**
|
|
7
|
-
* Linear progress bar showing completion from 0 to 100
|
|
8
|
-
*
|
|
9
|
-
*
|
|
19
|
+
* Linear progress bar showing completion from 0 to 100. Pass `value={null}`
|
|
20
|
+
* when the amount of work is unknown: a short bar slides across the track on a
|
|
21
|
+
* loop, and under reduced motion it holds still in the middle. Built on Base
|
|
22
|
+
* UI Progress for accessible semantics.
|
|
10
23
|
*
|
|
11
|
-
* @bestPractice Use `Progress` for determinate 0–100 completion
|
|
24
|
+
* @bestPractice Use `Progress` for determinate 0–100 completion and `value={null}` when the total is unknown; position in a stepped flow is `ProgressDots`.
|
|
12
25
|
* @bestPractice For a ring instead of a bar use `ProgressCircle` - it needs an explicit `label` for its native `<progress>` accessible name.
|
|
26
|
+
* @bestPractice Mount `ReducedMotionProvider` at the app root - the indeterminate bar slides via `m.*` (LazyMotion).
|
|
13
27
|
* @category feedback
|
|
14
28
|
*/
|
|
15
|
-
const Progress = ({ className, ...props }) =>
|
|
29
|
+
const Progress = ({ className, value, ...props }) => {
|
|
30
|
+
// Gate on the preference directly: MotionConfig's reduced mode would jump
|
|
31
|
+
// the slide to its last keyframe, which leaves the bar off the track.
|
|
32
|
+
const reducedMotion = useReducedMotion() === true;
|
|
33
|
+
const indeterminate = value === null;
|
|
34
|
+
const sliding = indeterminate && !reducedMotion;
|
|
35
|
+
return (_jsx(ProgressPrimitive.Root, { className: cn("bg-muted relative block h-2 w-full overflow-hidden rounded-full", className), "data-component": "progress", "data-slot": "progress", value: value, ...props, children: _jsx(ProgressPrimitive.Track, { className: "block size-full", "data-component": "progress-track", "data-slot": "progress-track", children: _jsx(ProgressPrimitive.Indicator, { className: cn("bg-primary transition-width block h-full", indeterminate && "w-1/3 rounded-full", indeterminate && !sliding && "translate-x-full"), "data-component": "progress-indicator", "data-slot": "progress-indicator", render: sliding ? (_jsx(m.div, { animate: SLIDE, transition: SLIDE_TRANSITION })) : (_jsx("div", {})) }) }) }));
|
|
36
|
+
};
|
|
16
37
|
const clamp = (value, min, max) => {
|
|
17
38
|
if (Number.isNaN(value)) {
|
|
18
39
|
return min;
|
|
@@ -30,13 +30,17 @@ declare const RadioGroup: ({ className, ...props }: RadioGroupPrimitive.Props) =
|
|
|
30
30
|
export type RadioGroupItemProps = RadioProps & {
|
|
31
31
|
/** Label text for the radio option */
|
|
32
32
|
children: React.ReactNode;
|
|
33
|
+
/** Optional helper text below the label; wired to the radio via `aria-describedby`. */
|
|
34
|
+
description?: React.ReactNode;
|
|
33
35
|
};
|
|
34
36
|
/**
|
|
35
|
-
* A radio option with an associated label
|
|
37
|
+
* A radio option with an associated label and optional description. Combines
|
|
38
|
+
* Radio, Label and muted helper text, laid out like `CheckboxField`.
|
|
36
39
|
*
|
|
37
40
|
* @param props - The radio group item props
|
|
38
41
|
* @param props.value - Unique value for this radio option (required).
|
|
39
42
|
* @param props.children - Label text content.
|
|
43
|
+
* @param props.description - Helper text below the label, linked to the radio via `aria-describedby`.
|
|
40
44
|
* @param props.size - Size using shared ComponentSize scale. Defaults to "sm".
|
|
41
45
|
* @param props.disabled - Disable this radio option. Defaults to false.
|
|
42
46
|
* @param props.className - Additional CSS classes for the radio input.
|
|
@@ -46,9 +50,9 @@ export type RadioGroupItemProps = RadioProps & {
|
|
|
46
50
|
* ```tsx
|
|
47
51
|
* <RadioGroup>
|
|
48
52
|
* <RadioGroupItem value="a">Option A</RadioGroupItem>
|
|
49
|
-
* <RadioGroupItem value="b" disabled>Option B</RadioGroupItem>
|
|
53
|
+
* <RadioGroupItem value="b" description="Arrives tomorrow" disabled>Option B</RadioGroupItem>
|
|
50
54
|
* </RadioGroup>
|
|
51
55
|
* ```
|
|
52
56
|
*/
|
|
53
|
-
declare const RadioGroupItem: ({ children, className, id: providedId, disabled, ...props }: RadioGroupItemProps) => React.JSX.Element;
|
|
57
|
+
declare const RadioGroupItem: ({ children, description, className, id: providedId, disabled, "aria-describedby": ariaDescribedBy, ...props }: RadioGroupItemProps) => React.JSX.Element;
|
|
54
58
|
export { RadioGroup, RadioGroupItem };
|
|
@@ -5,8 +5,10 @@ import { cn } from "@instruments/materia-ui/utils/cn";
|
|
|
5
5
|
import { useId } from "react";
|
|
6
6
|
import { CONTROL_LINE_CLASSES } from "../../tokens/control-line.js";
|
|
7
7
|
import { Flex } from "../flex.js";
|
|
8
|
+
import { Grid } from "../grid.js";
|
|
8
9
|
import { Label } from "../label.js";
|
|
9
10
|
import { Radio } from "../radio.js";
|
|
11
|
+
import { Text } from "../text.js";
|
|
10
12
|
/**
|
|
11
13
|
* Container for a group of radio buttons. Manages single selection state.
|
|
12
14
|
*
|
|
@@ -34,11 +36,13 @@ import { Radio } from "../radio.js";
|
|
|
34
36
|
*/
|
|
35
37
|
const RadioGroup = ({ className, ...props }) => (_jsx(RadioGroupPrimitive, { className: cn("grid gap-3", className), "data-component": "radio-group", "data-slot": "radio-group", ...props }));
|
|
36
38
|
/**
|
|
37
|
-
* A radio option with an associated label
|
|
39
|
+
* A radio option with an associated label and optional description. Combines
|
|
40
|
+
* Radio, Label and muted helper text, laid out like `CheckboxField`.
|
|
38
41
|
*
|
|
39
42
|
* @param props - The radio group item props
|
|
40
43
|
* @param props.value - Unique value for this radio option (required).
|
|
41
44
|
* @param props.children - Label text content.
|
|
45
|
+
* @param props.description - Helper text below the label, linked to the radio via `aria-describedby`.
|
|
42
46
|
* @param props.size - Size using shared ComponentSize scale. Defaults to "sm".
|
|
43
47
|
* @param props.disabled - Disable this radio option. Defaults to false.
|
|
44
48
|
* @param props.className - Additional CSS classes for the radio input.
|
|
@@ -48,13 +52,19 @@ const RadioGroup = ({ className, ...props }) => (_jsx(RadioGroupPrimitive, { cla
|
|
|
48
52
|
* ```tsx
|
|
49
53
|
* <RadioGroup>
|
|
50
54
|
* <RadioGroupItem value="a">Option A</RadioGroupItem>
|
|
51
|
-
* <RadioGroupItem value="b" disabled>Option B</RadioGroupItem>
|
|
55
|
+
* <RadioGroupItem value="b" description="Arrives tomorrow" disabled>Option B</RadioGroupItem>
|
|
52
56
|
* </RadioGroup>
|
|
53
57
|
* ```
|
|
54
58
|
*/
|
|
55
|
-
const RadioGroupItem = ({ children, className, id: providedId, disabled, ...props }) => {
|
|
59
|
+
const RadioGroupItem = ({ children, description, className, id: providedId, disabled, "aria-describedby": ariaDescribedBy, ...props }) => {
|
|
56
60
|
const generatedId = useId();
|
|
61
|
+
const descriptionId = useId();
|
|
57
62
|
const id = providedId ?? generatedId;
|
|
58
|
-
|
|
63
|
+
const hasDescription = description !== undefined && description !== null;
|
|
64
|
+
// Keep any ids the caller passed, and add the description's own.
|
|
65
|
+
const describedBy = hasDescription
|
|
66
|
+
? [ariaDescribedBy, descriptionId].join(" ").trim()
|
|
67
|
+
: ariaDescribedBy;
|
|
68
|
+
return (_jsxs(Flex, { align: "flex-start", "data-component": "radio-group-item", "data-disabled": disabled, "data-slot": "radio-group-item", gap: "sm", children: [_jsx("span", { className: cn(CONTROL_LINE_CLASSES, "text-paragraph-sm"), children: _jsx(Radio, { "aria-describedby": describedBy, className: className, disabled: disabled, id: id, ...props }) }), _jsxs(Grid, { columns: "none", gap: "2xs", children: [_jsx(Label, { className: cn("cursor-pointer", disabled === true ? "cursor-not-allowed opacity-60" : null), htmlFor: id, children: children }), hasDescription ? (_jsx(Text, { id: descriptionId, size: "paragraph-sm", variant: "muted", children: description })) : null] })] }));
|
|
59
69
|
};
|
|
60
70
|
export { RadioGroup, RadioGroupItem };
|
|
@@ -24,7 +24,7 @@ interface TextareaProps extends WithTestId<React.ComponentProps<"textarea">> {
|
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
26
|
* @bestPractice Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
|
|
27
|
-
* @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for
|
|
27
|
+
* @bestPractice It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
|
|
28
28
|
* @bestPractice Give it a label - inside a `Field` a `FieldLabel` targets it with no ids to wire; standalone, point a `Label`'s `htmlFor` at its `id`. A placeholder is not an accessible name.
|
|
29
29
|
* @category forms
|
|
30
30
|
*/
|