@scalewing/react 1.32.0 → 1.33.1
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 +20 -0
- package/README.md +1 -1
- package/dist/components/Box.d.ts +18 -0
- package/dist/components/Box.js +4 -2
- package/dist/components/Split.js +33 -46
- package/dist/components/use-split-range.d.ts +17 -0
- package/dist/components/use-split-range.js +49 -0
- package/dist/css/css-box-link.d.ts +13 -0
- package/dist/css/css-box-link.js +50 -0
- package/dist/css/css-document.js +3 -0
- package/dist/css/css-scroll-region.d.ts +7 -3
- package/dist/css/css-scroll-region.js +8 -3
- package/dist/css/css-split.js +1 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/split-measure.d.ts +7 -0
- package/dist/split-measure.js +21 -0
- package/dist/split-size.d.ts +15 -1
- package/dist/split-size.js +21 -9
- package/dist/styles.css +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.33.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2a52f01: `Split` no longer widens the page in a narrow container. The split is now at most its container's width less the split minimum (`max-width: calc(100% - var(--sw-split-min))`), so the next column keeps 14rem and the start pane shrinks below its 22rem default instead of running past the container. Before, at 390 px the pane kept 22rem, the next column got no width and its words pushed the page out sideways (the gallery measured 481 px; found by Teisoro F-011). The drag and keyboard range follows the same room: the pane's maximum is the container less the handle and the next column's minimum (as before), and when that room is under the minimum the range narrows to it, so a gesture starts from the width on screen, a move to the end never collapses or jumps the pane, and one step to the start collapses it. The pane keeps the width the reader chose (a collapse never overwrites it) and shows it held to the container's range, so a pane expanded after the container widens shows at least its minimum. The separator's `aria-valuenow` reports the width that shows (100 when the container leaves no range) and follows container resizes. No API, class or token changes.
|
|
8
|
+
|
|
9
|
+
## 1.33.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3a3dbf6: A quiet link look for a title that opens something, such as a table row's title (`docs/requests/teisoro-box-link-quiet.md`, Teisoro F-011 task 2145):
|
|
14
|
+
|
|
15
|
+
- `Box` `link` (`BoxLink`, `boxLinkAppearances`): `'accent'` (default, unchanged: the canvas link) or `'quiet'`, which keeps the surrounding text's colour at weight 600 and underlines on hover and keyboard focus. Its weight is the non-colour cue (WCAG 1.4.1), so it is for text lighter than 600; axe's `link-in-text-block` passes it on colour (the same colour and background as its text) or on its weight, and an accent link in body text that falls under 3:1 fails it. Only for `as="a"` (otherwise a `TypeError`); an unknown value throws a `RangeError`.
|
|
16
|
+
|
|
17
|
+
New generated class `sw-link-quiet`. No new tokens. The gallery adds `axe-core` (4.13.0, already in the lockfile through `eslint-plugin-jsx-a11y`) as an exact-pinned dev dependency for its accessibility check (pinned to the version the lockfile already holds, so nothing new is fetched); no production dependency.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- f3f318e: A scroll region (`Table`'s wrapper, the `DenominationGrid` strip) is now `position: relative`, so it is the containing block of anything absolutely placed inside it that no nearer box holds, such as a visually hidden label (`sw-sr-only`) or a field's hidden name in a cell past the region's edge. Before, such a label was placed against the page and widened it on a phone while the table scrolled sideways (reported by Teisoro F-011, an inline edit in a table row at 390 px). Anything absolutely placed in a table with no positioned box in between now anchors to the table's region and scrolls with it, rather than to a positioned box outside the table (or the page). No API, class or token changes.
|
|
22
|
+
|
|
3
23
|
## 1.32.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
57
57
|
|
|
58
58
|
`AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. An unknown `appearance` throws a `RangeError`.
|
|
59
59
|
|
|
60
|
-
`Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
|
|
60
|
+
`Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
|
|
61
61
|
|
|
62
62
|
```tsx
|
|
63
63
|
<Steps aria-label="Heron survey">
|
package/dist/components/Box.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RadiusStep, type SemanticColorKey } from '@scalewing/tokens';
|
|
2
2
|
import { type AnchorHTMLAttributes, type HTMLAttributes } from 'react';
|
|
3
3
|
import { type BoxBorderValue } from '../css/css-box-border.js';
|
|
4
|
+
import { type BoxLinkAppearance } from '../css/css-box-link.js';
|
|
4
5
|
import { type ColumnSpanProps } from '../column-span-classes.js';
|
|
5
6
|
import { type SpacingProps } from '../spacing-classes.js';
|
|
6
7
|
import { type VisibilityProps } from '../visibility-classes.js';
|
|
@@ -15,8 +16,17 @@ export type BoxProps = SpacingProps & VisibilityProps & ColumnSpanProps & HTMLAt
|
|
|
15
16
|
* full-width bar, such as a section's sub-navigation).
|
|
16
17
|
*/
|
|
17
18
|
border?: BoxBorder;
|
|
19
|
+
/**
|
|
20
|
+
* How a `Box as="a"` reads: `accent` (default) is the canvas link;
|
|
21
|
+
* `quiet` keeps the surrounding text's colour at weight 600 and
|
|
22
|
+
* underlines on hover and focus, for a title that opens something,
|
|
23
|
+
* such as a table row's. Weight is its non-colour cue, so use it in
|
|
24
|
+
* text lighter than 600. Only for `as="a"`; an unknown value throws.
|
|
25
|
+
*/
|
|
26
|
+
link?: BoxLink;
|
|
18
27
|
};
|
|
19
28
|
export type BoxBorder = BoxBorderValue;
|
|
29
|
+
export type BoxLink = BoxLinkAppearance;
|
|
20
30
|
export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps & VisibilityProps & ColumnSpanProps & HTMLAttributes<HTMLElement> & Pick<AnchorHTMLAttributes<HTMLAnchorElement>, "rel" | "download" | "href" | "target"> & {
|
|
21
31
|
as?: BoxElement;
|
|
22
32
|
background?: Extract<SemanticColorKey, "background" | "surface">;
|
|
@@ -27,4 +37,12 @@ export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps
|
|
|
27
37
|
* full-width bar, such as a section's sub-navigation).
|
|
28
38
|
*/
|
|
29
39
|
border?: BoxBorder;
|
|
40
|
+
/**
|
|
41
|
+
* How a `Box as="a"` reads: `accent` (default) is the canvas link;
|
|
42
|
+
* `quiet` keeps the surrounding text's colour at weight 600 and
|
|
43
|
+
* underlines on hover and focus, for a title that opens something,
|
|
44
|
+
* such as a table row's. Weight is its non-colour cue, so use it in
|
|
45
|
+
* text lighter than 600. Only for `as="a"`; an unknown value throws.
|
|
46
|
+
*/
|
|
47
|
+
link?: BoxLink;
|
|
30
48
|
} & import("react").RefAttributes<HTMLElement>>;
|
package/dist/components/Box.js
CHANGED
|
@@ -2,10 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, } from 'react';
|
|
3
3
|
import { cx } from '../class-names.js';
|
|
4
4
|
import { boxBorderClass } from '../css/css-box-border.js';
|
|
5
|
+
import { assertBoxLink, boxLinkClass, } from '../css/css-box-link.js';
|
|
5
6
|
import { columnSpanClassNames, } from '../column-span-classes.js';
|
|
6
7
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
7
8
|
import { visibilityClassNames, } from '../visibility-classes.js';
|
|
8
|
-
export const Box = forwardRef(function Box({ as = 'div', background, border, className, columnSpan, hideBelow, hideFrom, padding, paddingBottom, paddingLeft, paddingRight, paddingTop, paddingX, paddingY, radius, style, ...rest }, ref) {
|
|
9
|
+
export const Box = forwardRef(function Box({ as = 'div', background, border, className, columnSpan, hideBelow, hideFrom, link, padding, paddingBottom, paddingLeft, paddingRight, paddingTop, paddingX, paddingY, radius, style, ...rest }, ref) {
|
|
10
|
+
assertBoxLink(link, as);
|
|
9
11
|
const Component = as;
|
|
10
12
|
return (_jsx(Component, { ref: ref, className: cx(...spacingClassNames({
|
|
11
13
|
padding,
|
|
@@ -15,7 +17,7 @@ export const Box = forwardRef(function Box({ as = 'div', background, border, cla
|
|
|
15
17
|
paddingTop,
|
|
16
18
|
paddingX,
|
|
17
19
|
paddingY,
|
|
18
|
-
}), ...visibilityClassNames({ hideBelow, hideFrom }), ...columnSpanClassNames({ columnSpan }), boxBorderClass(border), className), style: {
|
|
20
|
+
}), ...visibilityClassNames({ hideBelow, hideFrom }), ...columnSpanClassNames({ columnSpan }), boxBorderClass(border), boxLinkClass(link), className), style: {
|
|
19
21
|
backgroundColor: background
|
|
20
22
|
? `var(--sw-color-${background})`
|
|
21
23
|
: undefined,
|
package/dist/components/Split.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { spacingScale } from '@scalewing/tokens';
|
|
4
|
-
import { useCallback, useId,
|
|
4
|
+
import { useCallback, useId, useRef, useState, } from 'react';
|
|
5
5
|
import { cx } from '../class-names.js';
|
|
6
|
-
import { applySplitDrag, applySplitKey,
|
|
7
|
-
|
|
8
|
-
const styles = getComputedStyle(node);
|
|
9
|
-
const remPx = cssLengthToPx(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
10
|
-
return readSplitMetrics((name) => styles.getPropertyValue(name), remPx);
|
|
11
|
-
}
|
|
6
|
+
import { applySplitDrag, applySplitKey, clampSplitSize, splitPercent, } from '../split-size.js';
|
|
7
|
+
import { useSplitRange } from './use-split-range.js';
|
|
12
8
|
export function Split({ children, className, collapsed: collapsedProp, label, onCollapsedChange, ...rest }) {
|
|
13
9
|
const paneId = useId();
|
|
14
10
|
const splitRef = useRef(null);
|
|
15
11
|
const handleRef = useRef(null);
|
|
16
12
|
const dragRef = useRef(null);
|
|
17
13
|
const [width, setWidth] = useState(null);
|
|
18
|
-
const [range, setRange] = useState(null);
|
|
19
14
|
const [collapsedUncontrolled, setCollapsedUncontrolled] = useState(false);
|
|
20
15
|
const collapsed = collapsedProp ?? collapsedUncontrolled;
|
|
21
16
|
const setCollapsed = useCallback((next) => {
|
|
@@ -24,38 +19,25 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
24
19
|
setCollapsedUncontrolled(next);
|
|
25
20
|
}
|
|
26
21
|
}, [collapsedProp, onCollapsedChange]);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
setWidth((current) => current ?? measured.defaultWidth);
|
|
35
|
-
}, []);
|
|
36
|
-
function dragMetrics() {
|
|
37
|
-
const split = splitRef.current;
|
|
38
|
-
const handle = handleRef.current;
|
|
39
|
-
if (!split) {
|
|
40
|
-
return { min: 0, max: 0, step: 16, defaultWidth: 0 };
|
|
41
|
-
}
|
|
42
|
-
const measured = metricsFrom(split);
|
|
43
|
-
const parentWidth = split.parentElement?.getBoundingClientRect().width ?? 0;
|
|
44
|
-
const handleWidth = handle?.getBoundingClientRect().width ?? 0;
|
|
45
|
-
return {
|
|
46
|
-
...measured,
|
|
47
|
-
max: effectiveSplitMax(measured.max, parentWidth, handleWidth, measured.min),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function currentSnapshot() {
|
|
51
|
-
const measured = dragMetrics();
|
|
22
|
+
const { range, measure } = useSplitRange(splitRef, handleRef);
|
|
23
|
+
/**
|
|
24
|
+
* `width` is the reader's chosen width; what shows is it held to the range
|
|
25
|
+
* the container leaves, so an expansion never shows a pane under its
|
|
26
|
+
* minimum or past the room, and a gesture starts from what shows.
|
|
27
|
+
*/
|
|
28
|
+
function currentSnapshot(metrics) {
|
|
52
29
|
return {
|
|
53
30
|
collapsed,
|
|
54
|
-
width: width ??
|
|
31
|
+
width: clampSplitSize(width ?? metrics.defaultWidth, metrics.min, metrics.max),
|
|
55
32
|
};
|
|
56
33
|
}
|
|
57
|
-
function applySnapshot(next) {
|
|
58
|
-
|
|
34
|
+
function applySnapshot(next, start) {
|
|
35
|
+
// Only a gesture that moves the pane chooses a width: a collapse, or a
|
|
36
|
+
// move held at the end of a narrow container's range, keeps the chosen
|
|
37
|
+
// one, so it shows again when the container widens.
|
|
38
|
+
if (!next.collapsed && next.width !== start.width) {
|
|
39
|
+
setWidth(next.width);
|
|
40
|
+
}
|
|
59
41
|
if (next.collapsed !== collapsed) {
|
|
60
42
|
setCollapsed(next.collapsed);
|
|
61
43
|
}
|
|
@@ -67,11 +49,12 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
67
49
|
event.preventDefault();
|
|
68
50
|
event.currentTarget.focus();
|
|
69
51
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
52
|
+
const metrics = measure();
|
|
70
53
|
dragRef.current = {
|
|
71
54
|
pointerId: event.pointerId,
|
|
72
55
|
startX: event.clientX,
|
|
73
|
-
snapshot: currentSnapshot(),
|
|
74
|
-
metrics
|
|
56
|
+
snapshot: currentSnapshot(metrics),
|
|
57
|
+
metrics,
|
|
75
58
|
moved: false,
|
|
76
59
|
};
|
|
77
60
|
}
|
|
@@ -84,7 +67,7 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
84
67
|
if (Math.abs(delta) >= spacingScale[1]) {
|
|
85
68
|
drag.moved = true;
|
|
86
69
|
}
|
|
87
|
-
applySnapshot(applySplitDrag(drag.snapshot, delta, drag.metrics));
|
|
70
|
+
applySnapshot(applySplitDrag(drag.snapshot, delta, drag.metrics), drag.snapshot);
|
|
88
71
|
}
|
|
89
72
|
function endDrag(event) {
|
|
90
73
|
const drag = dragRef.current;
|
|
@@ -101,17 +84,21 @@ export function Split({ children, className, collapsed: collapsedProp, label, on
|
|
|
101
84
|
if (!splitRef.current) {
|
|
102
85
|
return;
|
|
103
86
|
}
|
|
104
|
-
const
|
|
87
|
+
const metrics = measure();
|
|
88
|
+
const start = currentSnapshot(metrics);
|
|
89
|
+
const next = applySplitKey(event.key, start, metrics);
|
|
105
90
|
if (!next) {
|
|
106
91
|
return;
|
|
107
92
|
}
|
|
108
93
|
event.preventDefault();
|
|
109
|
-
applySnapshot(next);
|
|
94
|
+
applySnapshot(next, start);
|
|
110
95
|
}
|
|
111
|
-
|
|
96
|
+
// What shows: the chosen width held to the container's range.
|
|
97
|
+
const shownWidth = range
|
|
98
|
+
? clampSplitSize(width ?? range.defaultWidth, range.min, range.max)
|
|
99
|
+
: null;
|
|
100
|
+
const valueNow = collapsed || !range || shownWidth === null
|
|
112
101
|
? 0
|
|
113
|
-
: range
|
|
114
|
-
|
|
115
|
-
: 0;
|
|
116
|
-
return (_jsxs("div", { ...rest, ref: splitRef, className: cx('sw-split', className), "data-collapsed": collapsed ? '' : undefined, children: [_jsx("div", { className: "sw-split-pane", id: paneId, "aria-hidden": collapsed || undefined, style: width === null ? undefined : { width: `${width}px` }, children: children }), _jsx("div", { ref: handleRef, "aria-controls": paneId, "aria-label": collapsed ? `Show ${label}` : `Resize ${label}`, "aria-orientation": "vertical", "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": valueNow, className: "sw-split-handle", onKeyDown: onKeyDown, onPointerCancel: endDrag, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, role: "separator", tabIndex: 0, children: _jsx("span", { "aria-hidden": "true", className: "sw-split-grip" }) })] }));
|
|
102
|
+
: splitPercent(shownWidth, range.min, range.max);
|
|
103
|
+
return (_jsxs("div", { ...rest, ref: splitRef, className: cx('sw-split', className), "data-collapsed": collapsed ? '' : undefined, children: [_jsx("div", { className: "sw-split-pane", id: paneId, "aria-hidden": collapsed || undefined, style: shownWidth === null ? undefined : { width: `${shownWidth}px` }, children: children }), _jsx("div", { ref: handleRef, "aria-controls": paneId, "aria-label": collapsed ? `Show ${label}` : `Resize ${label}`, "aria-orientation": "vertical", "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": valueNow, className: "sw-split-handle", onKeyDown: onKeyDown, onPointerCancel: endDrag, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, role: "separator", tabIndex: 0, children: _jsx("span", { "aria-hidden": "true", className: "sw-split-grip" }) })] }));
|
|
117
104
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type SplitMetrics } from '../split-size.js';
|
|
3
|
+
export type SplitRange = Pick<SplitMetrics, 'min' | 'max'> & {
|
|
4
|
+
defaultWidth: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The range a Split's pane can take in its container, measured on layout and
|
|
8
|
+
* whenever the container resizes, and `measure` for a gesture, which also
|
|
9
|
+
* brings the shown range up to date. An unchanged range keeps its state, so
|
|
10
|
+
* a resize that changes nothing does not render again.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSplitRange(splitRef: RefObject<HTMLDivElement | null>, handleRef: RefObject<HTMLDivElement | null>): {
|
|
13
|
+
range: SplitRange | null;
|
|
14
|
+
measure: () => SplitMetrics & {
|
|
15
|
+
defaultWidth: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { containerContentWidth, splitTokenMetrics } from '../split-measure.js';
|
|
4
|
+
import { effectiveSplitRange } from '../split-size.js';
|
|
5
|
+
function sameRange(a, b) {
|
|
6
|
+
return (a !== null &&
|
|
7
|
+
a.min === b.min &&
|
|
8
|
+
a.max === b.max &&
|
|
9
|
+
a.defaultWidth === b.defaultWidth);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The range a Split's pane can take in its container, measured on layout and
|
|
13
|
+
* whenever the container resizes, and `measure` for a gesture, which also
|
|
14
|
+
* brings the shown range up to date. An unchanged range keeps its state, so
|
|
15
|
+
* a resize that changes nothing does not render again.
|
|
16
|
+
*/
|
|
17
|
+
export function useSplitRange(splitRef, handleRef) {
|
|
18
|
+
const [range, setRange] = useState(null);
|
|
19
|
+
const measure = useCallback(() => {
|
|
20
|
+
const split = splitRef.current;
|
|
21
|
+
if (!split) {
|
|
22
|
+
return { min: 0, max: 0, step: 16, defaultWidth: 0 };
|
|
23
|
+
}
|
|
24
|
+
const tokens = splitTokenMetrics(split);
|
|
25
|
+
const handleWidth = handleRef.current?.getBoundingClientRect().width ?? 0;
|
|
26
|
+
const measured = {
|
|
27
|
+
...tokens,
|
|
28
|
+
...effectiveSplitRange(tokens, containerContentWidth(split), handleWidth),
|
|
29
|
+
};
|
|
30
|
+
const next = {
|
|
31
|
+
min: measured.min,
|
|
32
|
+
max: measured.max,
|
|
33
|
+
defaultWidth: measured.defaultWidth,
|
|
34
|
+
};
|
|
35
|
+
setRange((current) => (sameRange(current, next) ? current : next));
|
|
36
|
+
return measured;
|
|
37
|
+
}, [splitRef, handleRef]);
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const parent = splitRef.current?.parentElement;
|
|
40
|
+
measure();
|
|
41
|
+
if (!parent || typeof ResizeObserver === 'undefined') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const observer = new ResizeObserver(() => measure());
|
|
45
|
+
observer.observe(parent);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [measure, splitRef]);
|
|
48
|
+
return { range, measure };
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a `Box as="a"` reads as a link. `accent` (the default) is the
|
|
3
|
+
* canvas link: the accent colour, no underline. `quiet` is a title that
|
|
4
|
+
* opens something, such as the title of a table row: the surrounding
|
|
5
|
+
* text's colour at weight 600, underlined while hovered or focused.
|
|
6
|
+
*/
|
|
7
|
+
export declare const boxLinkAppearances: readonly ["accent", "quiet"];
|
|
8
|
+
export type BoxLinkAppearance = (typeof boxLinkAppearances)[number];
|
|
9
|
+
export declare function assertBoxLink(link: unknown, as: string): void;
|
|
10
|
+
/** The generated class for a Box `link` prop, or none for the canvas link. */
|
|
11
|
+
export declare function boxLinkClass(link: BoxLinkAppearance | undefined): string | undefined;
|
|
12
|
+
export declare function cssBoxLinkClasses(): string;
|
|
13
|
+
export declare function boxLinkClassCatalog(): string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
/**
|
|
3
|
+
* How a `Box as="a"` reads as a link. `accent` (the default) is the
|
|
4
|
+
* canvas link: the accent colour, no underline. `quiet` is a title that
|
|
5
|
+
* opens something, such as the title of a table row: the surrounding
|
|
6
|
+
* text's colour at weight 600, underlined while hovered or focused.
|
|
7
|
+
*/
|
|
8
|
+
export const boxLinkAppearances = ['accent', 'quiet'];
|
|
9
|
+
const quietLinkClass = 'sw-link-quiet';
|
|
10
|
+
export function assertBoxLink(link, as) {
|
|
11
|
+
if (link === undefined)
|
|
12
|
+
return;
|
|
13
|
+
if (!boxLinkAppearances.includes(link))
|
|
14
|
+
throw new RangeError(`Unknown Box link: ${String(link)}`);
|
|
15
|
+
if (as !== 'a')
|
|
16
|
+
throw new TypeError('Box link is for a link (as="a")');
|
|
17
|
+
}
|
|
18
|
+
/** The generated class for a Box `link` prop, or none for the canvas link. */
|
|
19
|
+
export function boxLinkClass(link) {
|
|
20
|
+
return link === 'quiet' ? quietLinkClass : undefined;
|
|
21
|
+
}
|
|
22
|
+
/*
|
|
23
|
+
* A quiet link keeps the colour of the text around it, so colour cannot
|
|
24
|
+
* tell it apart. Its weight does: 600, the label weight, in body-weight
|
|
25
|
+
* text, the non-colour cue WCAG 1.4.1 asks for, and one that forced colours
|
|
26
|
+
* keep. (axe's link-in-text-block passes it on colour, the same colour
|
|
27
|
+
* and background as its text, or on its weight; the weight is what keeps it
|
|
28
|
+
* a visible link.) Use it in text lighter
|
|
29
|
+
* than 600, such as a body row's title. In text already at 600 the quiet
|
|
30
|
+
* look has no cue; an accent link there needs at least 3:1 against the text.
|
|
31
|
+
* Hover and keyboard focus underline it; focus keeps the canvas focus ring.
|
|
32
|
+
* The rules outrank the canvas `[data-theme] a` rules, hover included.
|
|
33
|
+
* Forced colours paint it in the system link colour, as any link.
|
|
34
|
+
*/
|
|
35
|
+
export function cssBoxLinkClasses() {
|
|
36
|
+
const quiet = `[data-theme] a.${quietLinkClass}`;
|
|
37
|
+
return `${quiet} {
|
|
38
|
+
color: inherit;
|
|
39
|
+
font-weight: ${typographyVariants.label.fontWeight};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
${quiet}:hover,
|
|
43
|
+
${quiet}:focus-visible {
|
|
44
|
+
color: inherit;
|
|
45
|
+
text-decoration-line: underline;
|
|
46
|
+
}`;
|
|
47
|
+
}
|
|
48
|
+
export function boxLinkClassCatalog() {
|
|
49
|
+
return [quietLinkClass];
|
|
50
|
+
}
|
package/dist/css/css-document.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { cssBoxLinkClasses } from './css-box-link.js';
|
|
2
3
|
const body = typographyVariants.body;
|
|
3
4
|
/** The native controls that take typed text: the canvas's text controls. */
|
|
4
5
|
export const typedInputs = [
|
|
@@ -97,6 +98,8 @@ body {
|
|
|
97
98
|
outline-offset: var(--sw-focus-ring-offset);
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
${cssBoxLinkClasses()}
|
|
102
|
+
|
|
100
103
|
/*
|
|
101
104
|
* A programmatic focus target (tabindex="-1": a notice, a card or a heading
|
|
102
105
|
* that a script focuses after a save) takes the accent ring past the ring
|
|
@@ -9,9 +9,13 @@ export declare function scrollShadeRules(selector: string): string;
|
|
|
9
9
|
/**
|
|
10
10
|
* A scroll region is a full-width box that scrolls its own content when the
|
|
11
11
|
* content is wider (or taller) than the box, so a wide table never widens the
|
|
12
|
-
* page. It is
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* page. It is positioned, so it is the containing block of anything
|
|
13
|
+
* absolutely placed inside it that no nearer box holds, such as a visually
|
|
14
|
+
* hidden label (`sw-sr-only`) in a cell past its edge: that stays inside the
|
|
15
|
+
* scroll, not placed against the page, where it would widen it. It is a
|
|
16
|
+
* keyboard stop, so its focus ring is drawn inside its edge where an
|
|
17
|
+
* ancestor's clipping cannot hide it. While its content is scrolled out past
|
|
18
|
+
* an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
|
|
15
19
|
* `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
|
|
16
20
|
* goes on, which overlay scrollbars (phones, macOS) do not give. In forced
|
|
17
21
|
* colors the shade is not drawn and the system's scrollbar is the cue. Table
|
|
@@ -34,9 +34,13 @@ export function scrollShadeRules(selector) {
|
|
|
34
34
|
/**
|
|
35
35
|
* A scroll region is a full-width box that scrolls its own content when the
|
|
36
36
|
* content is wider (or taller) than the box, so a wide table never widens the
|
|
37
|
-
* page. It is
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* page. It is positioned, so it is the containing block of anything
|
|
38
|
+
* absolutely placed inside it that no nearer box holds, such as a visually
|
|
39
|
+
* hidden label (`sw-sr-only`) in a cell past its edge: that stays inside the
|
|
40
|
+
* scroll, not placed against the page, where it would widen it. It is a
|
|
41
|
+
* keyboard stop, so its focus ring is drawn inside its edge where an
|
|
42
|
+
* ancestor's clipping cannot hide it. While its content is scrolled out past
|
|
43
|
+
* an inline edge, `ScrollRegion` adds `sw-scroll-more-start` or
|
|
40
44
|
* `sw-scroll-more-end`, and that edge draws a shade: the cue that the content
|
|
41
45
|
* goes on, which overlay scrollbars (phones, macOS) do not give. In forced
|
|
42
46
|
* colors the shade is not drawn and the system's scrollbar is the cue. Table
|
|
@@ -45,6 +49,7 @@ export function scrollShadeRules(selector) {
|
|
|
45
49
|
export function scrollRegionRules(selector) {
|
|
46
50
|
return `${selector} {
|
|
47
51
|
overflow: auto;
|
|
52
|
+
position: relative;
|
|
48
53
|
width: 100%;
|
|
49
54
|
}
|
|
50
55
|
|
package/dist/css/css-split.js
CHANGED
package/dist/css/stylesheet.js
CHANGED
|
@@ -27,6 +27,7 @@ import { fieldClassCatalog } from './css-field.js';
|
|
|
27
27
|
import { stepperClassCatalog } from './css-stepper.js';
|
|
28
28
|
import { stepsClassCatalog } from './css-steps.js';
|
|
29
29
|
import { boxBorderClassCatalog } from './css-box-border.js';
|
|
30
|
+
import { boxLinkClassCatalog } from './css-box-link.js';
|
|
30
31
|
import { cssComponentClasses } from './css-components.js';
|
|
31
32
|
import { buttonClassCatalog } from './css-button.js';
|
|
32
33
|
import { chartClassCatalog } from './css-chart.js';
|
|
@@ -106,6 +107,7 @@ export function utilityClassCatalog() {
|
|
|
106
107
|
...toastCardClassCatalog(),
|
|
107
108
|
...responsiveClassCatalog(),
|
|
108
109
|
...boxBorderClassCatalog(),
|
|
110
|
+
...boxLinkClassCatalog(),
|
|
109
111
|
];
|
|
110
112
|
}
|
|
111
113
|
export { generatePaletteStylesheet, paletteStylesheetIds, } from './css-palettes.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export { Badge, type BadgeProps, type BadgeSize, type BadgeTone, badgeSizes, bad
|
|
|
18
18
|
export { BarChart, type BarChartItem, type BarChartProps, type BarChartTone, barChartTones, } from './components/BarChart.js';
|
|
19
19
|
export { Button, type ButtonDisabledAppearance, type ButtonProps, type ButtonSize, type ButtonVariant, } from './components/Button.js';
|
|
20
20
|
export { buttonDisabledAppearances, webButtonVariants, } from './css/css-button.js';
|
|
21
|
-
export { Box, type BoxBorder, type BoxElement, type BoxProps, } from './components/Box.js';
|
|
21
|
+
export { Box, type BoxBorder, type BoxElement, type BoxLink, type BoxProps, } from './components/Box.js';
|
|
22
|
+
export { boxLinkAppearances } from './css/css-box-link.js';
|
|
22
23
|
export { ButtonGroup, buttonGroupAppearances, type ButtonGroupAppearance, type ButtonGroupJustify, type ButtonGroupProps, } from './components/ButtonGroup.js';
|
|
23
24
|
export { Card, type CardProps, type CardVariant } from './components/Card.js';
|
|
24
25
|
export { Dialog, dialogAppearances, dialogSizes, type DialogAppearance, type DialogProps, type DialogRole, type DialogSize, type DialogTitleLevel, } from './components/Dialog.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { BarChart, barChartTones, } from './components/BarChart.js';
|
|
|
18
18
|
export { Button, } from './components/Button.js';
|
|
19
19
|
export { buttonDisabledAppearances, webButtonVariants, } from './css/css-button.js';
|
|
20
20
|
export { Box, } from './components/Box.js';
|
|
21
|
+
export { boxLinkAppearances } from './css/css-box-link.js';
|
|
21
22
|
export { ButtonGroup, buttonGroupAppearances, } from './components/ButtonGroup.js';
|
|
22
23
|
export { Card } from './components/Card.js';
|
|
23
24
|
export { Dialog, dialogAppearances, dialogSizes, } from './components/Dialog.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SplitMetrics } from './split-size.js';
|
|
2
|
+
/** The container's content width, which the split's percentage cap uses. */
|
|
3
|
+
export declare function containerContentWidth(node: HTMLElement): number;
|
|
4
|
+
/** The split's token range, step and default width, in px. */
|
|
5
|
+
export declare function splitTokenMetrics(node: HTMLElement): SplitMetrics & {
|
|
6
|
+
defaultWidth: number;
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// DOM reads for Split: the split's token metrics from its computed custom
|
|
2
|
+
// properties, and its container's content width (what the stylesheet's
|
|
3
|
+
// percentage cap resolves against). The range math stays pure in
|
|
4
|
+
// `split-size.ts`.
|
|
5
|
+
import { cssLengthToPx, readSplitMetrics, } from './split-size.js';
|
|
6
|
+
/** The container's content width, which the split's percentage cap uses. */
|
|
7
|
+
export function containerContentWidth(node) {
|
|
8
|
+
const parent = node.parentElement;
|
|
9
|
+
if (!parent) {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
const styles = getComputedStyle(parent);
|
|
13
|
+
const padding = cssLengthToPx(styles.paddingLeft) + cssLengthToPx(styles.paddingRight);
|
|
14
|
+
return Math.max(0, parent.clientWidth - padding);
|
|
15
|
+
}
|
|
16
|
+
/** The split's token range, step and default width, in px. */
|
|
17
|
+
export function splitTokenMetrics(node) {
|
|
18
|
+
const styles = getComputedStyle(node);
|
|
19
|
+
const remPx = cssLengthToPx(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
20
|
+
return readSplitMetrics((name) => styles.getPropertyValue(name), remPx);
|
|
21
|
+
}
|
package/dist/split-size.d.ts
CHANGED
|
@@ -12,7 +12,21 @@ export declare function readSplitMetrics(read: (name: string) => string, remPx?:
|
|
|
12
12
|
defaultWidth: number;
|
|
13
13
|
};
|
|
14
14
|
export declare function clampSplitSize(size: number, min: number, max: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* The pane's place between its minimum and maximum as 0–100. When the
|
|
17
|
+
* container leaves no range (min equals max), the pane is as wide as it can
|
|
18
|
+
* be: 100.
|
|
19
|
+
*/
|
|
15
20
|
export declare function splitPercent(width: number, min: number, max: number): number;
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The range a pane can take in its container. The stylesheet keeps the split
|
|
23
|
+
* at most its container less the split minimum, so the next column keeps that
|
|
24
|
+
* minimum and the pane gets the room left after the handle. In a narrow
|
|
25
|
+
* container that room is under the token minimum, so the range narrows to it:
|
|
26
|
+
* a pane never grows past what shows, and only a move narrower than the room
|
|
27
|
+
* collapses it. A container that is not laid out (zero width) keeps the
|
|
28
|
+
* token range.
|
|
29
|
+
*/
|
|
30
|
+
export declare function effectiveSplitRange(tokens: Pick<SplitMetrics, 'min' | 'max'>, containerWidth: number, handleWidth: number): Pick<SplitMetrics, 'min' | 'max'>;
|
|
17
31
|
export declare function applySplitDrag(start: SplitSnapshot, deltaX: number, metrics: SplitMetrics): SplitSnapshot;
|
|
18
32
|
export declare function applySplitKey(key: string, start: SplitSnapshot, metrics: SplitMetrics): SplitSnapshot | null;
|
package/dist/split-size.js
CHANGED
|
@@ -23,21 +23,33 @@ export function readSplitMetrics(read, remPx = 16) {
|
|
|
23
23
|
export function clampSplitSize(size, min, max) {
|
|
24
24
|
return Math.min(max, Math.max(min, size));
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The pane's place between its minimum and maximum as 0–100. When the
|
|
28
|
+
* container leaves no range (min equals max), the pane is as wide as it can
|
|
29
|
+
* be: 100.
|
|
30
|
+
*/
|
|
26
31
|
export function splitPercent(width, min, max) {
|
|
27
32
|
if (max <= min) {
|
|
28
|
-
return
|
|
33
|
+
return 100;
|
|
29
34
|
}
|
|
30
35
|
return Math.round(((clampSplitSize(width, min, max) - min) / (max - min)) * 100);
|
|
31
36
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The range a pane can take in its container. The stylesheet keeps the split
|
|
39
|
+
* at most its container less the split minimum, so the next column keeps that
|
|
40
|
+
* minimum and the pane gets the room left after the handle. In a narrow
|
|
41
|
+
* container that room is under the token minimum, so the range narrows to it:
|
|
42
|
+
* a pane never grows past what shows, and only a move narrower than the room
|
|
43
|
+
* collapses it. A container that is not laid out (zero width) keeps the
|
|
44
|
+
* token range.
|
|
45
|
+
*/
|
|
46
|
+
export function effectiveSplitRange(tokens, containerWidth, handleWidth) {
|
|
47
|
+
if (containerWidth <= 0) {
|
|
48
|
+
return { min: tokens.min, max: tokens.max };
|
|
35
49
|
}
|
|
36
|
-
const room =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return Math.min(tokenMax, room);
|
|
50
|
+
const room = Math.max(0, containerWidth - handleWidth - tokens.min);
|
|
51
|
+
const max = Math.min(tokens.max, room);
|
|
52
|
+
return { min: Math.min(tokens.min, max), max };
|
|
41
53
|
}
|
|
42
54
|
export function applySplitDrag(start, deltaX, metrics) {
|
|
43
55
|
if (start.collapsed) {
|
package/dist/styles.css
CHANGED
|
@@ -628,6 +628,17 @@ body {
|
|
|
628
628
|
outline-offset: var(--sw-focus-ring-offset);
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
[data-theme] a.sw-link-quiet {
|
|
632
|
+
color: inherit;
|
|
633
|
+
font-weight: 600;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
[data-theme] a.sw-link-quiet:hover,
|
|
637
|
+
[data-theme] a.sw-link-quiet:focus-visible {
|
|
638
|
+
color: inherit;
|
|
639
|
+
text-decoration-line: underline;
|
|
640
|
+
}
|
|
641
|
+
|
|
631
642
|
/*
|
|
632
643
|
* A programmatic focus target (tabindex="-1": a notice, a card or a heading
|
|
633
644
|
* that a script focuses after a save) takes the accent ring past the ring
|
|
@@ -1537,6 +1548,7 @@ body {
|
|
|
1537
1548
|
|
|
1538
1549
|
.sw-table-wrap {
|
|
1539
1550
|
overflow: auto;
|
|
1551
|
+
position: relative;
|
|
1540
1552
|
width: 100%;
|
|
1541
1553
|
}
|
|
1542
1554
|
|
|
@@ -4348,6 +4360,7 @@ body {
|
|
|
4348
4360
|
|
|
4349
4361
|
.sw-denomination-scroll {
|
|
4350
4362
|
overflow: auto;
|
|
4363
|
+
position: relative;
|
|
4351
4364
|
width: 100%;
|
|
4352
4365
|
}
|
|
4353
4366
|
|
|
@@ -5308,6 +5321,7 @@ body {
|
|
|
5308
5321
|
display: flex;
|
|
5309
5322
|
flex: 0 0 auto;
|
|
5310
5323
|
flex-direction: row;
|
|
5324
|
+
max-width: calc(100% - var(--sw-split-min));
|
|
5311
5325
|
min-width: 0;
|
|
5312
5326
|
}
|
|
5313
5327
|
|