@cueplusplus/ui 0.5.0 → 0.7.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/CHANGELOG.md +53 -0
- package/dist/chrome/_drag.js +59 -0
- package/dist/chrome/_status-bar-item.js +85 -0
- package/dist/chrome/_status-bar.d.ts +15 -0
- package/dist/chrome/_status-bar.js +171 -0
- package/dist/chrome/app-shell.d.ts +69 -1
- package/dist/chrome/app-shell.js +38 -11
- package/dist/chrome/index.d.ts +3 -2
- package/dist/chrome/status-bar.d.ts +87 -4
- package/dist/chrome/status-bar.js +80 -16
- package/dist/chrome/title-bar.d.ts +11 -1
- package/dist/chrome/title-bar.js +1 -3
- package/dist/date/_segments.js +15 -4
- package/dist/forms/_chassis.d.ts +104 -4
- package/dist/forms/_chassis.js +104 -4
- package/dist/forms/input-group.d.ts +8 -3
- package/dist/forms/input-group.js +8 -3
- package/dist/forms/number-field.d.ts +3 -2
- package/dist/forms/number-field.js +3 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/dist/instruments/_data-row.d.ts +68 -0
- package/dist/instruments/_data-row.js +139 -0
- package/dist/instruments/data-row.d.ts +94 -0
- package/dist/instruments/data-row.js +151 -0
- package/dist/instruments/data-tree.d.ts +126 -0
- package/dist/instruments/data-tree.js +303 -0
- package/dist/instruments/index.d.ts +6 -2
- package/dist/instruments/index.js +5 -1
- package/dist/instruments/ledger.d.ts +105 -0
- package/dist/instruments/ledger.js +114 -0
- package/dist/instruments/table.d.ts +108 -2
- package/dist/instruments/table.js +165 -39
- package/dist/theming/_presets.js +3 -3
- package/manifest/components/app-shell.json +45 -4
- package/manifest/components/autocomplete.json +2 -0
- package/manifest/components/color-field.json +2 -0
- package/manifest/components/color-picker.json +2 -0
- package/manifest/components/combobox.json +2 -0
- package/manifest/components/composer.json +2 -0
- package/manifest/components/data-row.json +208 -0
- package/manifest/components/data-tree.json +151 -0
- package/manifest/components/date-field.json +2 -0
- package/manifest/components/date-picker.json +2 -0
- package/manifest/components/date-range-picker.json +2 -0
- package/manifest/components/env-var-input.json +2 -0
- package/manifest/components/input-group.json +4 -2
- package/manifest/components/input.json +3 -1
- package/manifest/components/ledger.json +187 -0
- package/manifest/components/multi-select.json +2 -0
- package/manifest/components/musical-time-input.json +2 -0
- package/manifest/components/number-field.json +3 -1
- package/manifest/components/otp-field.json +2 -0
- package/manifest/components/password-input.json +2 -0
- package/manifest/components/scrub-input.json +2 -0
- package/manifest/components/search-input.json +2 -0
- package/manifest/components/select.json +1 -0
- package/manifest/components/status-bar.json +146 -12
- package/manifest/components/table-scroll-region.json +6 -0
- package/manifest/components/table.json +113 -5
- package/manifest/components/tags-input.json +2 -0
- package/manifest/components/textarea.json +3 -1
- package/manifest/components/time-field.json +2 -0
- package/manifest/components/title-bar.json +1 -1
- package/manifest/components/toggle-group.json +2 -0
- package/manifest/components/toggle.json +2 -0
- package/manifest/manifest.json +72 -33
- package/manifest/tokens.json +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { StatusBarProminence } from "./_status-bar.js";
|
|
1
2
|
import * as React from "react";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
4
|
//#region src/chrome/status-bar.d.ts
|
|
3
5
|
/**
|
|
4
6
|
* Ink for a status-bar item. `neutral` inherits the bar's own subtle grey; the
|
|
@@ -19,28 +21,109 @@ interface StatusBarItemProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
|
19
21
|
push?: boolean;
|
|
20
22
|
/** Optional leading icon (e.g. a lucide icon), drawn `aria-hidden`. */
|
|
21
23
|
icon?: React.ElementType;
|
|
24
|
+
/**
|
|
25
|
+
* Make the item a real control: a `<button type="button">` with hover, press
|
|
26
|
+
* and keyboard-focus feedback. Defaults to `false`.
|
|
27
|
+
*
|
|
28
|
+
* A native button, not a `<div>` with an `onClick` — keyboard order is DOM
|
|
29
|
+
* order, `Enter` and `Space` work because the platform makes them work, and
|
|
30
|
+
* assistive technology is told what the thing is by the element rather than
|
|
31
|
+
* by a role attribute that has to be kept honest by hand.
|
|
32
|
+
*
|
|
33
|
+
* **An icon-only interactive item needs an `aria-label`.** There is no text
|
|
34
|
+
* for a screen reader to read and the icon is drawn `aria-hidden`, so without
|
|
35
|
+
* one the control announces as "button" and nothing else.
|
|
36
|
+
*/
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* How loudly the item states itself. Defaults to `"quiet"`.
|
|
40
|
+
*
|
|
41
|
+
* `quiet` is a reading: tone ink on the bar's own ground. `filled` is a chip
|
|
42
|
+
* — a full-height tone wash behind tone ink — for the small set of states
|
|
43
|
+
* that earn a colour of their own: a warning, an error, the remote you are
|
|
44
|
+
* attached to. It is loud deliberately, and a bar of five filled items is a
|
|
45
|
+
* bar nobody reads.
|
|
46
|
+
*/
|
|
47
|
+
prominence?: StatusBarProminence;
|
|
48
|
+
/**
|
|
49
|
+
* Replace the rendered element (Base UI render prop) — `render={<a href="…" />}`
|
|
50
|
+
* for a status item that navigates.
|
|
51
|
+
*
|
|
52
|
+
* Implies {@link StatusBarItemProps.interactive}: whatever is rendered gets
|
|
53
|
+
* the control chassis, because an element a reader can click has to look like
|
|
54
|
+
* one whether it is a button or a link.
|
|
55
|
+
*/
|
|
56
|
+
render?: useRender.RenderProp;
|
|
57
|
+
/**
|
|
58
|
+
* Grey the control out, take it out of the pointer's reach and announce it as
|
|
59
|
+
* disabled. Defaults to `false`. Ignored by a static item, which is a reading
|
|
60
|
+
* and not a control.
|
|
61
|
+
*
|
|
62
|
+
* Honoured whatever {@link StatusBarItemProps.render} renders, but by two
|
|
63
|
+
* routes: the `disabled` attribute where it means something — the default
|
|
64
|
+
* `<button>`, a form control, a component that takes a `disabled` prop of its
|
|
65
|
+
* own — and `aria-disabled` plus `data-disabled` everywhere, which is all an
|
|
66
|
+
* anchor gets. There is no such thing as a disabled link; a link that should
|
|
67
|
+
* not be followed should not be a link, and this states the intent rather
|
|
68
|
+
* than pretending to enforce it.
|
|
69
|
+
*/
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
}
|
|
72
|
+
interface StatusBarGroupProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
73
|
+
/**
|
|
74
|
+
* Start the trailing cluster at this group: pushes it and everything after it
|
|
75
|
+
* to the far edge, exactly as `push` does on a single item.
|
|
76
|
+
*/
|
|
77
|
+
push?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* What the cluster is called. Optional, and worth giving whenever the members
|
|
80
|
+
* only make sense together — "Repository", "Connection" — because a `group`
|
|
81
|
+
* with no name is a landmark a screen reader reads straight past.
|
|
82
|
+
*/
|
|
83
|
+
"aria-label"?: string;
|
|
22
84
|
}
|
|
23
85
|
/**
|
|
24
86
|
* The quiet strip along the bottom of a window.
|
|
25
87
|
*
|
|
26
88
|
* The smallest type in the system in the subtlest ink: a status bar is read
|
|
27
89
|
* when you go looking for it and ignored otherwise, so it must never compete
|
|
28
|
-
* with the work above it. Height comes from `--cue-chrome-statusbar
|
|
90
|
+
* with the work above it. Height comes from `--cue-chrome-statusbar`, and the
|
|
91
|
+
* bar clips rather than wrapping — one rung of the frame, always, whatever it
|
|
92
|
+
* is asked to hold.
|
|
93
|
+
*
|
|
94
|
+
* Items come in two kinds and the difference is deliberate. A plain
|
|
95
|
+
* `StatusBar.Item` is a **reading**: a `<div>`, inert by construction, with no
|
|
96
|
+
* hover, no focus and nothing to click — which is what most of a status bar is.
|
|
97
|
+
* `interactive` (or a `render`) makes one a **control**, and only then does it
|
|
98
|
+
* grow the button chassis, the full-height hover fill and the inset focus ring.
|
|
99
|
+
* `StatusBar.Group` fuses adjacent items into one segmented control.
|
|
29
100
|
*
|
|
30
101
|
* Not a live region — a status bar changes constantly (frame counters, DMX
|
|
31
102
|
* output), and announcing every change would make the app unusable with a
|
|
32
103
|
* screen reader. Put anything that genuinely must be announced in a `Toast`.
|
|
33
104
|
*
|
|
34
|
-
* Static markup — no `"use client"`.
|
|
105
|
+
* Static markup — no `"use client"`. A bar of readings renders on the server
|
|
106
|
+
* and ships no JavaScript at all; the interactive item is the one part that
|
|
107
|
+
* crosses the boundary, and it crosses it alone (`./_status-bar-item.tsx`).
|
|
35
108
|
*
|
|
36
109
|
* @example
|
|
37
110
|
* <StatusBar>
|
|
38
111
|
* <StatusBar.Item icon={Plug} tone="ok">Art-Net connected</StatusBar.Item>
|
|
39
112
|
* <StatusBar.Item push>512 channels</StatusBar.Item>
|
|
40
113
|
* </StatusBar>
|
|
114
|
+
* @example
|
|
115
|
+
* // The VS Code shape: a clickable cluster left, a loud state right.
|
|
116
|
+
* <StatusBar>
|
|
117
|
+
* <StatusBar.Group aria-label="Repository">
|
|
118
|
+
* <StatusBar.Item interactive icon={GitBranch}>main</StatusBar.Item>
|
|
119
|
+
* <StatusBar.Item interactive icon={ArrowUp}>2</StatusBar.Item>
|
|
120
|
+
* </StatusBar.Group>
|
|
121
|
+
* <StatusBar.Item push interactive prominence="filled" tone="danger">3 faults</StatusBar.Item>
|
|
122
|
+
* </StatusBar>
|
|
41
123
|
*/
|
|
42
124
|
declare const StatusBar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>> & {
|
|
43
|
-
Item: React.ForwardRefExoticComponent<StatusBarItemProps & React.RefAttributes<
|
|
125
|
+
Item: React.ForwardRefExoticComponent<StatusBarItemProps & React.RefAttributes<HTMLElement>>;
|
|
126
|
+
Group: React.ForwardRefExoticComponent<StatusBarGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
44
127
|
};
|
|
45
128
|
//#endregion
|
|
46
|
-
export { StatusBar, StatusBarItemProps, StatusBarProps, StatusBarTone };
|
|
129
|
+
export { StatusBar, StatusBarGroupProps, StatusBarItemProps, type StatusBarProminence, StatusBarProps, StatusBarTone };
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { STATUS_BAR_GROUP_CLASSES, statusBarItemClasses } from "./_status-bar.js";
|
|
3
|
+
import { StatusBarInteractiveItem } from "./_status-bar-item.js";
|
|
2
4
|
import * as React from "react";
|
|
3
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
6
|
//#region src/chrome/status-bar.tsx
|
|
5
|
-
const ITEM_TONES = {
|
|
6
|
-
neutral: "",
|
|
7
|
-
accent: "text-accent",
|
|
8
|
-
ok: "text-ok",
|
|
9
|
-
busy: "text-busy",
|
|
10
|
-
warn: "text-warn",
|
|
11
|
-
danger: "text-danger",
|
|
12
|
-
info: "text-info"
|
|
13
|
-
};
|
|
14
7
|
const StatusBarRoot = React.forwardRef(function StatusBar({ className, ...elementProps }, ref) {
|
|
15
8
|
return /* @__PURE__ */ jsx("div", {
|
|
16
9
|
ref,
|
|
17
10
|
"data-slot": "status-bar",
|
|
18
|
-
className: cn("flex h-(--cue-chrome-statusbar) w-full shrink-0 items-center gap-(--cue-space-4) border-t border-border bg-surface-1 px-(--cue-pad-row-x) font-mono text-(length:--cue-text-label) text-fg-subtle", className),
|
|
11
|
+
className: cn("flex h-(--cue-chrome-statusbar) w-full shrink-0 items-center gap-(--cue-space-4) overflow-x-clip border-t border-border bg-surface-1 px-(--cue-pad-row-x) font-mono text-(length:--cue-text-label) text-fg-subtle", className),
|
|
19
12
|
...elementProps
|
|
20
13
|
});
|
|
21
14
|
});
|
|
22
|
-
/**
|
|
23
|
-
|
|
15
|
+
/**
|
|
16
|
+
* One reading in the bar: a mode, a count, a connection state — or, given
|
|
17
|
+
* `interactive`, one control in it.
|
|
18
|
+
*/
|
|
19
|
+
const StatusBarItem = React.forwardRef(function StatusBarItem({ className, tone = "neutral", push = false, icon: Icon, interactive = false, prominence = "quiet", render, disabled, children, ...elementProps }, ref) {
|
|
20
|
+
if (interactive || render !== void 0) return /* @__PURE__ */ jsx(StatusBarInteractiveItem, {
|
|
21
|
+
ref,
|
|
22
|
+
tone,
|
|
23
|
+
prominence,
|
|
24
|
+
className,
|
|
25
|
+
push,
|
|
26
|
+
icon: Icon,
|
|
27
|
+
render,
|
|
28
|
+
disabled,
|
|
29
|
+
elementProps,
|
|
30
|
+
children
|
|
31
|
+
});
|
|
24
32
|
return /* @__PURE__ */ jsxs("div", {
|
|
25
33
|
ref,
|
|
26
34
|
"data-slot": "status-bar-item",
|
|
27
|
-
className: cn(
|
|
35
|
+
className: cn(statusBarItemClasses({
|
|
36
|
+
tone,
|
|
37
|
+
prominence
|
|
38
|
+
}), push ? "ml-auto" : void 0, className),
|
|
28
39
|
...elementProps,
|
|
29
40
|
children: [Icon === void 0 ? null : /* @__PURE__ */ jsx(Icon, {
|
|
30
41
|
"aria-hidden": "true",
|
|
@@ -33,24 +44,77 @@ const StatusBarItem = React.forwardRef(function StatusBarItem({ className, tone
|
|
|
33
44
|
});
|
|
34
45
|
});
|
|
35
46
|
/**
|
|
47
|
+
* Items that belong to each other, one pixel apart.
|
|
48
|
+
*
|
|
49
|
+
* The bar spaces its readings at `--cue-space-4` because they are unrelated
|
|
50
|
+
* facts. A branch name and its ahead/behind count are not unrelated facts; they
|
|
51
|
+
* are one control that happens to have two halves, and the gap between them is
|
|
52
|
+
* what says so. This is VS Code's `compact` mechanic — the seam collapses, and
|
|
53
|
+
* hovering either half grounds both while the half under the cursor goes one
|
|
54
|
+
* rung further — expressed as a wrapper rather than as an id-reference between
|
|
55
|
+
* two items, because a design system's items do not have ids to point at.
|
|
56
|
+
*
|
|
57
|
+
* A `role="group"` rather than nothing: the members are announced as a set, and
|
|
58
|
+
* the DOM order they are announced in is the order they are read in. Give it an
|
|
59
|
+
* `aria-label` when the members only make sense together.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* <StatusBar.Group aria-label="Repository">
|
|
63
|
+
* <StatusBar.Item interactive icon={GitBranch}>main</StatusBar.Item>
|
|
64
|
+
* <StatusBar.Item interactive icon={ArrowUp}>2</StatusBar.Item>
|
|
65
|
+
* </StatusBar.Group>
|
|
66
|
+
*/
|
|
67
|
+
const StatusBarGroup = React.forwardRef(function StatusBarGroup({ className, push = false, role = "group", ...elementProps }, ref) {
|
|
68
|
+
return /* @__PURE__ */ jsx("div", {
|
|
69
|
+
ref,
|
|
70
|
+
"data-slot": "status-bar-group",
|
|
71
|
+
role,
|
|
72
|
+
className: cn(STATUS_BAR_GROUP_CLASSES, push ? "ml-auto" : void 0, className),
|
|
73
|
+
...elementProps
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
36
77
|
* The quiet strip along the bottom of a window.
|
|
37
78
|
*
|
|
38
79
|
* The smallest type in the system in the subtlest ink: a status bar is read
|
|
39
80
|
* when you go looking for it and ignored otherwise, so it must never compete
|
|
40
|
-
* with the work above it. Height comes from `--cue-chrome-statusbar
|
|
81
|
+
* with the work above it. Height comes from `--cue-chrome-statusbar`, and the
|
|
82
|
+
* bar clips rather than wrapping — one rung of the frame, always, whatever it
|
|
83
|
+
* is asked to hold.
|
|
84
|
+
*
|
|
85
|
+
* Items come in two kinds and the difference is deliberate. A plain
|
|
86
|
+
* `StatusBar.Item` is a **reading**: a `<div>`, inert by construction, with no
|
|
87
|
+
* hover, no focus and nothing to click — which is what most of a status bar is.
|
|
88
|
+
* `interactive` (or a `render`) makes one a **control**, and only then does it
|
|
89
|
+
* grow the button chassis, the full-height hover fill and the inset focus ring.
|
|
90
|
+
* `StatusBar.Group` fuses adjacent items into one segmented control.
|
|
41
91
|
*
|
|
42
92
|
* Not a live region — a status bar changes constantly (frame counters, DMX
|
|
43
93
|
* output), and announcing every change would make the app unusable with a
|
|
44
94
|
* screen reader. Put anything that genuinely must be announced in a `Toast`.
|
|
45
95
|
*
|
|
46
|
-
* Static markup — no `"use client"`.
|
|
96
|
+
* Static markup — no `"use client"`. A bar of readings renders on the server
|
|
97
|
+
* and ships no JavaScript at all; the interactive item is the one part that
|
|
98
|
+
* crosses the boundary, and it crosses it alone (`./_status-bar-item.tsx`).
|
|
47
99
|
*
|
|
48
100
|
* @example
|
|
49
101
|
* <StatusBar>
|
|
50
102
|
* <StatusBar.Item icon={Plug} tone="ok">Art-Net connected</StatusBar.Item>
|
|
51
103
|
* <StatusBar.Item push>512 channels</StatusBar.Item>
|
|
52
104
|
* </StatusBar>
|
|
105
|
+
* @example
|
|
106
|
+
* // The VS Code shape: a clickable cluster left, a loud state right.
|
|
107
|
+
* <StatusBar>
|
|
108
|
+
* <StatusBar.Group aria-label="Repository">
|
|
109
|
+
* <StatusBar.Item interactive icon={GitBranch}>main</StatusBar.Item>
|
|
110
|
+
* <StatusBar.Item interactive icon={ArrowUp}>2</StatusBar.Item>
|
|
111
|
+
* </StatusBar.Group>
|
|
112
|
+
* <StatusBar.Item push interactive prominence="filled" tone="danger">3 faults</StatusBar.Item>
|
|
113
|
+
* </StatusBar>
|
|
53
114
|
*/
|
|
54
|
-
const StatusBar = Object.assign(StatusBarRoot, {
|
|
115
|
+
const StatusBar = Object.assign(StatusBarRoot, {
|
|
116
|
+
Item: StatusBarItem,
|
|
117
|
+
Group: StatusBarGroup
|
|
118
|
+
});
|
|
55
119
|
//#endregion
|
|
56
120
|
export { StatusBar };
|
|
@@ -9,7 +9,17 @@ interface TitleBarProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
|
9
9
|
* the same component ships into both shells and neither one recognises the
|
|
10
10
|
* other's contract. The interactive clusters are carved back out with
|
|
11
11
|
* `app-region: no-drag`, or the buttons in the bar would only ever move the
|
|
12
|
-
* window instead of firing.
|
|
12
|
+
* window instead of firing. Both halves of that contract live in
|
|
13
|
+
* `./_drag.ts`, which `AppShell.Bar` emits from as well — the two bars are
|
|
14
|
+
* draggable the same way or they are not draggable the same way.
|
|
15
|
+
*
|
|
16
|
+
* Double-click-to-maximise is not wired here. Tauri's own injected script
|
|
17
|
+
* already provides it on any drag region, cancel-by-dragging-away on macOS
|
|
18
|
+
* included; Electron provides nothing, and an Electron app has to listen for
|
|
19
|
+
* `dblclick` and toggle the window itself. Neither is a decision a component
|
|
20
|
+
* in a design system can make on the window's behalf.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to `false`.
|
|
13
23
|
*/
|
|
14
24
|
platformDrag?: boolean;
|
|
15
25
|
/**
|
package/dist/chrome/title-bar.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { DRAG_CLASSES, NO_DRAG_CLASSES } from "./_drag.js";
|
|
3
4
|
import * as React from "react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/chrome/title-bar.tsx
|
|
6
|
-
/** `app-region` needs the unprefixed and the WebKit form to cover both shells. */
|
|
7
|
-
const DRAG_CLASSES = "[app-region:drag] [-webkit-app-region:drag]";
|
|
8
|
-
const NO_DRAG_CLASSES = "[app-region:no-drag] [-webkit-app-region:no-drag]";
|
|
9
7
|
/**
|
|
10
8
|
* The window's own bar: app mark left, document name centred, controls right.
|
|
11
9
|
*
|
package/dist/date/_segments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
-
import { controlVariants } from "../forms/_chassis.js";
|
|
2
|
+
import { controlGroupFocusClasses, controlVariants } from "../forms/_chassis.js";
|
|
3
3
|
import { ariaClassName } from "../lib/aria-class.js";
|
|
4
4
|
import "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -24,11 +24,22 @@ import { DateInput, DateSegment } from "react-aria-components";
|
|
|
24
24
|
* The chassis lights its rim on `:focus`, which this group can never match —
|
|
25
25
|
* React Aria keeps DOM focus on the individual segment and reports the group's
|
|
26
26
|
* state as `data-focus-within`. Without the extra variant the inherited
|
|
27
|
-
* `focus:border-
|
|
28
|
-
* only control in the system whose rim stays dead while it is being
|
|
27
|
+
* `focus:border-border-strong` is a rule that cannot fire, and a segmented field
|
|
28
|
+
* would be the only control in the system whose rim stays dead while it is being
|
|
29
|
+
* typed into.
|
|
30
|
+
*
|
|
31
|
+
* A segmented field is a grouped control in every sense that matters, so it
|
|
32
|
+
* takes the grouped contract whole rather than a rim of its own invention:
|
|
33
|
+
* `controlGroupFocusClasses` strengthens the rim for any focus inside and paints
|
|
34
|
+
* the single flush `--cue-focus` ring only for a `:focus-visible` descendant.
|
|
35
|
+
* React Aria makes each value segment `contentEditable`, and a typing surface
|
|
36
|
+
* matches `:focus-visible` on a pointer click as well as on Tab, so the ring
|
|
37
|
+
* lands whenever the field is actually being edited. `data-focus-within` is kept
|
|
38
|
+
* beside the native `:focus-within` rim because it is the state React Aria
|
|
39
|
+
* documents, and the two agree on the same colour either way.
|
|
29
40
|
*/
|
|
30
41
|
function segmentGroupClasses(size) {
|
|
31
|
-
return cn(controlVariants({ size }), "inline-flex w-auto items-center whitespace-nowrap data-[focus-within]:border-
|
|
42
|
+
return cn(controlVariants({ size }), "inline-flex w-auto items-center whitespace-nowrap data-[focus-within]:border-border-strong", controlGroupFocusClasses);
|
|
32
43
|
}
|
|
33
44
|
/**
|
|
34
45
|
* One segment. `data-focused` — not `:focus` — carries the highlight, because
|
package/dist/forms/_chassis.d.ts
CHANGED
|
@@ -20,10 +20,84 @@ type FormControlSize = "sm" | "md" | "lg";
|
|
|
20
20
|
* raising them off it, which is the portfolio-wide convention for anything the
|
|
21
21
|
* user types into.
|
|
22
22
|
*
|
|
23
|
+
* **Focus is one signal, not two.** The rim walks one rung up the border ladder
|
|
24
|
+
* on `:focus` — `border-border` to `border-border-strong` — and `:focus-visible`
|
|
25
|
+
* adds a single 2px outline, pulled back over that rim with
|
|
26
|
+
* `-outline-offset-1` so it sits flush against the control instead of floating
|
|
27
|
+
* a pixel clear of it. The recipe this replaced painted a saturated accent rim
|
|
28
|
+
* *and* an offset accent ring simultaneously, which in a dark, dense console
|
|
29
|
+
* reads as an error or a selection rather than as "the caret is here". No
|
|
30
|
+
* shipping system draws both: Carbon and Radix each draw one flush ring, and
|
|
31
|
+
* shadcn moved off the offset-ring default it used to ship.
|
|
32
|
+
*
|
|
33
|
+
* Flush is also the cheap way to stay correct. WCAG 2.4.13 accepts a 2px
|
|
34
|
+
* perimeter only when it touches the component's own outer edge; an indicator
|
|
35
|
+
* inset further, floating on a gap, has to be 3px thick to cover the same area.
|
|
36
|
+
* And because an outline is painted outside the layout box, none of this moves
|
|
37
|
+
* a control by a pixel or reflows the row it sits in.
|
|
38
|
+
*
|
|
39
|
+
* There is no quieter pointer path to fall back on, which is why the single
|
|
40
|
+
* treatment has to be quiet itself: every engine puts typing surfaces on the
|
|
41
|
+
* `:focus-visible` allowlist, so a text input matches it on a mouse click just
|
|
42
|
+
* as it does on Tab. Only the non-typing controls that ride this chassis — the
|
|
43
|
+
* `Select` trigger, the date triggers — actually distinguish the two.
|
|
44
|
+
*
|
|
45
|
+
* `--cue-focus`, not `--cue-accent`. Every theme ships both roles, and keeping
|
|
46
|
+
* them separable is what WCAG 1.4.11 needs: one role whose job is "read at 3:1
|
|
47
|
+
* against the surface behind the ring", independent of the role whose job is
|
|
48
|
+
* "look like the brand". Seventeen of the twenty theme blocks still set the two
|
|
49
|
+
* to the same value, so there the separation costs nothing on screen — but three
|
|
50
|
+
* light presets can no longer afford it. luma.light, venu.light and hivehub.light
|
|
51
|
+
* each read below 3:1 against surfaces a field actually sits on (luma.light as
|
|
52
|
+
* low as 2.45, and against four of its five grounds), so each now carries its
|
|
53
|
+
* own ring — the accent's hue stepped darker — while the accent itself stays
|
|
54
|
+
* put. Measured across all five grounds a field can land on — the sunken fill,
|
|
55
|
+
* the page background and the three surface rungs — every preset now clears
|
|
56
|
+
* 3:1, the thinnest being signal.light's 3.27. Pointing the ring at the focus
|
|
57
|
+
* role is what made that retune possible without repainting every accent
|
|
58
|
+
* surface in the theme.
|
|
59
|
+
*
|
|
60
|
+
* (Named in prose rather than as `--cue-*` on purpose: `tokensUsed` in the
|
|
61
|
+
* published manifest is scanned out of this file's text, so spelling the other
|
|
62
|
+
* four grounds as tokens would have every control on this chassis claim it
|
|
63
|
+
* paints a surface it never touches.)
|
|
64
|
+
*
|
|
65
|
+
* `--cue-danger` carries the same obligation on an invalid field, where it is
|
|
66
|
+
* the ring rather than only the rim — a 1.4.11 surface, not purely a semantic
|
|
67
|
+
* colour. It needed no retune: the red already clears 3:1 on all five grounds in
|
|
68
|
+
* every preset, thinnest at terminal.dark's 3.13. Both floors are held by
|
|
69
|
+
* `test/theming/contrast.test.ts`, which measures every block on every ground —
|
|
70
|
+
* the guard that was missing when these three shipped a ring nobody could see.
|
|
71
|
+
*
|
|
72
|
+
* The ring is an `outline` and never a `box-shadow`: `forced-colors: active`
|
|
73
|
+
* discards box shadows outright and repaints outlines in the user's own
|
|
74
|
+
* highlight colour, so an outline is the only indicator that survives
|
|
75
|
+
* high-contrast mode.
|
|
76
|
+
*
|
|
77
|
+
* **Invalid and focused: the ring turns red, it does not sit beside the rim.**
|
|
78
|
+
* `data-[invalid]:border-danger` does win the cascade over the focus rim, but
|
|
79
|
+
* winning it changes nothing on screen: an outline's band runs from
|
|
80
|
+
* `border-edge + offset` outward by its own width, so a 2px ring at
|
|
81
|
+
* `-outline-offset-1` spans from one pixel inside the border box to one pixel
|
|
82
|
+
* outside it — the whole 1px rim — and outlines paint after borders. Measured
|
|
83
|
+
* in both engines: an invalid control paints 456 danger pixels at rest and zero
|
|
84
|
+
* while focused. Pushing the ring off the rim would mean either floating it on
|
|
85
|
+
* a gap (WCAG 2.4.13 then wants 3px, and the halo is back) or insetting it
|
|
86
|
+
* further (which buries it deeper). So the ring takes the danger colour instead
|
|
87
|
+
* — `data-[invalid]:focus-visible:outline-danger` — which is the ruling shadcn
|
|
88
|
+
* and Radix both reached: the border stays danger, the ring communicates focus,
|
|
89
|
+
* and red communicates invalid whichever of the two is painting.
|
|
90
|
+
*
|
|
91
|
+
* The compound modifier outranks the plain one on specificity — an attribute
|
|
92
|
+
* selector and a pseudo-class against one pseudo-class — so its position in
|
|
93
|
+
* Tailwind's utility order cannot decide it. `focus-visible:outline-solid` and
|
|
94
|
+
* `focus-visible:outline-2` still supply the style and the width in both cases,
|
|
95
|
+
* which is why only the colour is restated.
|
|
96
|
+
*
|
|
23
97
|
* `focus-visible:outline-solid` re-declares the outline style the sibling
|
|
24
98
|
* `outline-none` switched off — see the note on `buttonChassis`.
|
|
25
99
|
*/
|
|
26
|
-
declare const controlSurface = "w-full rounded-(--radius-control) border border-border bg-sunken font-mono text-fg placeholder:text-fg-subtle outline-none focus:border-
|
|
100
|
+
declare const controlSurface = "w-full rounded-(--radius-control) border border-border bg-sunken font-mono text-fg placeholder:text-fg-subtle outline-none focus:border-border-strong focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-1 focus-visible:outline-(--cue-focus) disabled:opacity-40 data-[disabled]:opacity-40 data-[invalid]:border-danger data-[invalid]:focus-visible:outline-danger";
|
|
27
101
|
/**
|
|
28
102
|
* The full recessed-control recipe, one variant per rung of the control ladder.
|
|
29
103
|
*
|
|
@@ -56,9 +130,35 @@ declare const controlClasses: string;
|
|
|
56
130
|
declare const bareControlClasses = "min-w-0 flex-1 border-0 bg-transparent p-0 font-mono text-[length:inherit] text-fg placeholder:text-fg-subtle outline-none disabled:opacity-40";
|
|
57
131
|
/**
|
|
58
132
|
* Focus feedback for a wrapper that owns the rim on behalf of an inner control.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
133
|
+
*
|
|
134
|
+
* A `<div>` never matches `:focus-visible` itself, so a group has to read the
|
|
135
|
+
* state off its descendants — and the two halves of {@link controlSurface}'s
|
|
136
|
+
* treatment want two different questions asked of them.
|
|
137
|
+
*
|
|
138
|
+
* The rim follows plain `:focus-within`: anything focused inside strengthens the
|
|
139
|
+
* border. That is the quiet half, and it is the right answer however the focus
|
|
140
|
+
* arrived. The ring follows `:has(:focus-visible)`, so it paints only when the
|
|
141
|
+
* descendant that has focus is itself showing a focus ring. Using
|
|
142
|
+
* `:focus-within` for *both* — the recipe this replaced — is what made a mouse
|
|
143
|
+
* click inside a `SearchInput` or a `NumberField` light the whole cluster up
|
|
144
|
+
* with a saturated block, the single loudest thing in a console form.
|
|
145
|
+
*
|
|
146
|
+
* `:has()` is Baseline — Chrome 105, Safari 15.4, Firefox 121 — so no
|
|
147
|
+
* `:focus-within` fallback layer is needed at the floor this library supports.
|
|
148
|
+
*
|
|
149
|
+
* Geometry matches {@link controlSurface} exactly, down to the negative offset:
|
|
150
|
+
* a grouped field and a plain one are the same control, and a rim that differed
|
|
151
|
+
* by a pixel between them would say otherwise. The inner control stays ringless
|
|
152
|
+
* — see {@link bareControlClasses} — because two concentric rings on one field
|
|
153
|
+
* is the halo this treatment exists to remove.
|
|
154
|
+
*
|
|
155
|
+
* The invalid ruling comes with the geometry, for the same reason: at
|
|
156
|
+
* `-outline-offset-1` the ring covers the rim it is drawn over, so an invalid
|
|
157
|
+
* group in focus recolours the ring rather than pretending the red under it is
|
|
158
|
+
* still visible. `data-invalid` is read off the wrapper, which is where Base
|
|
159
|
+
* UI's `Field` stamps it and where the group already takes
|
|
160
|
+
* `data-[invalid]:border-danger` from {@link controlVariants}.
|
|
61
161
|
*/
|
|
62
|
-
declare const controlGroupFocusClasses = "focus-within:border-
|
|
162
|
+
declare const controlGroupFocusClasses = "focus-within:border-border-strong has-focus-visible:outline-2 has-focus-visible:outline-solid has-focus-visible:-outline-offset-1 has-focus-visible:outline-(--cue-focus) data-[invalid]:has-focus-visible:outline-danger";
|
|
63
163
|
//#endregion
|
|
64
164
|
export { ControlVariantProps, FormControlSize, bareControlClasses, controlClasses, controlGroupFocusClasses, controlSurface, controlVariants };
|
package/dist/forms/_chassis.js
CHANGED
|
@@ -12,10 +12,84 @@ import { cva } from "../lib/cva.js";
|
|
|
12
12
|
* raising them off it, which is the portfolio-wide convention for anything the
|
|
13
13
|
* user types into.
|
|
14
14
|
*
|
|
15
|
+
* **Focus is one signal, not two.** The rim walks one rung up the border ladder
|
|
16
|
+
* on `:focus` — `border-border` to `border-border-strong` — and `:focus-visible`
|
|
17
|
+
* adds a single 2px outline, pulled back over that rim with
|
|
18
|
+
* `-outline-offset-1` so it sits flush against the control instead of floating
|
|
19
|
+
* a pixel clear of it. The recipe this replaced painted a saturated accent rim
|
|
20
|
+
* *and* an offset accent ring simultaneously, which in a dark, dense console
|
|
21
|
+
* reads as an error or a selection rather than as "the caret is here". No
|
|
22
|
+
* shipping system draws both: Carbon and Radix each draw one flush ring, and
|
|
23
|
+
* shadcn moved off the offset-ring default it used to ship.
|
|
24
|
+
*
|
|
25
|
+
* Flush is also the cheap way to stay correct. WCAG 2.4.13 accepts a 2px
|
|
26
|
+
* perimeter only when it touches the component's own outer edge; an indicator
|
|
27
|
+
* inset further, floating on a gap, has to be 3px thick to cover the same area.
|
|
28
|
+
* And because an outline is painted outside the layout box, none of this moves
|
|
29
|
+
* a control by a pixel or reflows the row it sits in.
|
|
30
|
+
*
|
|
31
|
+
* There is no quieter pointer path to fall back on, which is why the single
|
|
32
|
+
* treatment has to be quiet itself: every engine puts typing surfaces on the
|
|
33
|
+
* `:focus-visible` allowlist, so a text input matches it on a mouse click just
|
|
34
|
+
* as it does on Tab. Only the non-typing controls that ride this chassis — the
|
|
35
|
+
* `Select` trigger, the date triggers — actually distinguish the two.
|
|
36
|
+
*
|
|
37
|
+
* `--cue-focus`, not `--cue-accent`. Every theme ships both roles, and keeping
|
|
38
|
+
* them separable is what WCAG 1.4.11 needs: one role whose job is "read at 3:1
|
|
39
|
+
* against the surface behind the ring", independent of the role whose job is
|
|
40
|
+
* "look like the brand". Seventeen of the twenty theme blocks still set the two
|
|
41
|
+
* to the same value, so there the separation costs nothing on screen — but three
|
|
42
|
+
* light presets can no longer afford it. luma.light, venu.light and hivehub.light
|
|
43
|
+
* each read below 3:1 against surfaces a field actually sits on (luma.light as
|
|
44
|
+
* low as 2.45, and against four of its five grounds), so each now carries its
|
|
45
|
+
* own ring — the accent's hue stepped darker — while the accent itself stays
|
|
46
|
+
* put. Measured across all five grounds a field can land on — the sunken fill,
|
|
47
|
+
* the page background and the three surface rungs — every preset now clears
|
|
48
|
+
* 3:1, the thinnest being signal.light's 3.27. Pointing the ring at the focus
|
|
49
|
+
* role is what made that retune possible without repainting every accent
|
|
50
|
+
* surface in the theme.
|
|
51
|
+
*
|
|
52
|
+
* (Named in prose rather than as `--cue-*` on purpose: `tokensUsed` in the
|
|
53
|
+
* published manifest is scanned out of this file's text, so spelling the other
|
|
54
|
+
* four grounds as tokens would have every control on this chassis claim it
|
|
55
|
+
* paints a surface it never touches.)
|
|
56
|
+
*
|
|
57
|
+
* `--cue-danger` carries the same obligation on an invalid field, where it is
|
|
58
|
+
* the ring rather than only the rim — a 1.4.11 surface, not purely a semantic
|
|
59
|
+
* colour. It needed no retune: the red already clears 3:1 on all five grounds in
|
|
60
|
+
* every preset, thinnest at terminal.dark's 3.13. Both floors are held by
|
|
61
|
+
* `test/theming/contrast.test.ts`, which measures every block on every ground —
|
|
62
|
+
* the guard that was missing when these three shipped a ring nobody could see.
|
|
63
|
+
*
|
|
64
|
+
* The ring is an `outline` and never a `box-shadow`: `forced-colors: active`
|
|
65
|
+
* discards box shadows outright and repaints outlines in the user's own
|
|
66
|
+
* highlight colour, so an outline is the only indicator that survives
|
|
67
|
+
* high-contrast mode.
|
|
68
|
+
*
|
|
69
|
+
* **Invalid and focused: the ring turns red, it does not sit beside the rim.**
|
|
70
|
+
* `data-[invalid]:border-danger` does win the cascade over the focus rim, but
|
|
71
|
+
* winning it changes nothing on screen: an outline's band runs from
|
|
72
|
+
* `border-edge + offset` outward by its own width, so a 2px ring at
|
|
73
|
+
* `-outline-offset-1` spans from one pixel inside the border box to one pixel
|
|
74
|
+
* outside it — the whole 1px rim — and outlines paint after borders. Measured
|
|
75
|
+
* in both engines: an invalid control paints 456 danger pixels at rest and zero
|
|
76
|
+
* while focused. Pushing the ring off the rim would mean either floating it on
|
|
77
|
+
* a gap (WCAG 2.4.13 then wants 3px, and the halo is back) or insetting it
|
|
78
|
+
* further (which buries it deeper). So the ring takes the danger colour instead
|
|
79
|
+
* — `data-[invalid]:focus-visible:outline-danger` — which is the ruling shadcn
|
|
80
|
+
* and Radix both reached: the border stays danger, the ring communicates focus,
|
|
81
|
+
* and red communicates invalid whichever of the two is painting.
|
|
82
|
+
*
|
|
83
|
+
* The compound modifier outranks the plain one on specificity — an attribute
|
|
84
|
+
* selector and a pseudo-class against one pseudo-class — so its position in
|
|
85
|
+
* Tailwind's utility order cannot decide it. `focus-visible:outline-solid` and
|
|
86
|
+
* `focus-visible:outline-2` still supply the style and the width in both cases,
|
|
87
|
+
* which is why only the colour is restated.
|
|
88
|
+
*
|
|
15
89
|
* `focus-visible:outline-solid` re-declares the outline style the sibling
|
|
16
90
|
* `outline-none` switched off — see the note on `buttonChassis`.
|
|
17
91
|
*/
|
|
18
|
-
const controlSurface = "w-full rounded-(--radius-control) border border-border bg-sunken font-mono text-fg placeholder:text-fg-subtle outline-none focus:border-
|
|
92
|
+
const controlSurface = "w-full rounded-(--radius-control) border border-border bg-sunken font-mono text-fg placeholder:text-fg-subtle outline-none focus:border-border-strong focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-1 focus-visible:outline-(--cue-focus) disabled:opacity-40 data-[disabled]:opacity-40 data-[invalid]:border-danger data-[invalid]:focus-visible:outline-danger";
|
|
19
93
|
/**
|
|
20
94
|
* The full recessed-control recipe, one variant per rung of the control ladder.
|
|
21
95
|
*
|
|
@@ -51,9 +125,35 @@ const controlClasses = controlVariants();
|
|
|
51
125
|
const bareControlClasses = "min-w-0 flex-1 border-0 bg-transparent p-0 font-mono text-[length:inherit] text-fg placeholder:text-fg-subtle outline-none disabled:opacity-40";
|
|
52
126
|
/**
|
|
53
127
|
* Focus feedback for a wrapper that owns the rim on behalf of an inner control.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
128
|
+
*
|
|
129
|
+
* A `<div>` never matches `:focus-visible` itself, so a group has to read the
|
|
130
|
+
* state off its descendants — and the two halves of {@link controlSurface}'s
|
|
131
|
+
* treatment want two different questions asked of them.
|
|
132
|
+
*
|
|
133
|
+
* The rim follows plain `:focus-within`: anything focused inside strengthens the
|
|
134
|
+
* border. That is the quiet half, and it is the right answer however the focus
|
|
135
|
+
* arrived. The ring follows `:has(:focus-visible)`, so it paints only when the
|
|
136
|
+
* descendant that has focus is itself showing a focus ring. Using
|
|
137
|
+
* `:focus-within` for *both* — the recipe this replaced — is what made a mouse
|
|
138
|
+
* click inside a `SearchInput` or a `NumberField` light the whole cluster up
|
|
139
|
+
* with a saturated block, the single loudest thing in a console form.
|
|
140
|
+
*
|
|
141
|
+
* `:has()` is Baseline — Chrome 105, Safari 15.4, Firefox 121 — so no
|
|
142
|
+
* `:focus-within` fallback layer is needed at the floor this library supports.
|
|
143
|
+
*
|
|
144
|
+
* Geometry matches {@link controlSurface} exactly, down to the negative offset:
|
|
145
|
+
* a grouped field and a plain one are the same control, and a rim that differed
|
|
146
|
+
* by a pixel between them would say otherwise. The inner control stays ringless
|
|
147
|
+
* — see {@link bareControlClasses} — because two concentric rings on one field
|
|
148
|
+
* is the halo this treatment exists to remove.
|
|
149
|
+
*
|
|
150
|
+
* The invalid ruling comes with the geometry, for the same reason: at
|
|
151
|
+
* `-outline-offset-1` the ring covers the rim it is drawn over, so an invalid
|
|
152
|
+
* group in focus recolours the ring rather than pretending the red under it is
|
|
153
|
+
* still visible. `data-invalid` is read off the wrapper, which is where Base
|
|
154
|
+
* UI's `Field` stamps it and where the group already takes
|
|
155
|
+
* `data-[invalid]:border-danger` from {@link controlVariants}.
|
|
56
156
|
*/
|
|
57
|
-
const controlGroupFocusClasses = "focus-within:border-
|
|
157
|
+
const controlGroupFocusClasses = "focus-within:border-border-strong has-focus-visible:outline-2 has-focus-visible:outline-solid has-focus-visible:-outline-offset-1 has-focus-visible:outline-(--cue-focus) data-[invalid]:has-focus-visible:outline-danger";
|
|
58
158
|
//#endregion
|
|
59
159
|
export { bareControlClasses, controlClasses, controlGroupFocusClasses, controlSurface, controlVariants };
|
|
@@ -17,9 +17,14 @@ interface InputGroupProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
|
17
17
|
* The group — not the input — owns the chassis, so there is exactly one rim and
|
|
18
18
|
* one fill however many affordances are bolted on. It publishes that fact
|
|
19
19
|
* through context: an {@link Input} rendered inside strips its own border,
|
|
20
|
-
* background, height and padding and inherits the group's font size.
|
|
21
|
-
*
|
|
22
|
-
* `:focus
|
|
20
|
+
* background, height and padding and inherits the group's font size.
|
|
21
|
+
*
|
|
22
|
+
* Focus feedback splits in two, because a `<div>` matches neither `:focus` nor
|
|
23
|
+
* `:focus-visible` itself. The rim strengthens on `:focus-within` — anything
|
|
24
|
+
* focused inside, however it got there — while the ring waits for
|
|
25
|
+
* `:has(:focus-visible)`, so a mouse click into the field does not light the
|
|
26
|
+
* whole cluster up. Both halves live on the wrapper; the inner control paints
|
|
27
|
+
* no ring of its own.
|
|
23
28
|
*
|
|
24
29
|
* @example
|
|
25
30
|
* <InputGroup leading={<Search />} trailing={<Kbd>⌘K</Kbd>}>
|
|
@@ -17,9 +17,14 @@ const InputGroupContext = React.createContext(false);
|
|
|
17
17
|
* The group — not the input — owns the chassis, so there is exactly one rim and
|
|
18
18
|
* one fill however many affordances are bolted on. It publishes that fact
|
|
19
19
|
* through context: an {@link Input} rendered inside strips its own border,
|
|
20
|
-
* background, height and padding and inherits the group's font size.
|
|
21
|
-
*
|
|
22
|
-
* `:focus
|
|
20
|
+
* background, height and padding and inherits the group's font size.
|
|
21
|
+
*
|
|
22
|
+
* Focus feedback splits in two, because a `<div>` matches neither `:focus` nor
|
|
23
|
+
* `:focus-visible` itself. The rim strengthens on `:focus-within` — anything
|
|
24
|
+
* focused inside, however it got there — while the ring waits for
|
|
25
|
+
* `:has(:focus-visible)`, so a mouse click into the field does not light the
|
|
26
|
+
* whole cluster up. Both halves live on the wrapper; the inner control paints
|
|
27
|
+
* no ring of its own.
|
|
23
28
|
*
|
|
24
29
|
* @example
|
|
25
30
|
* <InputGroup leading={<Search />} trailing={<Kbd>⌘K</Kbd>}>
|
|
@@ -25,8 +25,9 @@ interface NumberFieldProps extends Omit<NumberField.Root.Props, "className"> {
|
|
|
25
25
|
* and values typed out of range are clamped to `min`/`max` on blur.
|
|
26
26
|
*
|
|
27
27
|
* The group owns the rim; the input and both buttons render bare inside it, so
|
|
28
|
-
* the whole cluster reads as one field
|
|
29
|
-
*
|
|
28
|
+
* the whole cluster reads as one field. Focus feedback follows the group's
|
|
29
|
+
* split: the rim strengthens on `:focus-within`, and the ring waits for
|
|
30
|
+
* `:has(:focus-visible)` so a click on a stepper does not paint one.
|
|
30
31
|
*
|
|
31
32
|
* @example
|
|
32
33
|
* <NumberField aria-label="Fade time" defaultValue={3} min={0} max={600} step={0.5} />
|
|
@@ -17,8 +17,9 @@ const stepperClasses = "flex shrink-0 items-center justify-center border-0 bg-tr
|
|
|
17
17
|
* and values typed out of range are clamped to `min`/`max` on blur.
|
|
18
18
|
*
|
|
19
19
|
* The group owns the rim; the input and both buttons render bare inside it, so
|
|
20
|
-
* the whole cluster reads as one field
|
|
21
|
-
*
|
|
20
|
+
* the whole cluster reads as one field. Focus feedback follows the group's
|
|
21
|
+
* split: the rim strengthens on `:focus-within`, and the ring waits for
|
|
22
|
+
* `:has(:focus-visible)` so a click on a stepper does not paint one.
|
|
22
23
|
*
|
|
23
24
|
* @example
|
|
24
25
|
* <NumberField aria-label="Fade time" defaultValue={3} min={0} max={600} step={0.5} />
|