highsoft-ui 1.0.171 → 1.0.172
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/CLAUDE.md +83 -0
- package/dist/Toggle-BHjptO7-.js +88 -0
- package/dist/ToggleCard-kMpxzTnh.js +57 -0
- package/dist/ToggleList-Dh7wb2-W.js +31 -0
- package/dist/components/Toggle/index.js +2 -0
- package/dist/components/ToggleCard/index.js +2 -0
- package/dist/components/ToggleList/context.js +6 -0
- package/dist/components/ToggleList/index.js +2 -0
- package/dist/highsoft-ui.css +1 -1
- package/dist/lib/components/Toggle/index.d.ts +34 -0
- package/dist/lib/components/ToggleCard/index.d.ts +19 -0
- package/dist/lib/components/ToggleList/context.d.ts +8 -0
- package/dist/lib/components/ToggleList/index.d.ts +20 -0
- package/dist/lib/main.d.ts +6 -0
- package/dist/main.js +17 -14
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -222,6 +222,89 @@ const viewItems: ButtonGroupItem[] = [
|
|
|
222
222
|
- In select mode each item exposes `aria-pressed` for its selected state; with `selectable={false}` the attribute is omitted entirely, so AT hears plain buttons, not toggles stuck "off".
|
|
223
223
|
- Items are real `Button`s: native `disabled` semantics, brand `:focus-visible` ring, and a transform-only transition on the design tokens that respects `prefers-reduced-motion`. Icon-only items need `buttonProps={{ 'aria-label': … }}` — the icon alone names nothing.
|
|
224
224
|
|
|
225
|
+
### Toggle
|
|
226
|
+
On/off switch for settings that apply immediately. Sizes, label type and disabled treatment match `Button`.
|
|
227
|
+
```tsx
|
|
228
|
+
<Toggle label="Email notifications" checked={on} onChange={setOn} />
|
|
229
|
+
<Toggle label="Animation" defaultChecked size={300} />
|
|
230
|
+
|
|
231
|
+
// Settings row: label left, switch on the far edge, description under the label
|
|
232
|
+
<Toggle fullWidth labelPosition="left" label="Credits"
|
|
233
|
+
description="Show the Highcharts.com credit in the chart corner." />
|
|
234
|
+
|
|
235
|
+
// No visible label — the surrounding UI names it
|
|
236
|
+
<Toggle aria-label="Show grid lines" />
|
|
237
|
+
|
|
238
|
+
// In a form: submits name=value while on, nothing while off (like a checkbox)
|
|
239
|
+
<Toggle label="Subscribe" name="subscribe" value="yes" />
|
|
240
|
+
```
|
|
241
|
+
- **`checked` / `defaultChecked` / `onChange(checked)`:** controlled or uncontrolled
|
|
242
|
+
- **`size`:** `50`–`500` (default `200`) — the row is as tall as a Button of the same size and the label uses the same type, so a Toggle lines up next to Buttons; the track scales with it (18×32 at 200)
|
|
243
|
+
- **`variant`:** `brand` (default) | `success` | `attention` | `danger` — the on-track colour, from the Button tokens of the same name. Keep `brand` for ordinary settings; use a status colour only when the on state itself means something (`danger` for "Maintenance mode"), and say it in the label — colour alone conveys nothing. **Exception:** light-mode `attention` uses `--primitives-attention-600` (hover 700), because the attention Button fill (attention-500) is only 2.15:1 against white, under WCAG 1.4.11's 3:1; dark follows the Button tokens.
|
|
244
|
+
- **`label`**, **`description`**, **`labelPosition`** (`right` default | `left`), **`fullWidth`** (the label fills the row so the switch sits on the far edge)
|
|
245
|
+
- **`name` / `value`** (default `'on'`) for native form submission; **`disabled`**; other props and the ref go to the `<button>`
|
|
246
|
+
- Off track is `--HS-text-tertiary-default`, on is the brand button background; disabled uses the Button disabled neutrals for both states, so on/off differ by thumb position only. Like `ButtonGroup`, **colours are not transitioned** — only the thumb's `transform`/`width` (bounce-fast) and the focus ring animate.
|
|
247
|
+
- Use a Toggle for an instant on/off setting; use `Checkbox` when the choice is submitted later or is one of several. For a bordered card where the whole card is the target, use `ToggleCard`.
|
|
248
|
+
- The track and text block carry global `hsui-toggle__track` / `hsui-toggle__text` classes so a wrapper (ToggleCard, app CSS) can style it; the module classes are hashed.
|
|
249
|
+
- **Disabled** is one neutral treatment for every variant (on/off differ by thumb position only). Light uses the disabled Button surface (neutral-300 track, white thumb). Dark has its own set — neutral-700 track with no edge (same shape as every other state; on background-100 it fades out and the thumb carries the state), neutral-500 thumb — because the Button's disabled text colour (neutral-400) made the thumb brighter than an *enabled* off thumb, and its neutral-1000 stroke drew a heavy black ring. Disabled controls are exempt from WCAG 1.4.11; the aim is recessive but recognisable.
|
|
250
|
+
|
|
251
|
+
**Accessibility:**
|
|
252
|
+
- A real `<button type="button" role="switch" aria-checked>`: Space and Enter flip it, it's in the tab order, and AT announces "Email notifications, switch, on". Disabled uses the native `disabled` attribute.
|
|
253
|
+
- The visible `label` is a `<label for>` the button **and** its `aria-labelledby`, so the name is exactly the visible text (WCAG 2.5.3). The **whole text block** is a click target: label, description, and in a `fullWidth` row the free space up to the switch. A click outside the `<label>` is forwarded to the button with `.click()`; links or other controls inside the description keep their own click and don't toggle. Padding you put *around* a Toggle is not a target — if the whole row/card should be, use `ToggleCard`. Without `label`, pass `aria-label`. `description` is wired via `aria-describedby` (merged with any you pass).
|
|
254
|
+
- State never rests on colour: the thumb position changes, and the off track (`--HS-text-tertiary-default`) clears 3:1 against every surface in both themes (WCAG 1.4.11).
|
|
255
|
+
- `:focus-visible` draws the Button's two-tone ring (background gap + brand) around the pill; `data-hs-pointer-focus` suppresses it like on Button. The thumb is `aria-hidden`.
|
|
256
|
+
- On touch (`pointer: coarse`) sizes 50/100 get an invisible hit area at least 24px tall (WCAG 2.5.8); the label is a click target at every size.
|
|
257
|
+
- Under `prefers-reduced-motion: reduce` the thumb jumps instead of sliding.
|
|
258
|
+
|
|
259
|
+
### ToggleCard
|
|
260
|
+
A **switch card**: a bordered card around a `Toggle` where a click or tap anywhere on the card flips the switch. It wraps `Toggle`, so it takes the same props (`label`, `description`, `checked`/`defaultChecked`/`onChange`, `size`, `name`/`value`, `disabled`; the ref goes to the switch `<button>`).
|
|
261
|
+
```tsx
|
|
262
|
+
<ToggleCard label="Credits" description="Show the Highcharts.com credit." defaultChecked />
|
|
263
|
+
|
|
264
|
+
// On a tinted surface, or next to transparent Buttons
|
|
265
|
+
<ToggleCard transparent label="Animation" />
|
|
266
|
+
|
|
267
|
+
// Status colour: only when "on" itself means something
|
|
268
|
+
<ToggleCard variant="danger" label="Maintenance mode" description="The public site shows a maintenance page." />
|
|
269
|
+
```
|
|
270
|
+
- **`variant`:** `brand` (default) | `success` | `attention` | `danger` — passed to the Toggle, and colours the card's on state to match
|
|
271
|
+
- **`transparent`:** no fill, the transparent Button's background + hairline tokens (the same chrome as ButtonGroup `transparent`); default is a background-0 card. A boolean rather than a variant, because `variant` is the colour.
|
|
272
|
+
- **`labelPosition`** defaults to `left` (text left, switch on the right edge); the card is always full width. **`size`** `50`–`500` also scales the card padding (12/16px at 200).
|
|
273
|
+
- Same resting stroke and corner as the other form controls (1px border-200, 8px), 2px on hover, 2px brand stroke when on. The default card adds a tint of the variant colour when on; `transparent` keeps no fill. Disabled keeps the card's own surface with a border-100 edge and muted text, on or off — not a background-50 fill, which in dark (neutral-800) is lighter than an enabled card and made the disabled one stand out most. Stroke changes are not transitioned (theme flips would fade).
|
|
274
|
+
- **The on-state colours are primitives, as the component's own light/dark pair per variant** (`--toggle-card-background-checked`, `--toggle-card-stroke-checked[-hover]`, remapped under `[data-theme='dark']` like ButtonGroup's container tokens); the Toggle inside keeps the semantic Button tokens. Light: a `-50` wash, stroke matching the track. Dark: the darkest wash that still clears 3:1 against the 600 track — `-1000`, danger `-1100` (danger-1000 is 2.96:1) — stroke a step lighter than the track. Don't swap in `--HS-surface-brand-background-50`: it remaps to brand-600 in dark, the track's own fill, and the switch disappears into the card. `.card.transparent` is written at that specificity on purpose so it outranks the dark blocks.
|
|
275
|
+
|
|
276
|
+
**Accessibility:**
|
|
277
|
+
- **Hover previews, it never toggles.** Hovering highlights the card and the track; only a click, tap, Space or Enter changes the state — a setting that flips as the pointer passes over it fails WCAG 3.2.1/3.2.2 and doesn't exist on touch.
|
|
278
|
+
- The card is a click target, not a second control: the `<button role="switch">` inside is the only focusable element, so AT hears one switch with the Toggle's name and description. The Toggle handles clicks on the switch and its text block (`.hsui-toggle__text`); the card forwards only the rest — its padding — so a description click never double-toggles.
|
|
279
|
+
- The focus ring moves from the pill to the card edge (`:has(:focus-visible)`), since the card is what you click.
|
|
280
|
+
|
|
281
|
+
### ToggleList
|
|
282
|
+
A settings list: `ToggleCard` rows in one frame with hairline dividers, each a whole-row click target.
|
|
283
|
+
```tsx
|
|
284
|
+
<ToggleList aria-label="Chart options">
|
|
285
|
+
<ToggleCard label="Credits" description="Show the Highcharts.com credit…" defaultChecked />
|
|
286
|
+
<ToggleCard label="Animation" description="Animate series on first render…" />
|
|
287
|
+
</ToggleList>
|
|
288
|
+
|
|
289
|
+
// Named by a visible heading
|
|
290
|
+
<Heading level={3} id="notif">Notifications</Heading>
|
|
291
|
+
<ToggleList aria-labelledby="notif">…</ToggleList>
|
|
292
|
+
|
|
293
|
+
// List-wide defaults; a row's own prop wins
|
|
294
|
+
<ToggleList aria-label="Site status" size={300} transparent>
|
|
295
|
+
<ToggleCard label="Public search" />
|
|
296
|
+
<ToggleCard variant="danger" label="Maintenance mode" />
|
|
297
|
+
</ToggleList>
|
|
298
|
+
```
|
|
299
|
+
- **Rows are `ToggleCard`s** — no separate row component. Inside a list a card picks up `size` / `variant` / `transparent` from the list (via `ToggleListContext`, in `ToggleList/context.ts` so ToggleCard can import it without a cycle) and its own prop overrides.
|
|
300
|
+
- **Frame:** 1px border-200, 8px corners (like `FormGroup`), border-200 hairlines between rows; `transparent` uses the transparent Button's background + stroke. The list clips rows to its corners.
|
|
301
|
+
- **Rows in a list differ from standalone cards:** no own stroke or corners; hover is a `--HS-surface-ui-background-50` wash (`transparent`: the transparent Button hover) rather than the 2px stroke, which would jitter between stacked rows; **no on-state tint** — the switch carries the state, a mostly-on list would otherwise become one tinted block, and a checked row's hover would vanish under its own tint; disabled rows stay on the list surface.
|
|
302
|
+
|
|
303
|
+
**Accessibility:**
|
|
304
|
+
- The list is a `role="group"` — always name it with `aria-label` or `aria-labelledby`, so AT announces "Chart options, group" before the switches. Each row is still one `role="switch"` named by its label and described by its description.
|
|
305
|
+
- Every row is a whole-row click target (padding, description, label, switch), forwarded to the switch with `.click()`; links in a description navigate without toggling; disabled rows ignore clicks and leave the tab order.
|
|
306
|
+
- Keyboard focus draws a 2px inset brand ring around the row — inset because the list clips anything outside it. Hover is pointer-only polish (WCAG doesn't require it); nothing depends on it.
|
|
307
|
+
|
|
225
308
|
### Heading
|
|
226
309
|
```tsx
|
|
227
310
|
<Heading level={1}>Page title</Heading>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as e } from "./clsx-BWCAbrjV.js";
|
|
3
|
+
import { forwardRef as t, useCallback as n, useId as r, useRef as i, useState as a } from "react";
|
|
4
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
5
|
+
var c = {
|
|
6
|
+
attention: "JOLtuG_attention",
|
|
7
|
+
brand: "JOLtuG_brand",
|
|
8
|
+
checked: "JOLtuG_checked",
|
|
9
|
+
danger: "JOLtuG_danger",
|
|
10
|
+
description: "JOLtuG_description",
|
|
11
|
+
disabled: "JOLtuG_disabled",
|
|
12
|
+
full: "JOLtuG_full",
|
|
13
|
+
label: "JOLtuG_label",
|
|
14
|
+
labelLeft: "JOLtuG_labelLeft",
|
|
15
|
+
"size-100": "JOLtuG_size-100",
|
|
16
|
+
"size-200": "JOLtuG_size-200",
|
|
17
|
+
"size-300": "JOLtuG_size-300",
|
|
18
|
+
"size-400": "JOLtuG_size-400",
|
|
19
|
+
"size-50": "JOLtuG_size-50",
|
|
20
|
+
"size-500": "JOLtuG_size-500",
|
|
21
|
+
success: "JOLtuG_success",
|
|
22
|
+
text: "JOLtuG_text",
|
|
23
|
+
thumb: "JOLtuG_thumb",
|
|
24
|
+
toggle: "JOLtuG_toggle",
|
|
25
|
+
track: "JOLtuG_track",
|
|
26
|
+
withDescription: "JOLtuG_withDescription"
|
|
27
|
+
}, l = t(function({ label: t, description: l, checked: u, defaultChecked: d = !1, onChange: f, size: p = 200, variant: m = "brand", labelPosition: h = "right", fullWidth: g = !1, name: _, value: v = "on", disabled: y = !1, id: b, className: x, style: S, onClick: C, "aria-describedby": w, ...T }, E) {
|
|
28
|
+
let D = i(null), O = n((e) => {
|
|
29
|
+
D.current = e, typeof E == "function" ? E(e) : E && (E.current = e);
|
|
30
|
+
}, [E]), k = r(), A = b ?? `hsui-toggle-${k}`, j = `${A}-label`, M = `${A}-description`, [N, P] = a(d), F = u !== void 0, I = F ? u : N, L = (e) => {
|
|
31
|
+
if (C?.(e), e.defaultPrevented || y) return;
|
|
32
|
+
let t = !I;
|
|
33
|
+
F || P(t), f?.(t);
|
|
34
|
+
}, R = (e) => {
|
|
35
|
+
let t = D.current;
|
|
36
|
+
y || !t || e.target.closest("label, a, button, input, select, textarea") || t.click();
|
|
37
|
+
}, z = [l == null ? null : M, w].filter(Boolean).join(" ") || void 0, B = t != null || l != null;
|
|
38
|
+
return /* @__PURE__ */ s("span", {
|
|
39
|
+
className: e("hsui-toggle", c.toggle, c[`size-${p}`], c[m], {
|
|
40
|
+
[c.checked]: I,
|
|
41
|
+
[c.disabled]: y,
|
|
42
|
+
[c.labelLeft]: h === "left",
|
|
43
|
+
[c.full]: g,
|
|
44
|
+
[c.withDescription]: l != null
|
|
45
|
+
}, x),
|
|
46
|
+
style: S,
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ o("button", {
|
|
49
|
+
ref: O,
|
|
50
|
+
id: A,
|
|
51
|
+
type: "button",
|
|
52
|
+
role: "switch",
|
|
53
|
+
"aria-checked": I,
|
|
54
|
+
"aria-labelledby": t == null ? void 0 : j,
|
|
55
|
+
"aria-describedby": z,
|
|
56
|
+
disabled: y,
|
|
57
|
+
className: e("hsui-toggle__track", c.track),
|
|
58
|
+
onClick: L,
|
|
59
|
+
...T,
|
|
60
|
+
children: /* @__PURE__ */ o("span", {
|
|
61
|
+
className: c.thumb,
|
|
62
|
+
"aria-hidden": "true"
|
|
63
|
+
})
|
|
64
|
+
}),
|
|
65
|
+
B && /* @__PURE__ */ s("span", {
|
|
66
|
+
className: e("hsui-toggle__text", c.text),
|
|
67
|
+
onClick: R,
|
|
68
|
+
children: [t != null && /* @__PURE__ */ o("label", {
|
|
69
|
+
id: j,
|
|
70
|
+
htmlFor: A,
|
|
71
|
+
className: c.label,
|
|
72
|
+
children: t
|
|
73
|
+
}), l != null && /* @__PURE__ */ o("span", {
|
|
74
|
+
id: M,
|
|
75
|
+
className: c.description,
|
|
76
|
+
children: l
|
|
77
|
+
})]
|
|
78
|
+
}),
|
|
79
|
+
_ && I && !y && /* @__PURE__ */ o("input", {
|
|
80
|
+
type: "hidden",
|
|
81
|
+
name: _,
|
|
82
|
+
value: v
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
export { l as t };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as e } from "./clsx-BWCAbrjV.js";
|
|
3
|
+
import { t } from "./Toggle-BHjptO7-.js";
|
|
4
|
+
import { useToggleList as n } from "./components/ToggleList/context.js";
|
|
5
|
+
import { forwardRef as r, useCallback as i, useRef as a, useState as o } from "react";
|
|
6
|
+
import { jsx as s } from "react/jsx-runtime";
|
|
7
|
+
var c = {
|
|
8
|
+
attention: "AlVaOW_attention",
|
|
9
|
+
brand: "AlVaOW_brand",
|
|
10
|
+
card: "AlVaOW_card",
|
|
11
|
+
checked: "AlVaOW_checked",
|
|
12
|
+
danger: "AlVaOW_danger",
|
|
13
|
+
disabled: "AlVaOW_disabled",
|
|
14
|
+
inList: "AlVaOW_inList",
|
|
15
|
+
"size-100": "AlVaOW_size-100",
|
|
16
|
+
"size-200": "AlVaOW_size-200",
|
|
17
|
+
"size-300": "AlVaOW_size-300",
|
|
18
|
+
"size-400": "AlVaOW_size-400",
|
|
19
|
+
"size-50": "AlVaOW_size-50",
|
|
20
|
+
"size-500": "AlVaOW_size-500",
|
|
21
|
+
success: "AlVaOW_success",
|
|
22
|
+
transparent: "AlVaOW_transparent"
|
|
23
|
+
}, l = r(function({ checked: r, defaultChecked: l = !1, onChange: u, size: d, labelPosition: f = "left", disabled: p = !1, variant: m, transparent: h, className: g, style: _, ...v }, y) {
|
|
24
|
+
let b = n(), x = b !== null, S = d ?? b?.size ?? 200, C = m ?? b?.variant ?? "brand", w = h ?? b?.transparent ?? !1, [T, E] = o(l), D = r !== void 0, O = D ? r : T, k = i((e) => {
|
|
25
|
+
D || E(e), u?.(e);
|
|
26
|
+
}, [D, u]), A = a(null), j = i((e) => {
|
|
27
|
+
A.current = e, typeof y == "function" ? y(e) : y && (y.current = e);
|
|
28
|
+
}, [y]), M = (e) => {
|
|
29
|
+
let t = A.current;
|
|
30
|
+
if (p || !t) return;
|
|
31
|
+
let n = e.target;
|
|
32
|
+
t.contains(n) || n.closest(".hsui-toggle__text") || t.click();
|
|
33
|
+
};
|
|
34
|
+
return /* @__PURE__ */ s("div", {
|
|
35
|
+
className: e("hsui-toggle-card", c.card, c[`size-${S}`], {
|
|
36
|
+
[c.checked]: O,
|
|
37
|
+
[c.disabled]: p,
|
|
38
|
+
[c.transparent]: w,
|
|
39
|
+
[c.inList]: x
|
|
40
|
+
}, c[C], g),
|
|
41
|
+
style: _,
|
|
42
|
+
onClick: M,
|
|
43
|
+
children: /* @__PURE__ */ s(t, {
|
|
44
|
+
...v,
|
|
45
|
+
ref: j,
|
|
46
|
+
checked: O,
|
|
47
|
+
onChange: k,
|
|
48
|
+
size: S,
|
|
49
|
+
labelPosition: f,
|
|
50
|
+
disabled: p,
|
|
51
|
+
variant: C,
|
|
52
|
+
fullWidth: !0
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export { l as t };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as e } from "./clsx-BWCAbrjV.js";
|
|
3
|
+
import { ToggleListContext as t } from "./components/ToggleList/context.js";
|
|
4
|
+
import { forwardRef as n, useMemo as r } from "react";
|
|
5
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
6
|
+
var a = {
|
|
7
|
+
list: "IfOx9W_list",
|
|
8
|
+
transparent: "IfOx9W_transparent"
|
|
9
|
+
}, o = n(function({ children: n, size: o, variant: s, transparent: c = !1, className: l, ...u }, d) {
|
|
10
|
+
let f = r(() => ({
|
|
11
|
+
size: o,
|
|
12
|
+
variant: s,
|
|
13
|
+
transparent: c
|
|
14
|
+
}), [
|
|
15
|
+
o,
|
|
16
|
+
s,
|
|
17
|
+
c
|
|
18
|
+
]);
|
|
19
|
+
return /* @__PURE__ */ i(t.Provider, {
|
|
20
|
+
value: f,
|
|
21
|
+
children: /* @__PURE__ */ i("div", {
|
|
22
|
+
ref: d,
|
|
23
|
+
role: "group",
|
|
24
|
+
className: e("hsui-toggle-list", a.list, { [a.transparent]: c }, l),
|
|
25
|
+
...u,
|
|
26
|
+
children: n
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
export { o as t };
|