@reliability-design/web 1.4.2 → 1.4.4
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 +27 -0
- package/dist/components/rel-autocomplete/README.md +4 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +80 -3
- package/dist/components/rel-button/rel-button.ts +2 -2
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +116 -9
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-radio/README.md +16 -1
- package/dist/components/rel-radio/rel-radio.ts +52 -1
- package/dist/components/rel-range-picker/rel-range-picker.ts +117 -2
- package/dist/components/rel-select/README.md +7 -0
- package/dist/components/rel-select/rel-select.ts +1237 -1119
- package/dist/components/rel-textfield/README.md +16 -0
- package/dist/components/rel-textfield/rel-textfield.ts +104 -6
- package/dist/components/rel-time-picker/README.md +199 -0
- package/dist/components/rel-time-picker/rel-time-picker.ts +994 -0
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +453 -39
- package/dist/index.d.ts +453 -39
- package/dist/index.js +2659 -339
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2663 -346
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog — @reliability-design/web
|
|
2
2
|
|
|
3
|
+
## 1.4.3 (2026-07-30)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **New `rel-time-picker` component.** A form-field trigger button opening an Hour / Min / AM-PM column popup. Follows the range-picker draft model: column clicks stage a draft, **Apply** commits (emitting `rel-change`), Cancel/`Escape`/outside click discards, and a **Now** button snaps the draft to the current time. Supports `format` (`12h` | `24h` — the committed `value` is always 24-hour `"HH:mm"`), `minute-step` intervals, inclusive `min-time`/`max-time` bounds, the standard field API (`label`, `placeholder`, `helper-text`, `error`/`invalid` + `error-message`, `required`, `readonly`, `disabled`, `clearable`, `size`, `fullWidth`, hidden form input via `name`), `::part` hooks (base, label, trigger, value, icon, panel, option, footer, helper), and full keyboard/ARIA support. The Angular package ships a matching ControlValueAccessor wrapper for reactive and template-driven forms.
|
|
8
|
+
- **`label-style` prop (`default` | `floating` | `none`) on `rel-textfield`, `rel-select`, and `rel-autocomplete`.** `"none"` renders no visible label; supply `field-aria-label` or `field-aria-labelledby` to keep the control's accessible name. The old `floatingLabel` boolean remains as a deprecated alias for `label-style="floating"`.
|
|
9
|
+
- **`orientation` prop on `rel-radio` (`vertical` | `horizontal`).** Horizontal lays options out in a wrapping row; the group now renders `role="radiogroup"` with `aria-orientation`.
|
|
10
|
+
- **`size` prop on `rel-radio` (`small` | `medium`).** Small drops the circle from 20px to 16px and steps the label/supporting typography down a size for denser forms.
|
|
11
|
+
|
|
12
|
+
## 1.4.2 (2026-07-29)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **New `rel-autocomplete` component.** A searchable single-select type-ahead built on `rel-textfield`, so every field feature (label, sizes, variants, floating label, icons, error display) carries over. Supports local items with `contains`/`starts-with`/`none` filtering or a remote `searchFn` (debounced, latest-query-wins with stale responses discarded, automatic loading row), per-item icons, bolded match highlighting, `clearable`, a configurable no-results row, full keyboard navigation, and combobox ARIA. Ships with an Angular ControlValueAccessor wrapper.
|
|
17
|
+
- Per-option leading icons in the `rel-select` menu — options accept an `icon` field (`rel-icon` name).
|
|
18
|
+
- `rel-select` multi-select chips no longer wrap to a second line — the visible chip count adapts to the field width, collapsing the rest into the "+N" chip; `max-visible-chips` is now an optional extra cap (default uncapped, was 3).
|
|
19
|
+
- `icon` prop on `rel-checkbox` — a leading `rel-icon` between the checkbox and label.
|
|
20
|
+
- `focus()` method on `rel-textfield` that forwards to the inner input (the host itself is not focusable).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Disabled `rel-button` styling — disabled buttons dim to 0.5 opacity, and only fill styles (primary, default, icon) flatten to the disabled fill; outline, ghost, link, and static keep their own shape.
|
|
25
|
+
- `rel-popover` rendering misaligned when anchored inside a centered `rel-modal`.
|
|
26
|
+
- Removed the native title-attribute tooltip on hover over a `rel-modal` title.
|
|
27
|
+
- Tab-focusing a below-fold `rel-checkbox`/`rel-radio` no longer scrolls the wrong container (which shoved the page past fixed shells and broke the layout).
|
|
28
|
+
- Closed `rel-drawer`/`rel-modal` panels are removed from the Tab order — keyboard focus no longer disappears into invisible close buttons or slotted content.
|
|
29
|
+
|
|
3
30
|
## 1.4.1 (2026-07-27)
|
|
4
31
|
|
|
5
32
|
### Fixed
|
|
@@ -59,7 +59,8 @@ Two data modes:
|
|
|
59
59
|
| `loading` | `boolean` | `false` | Externally-controlled loading row (internal `searchFn` loading is automatic) |
|
|
60
60
|
| `no-results-text` | `string` | `"No results found"` | Row shown when a query yields nothing |
|
|
61
61
|
| `loading-text` | `string` | `"Loading…"` | Text next to the loading spinner |
|
|
62
|
-
| `label`, `placeholder`, `size`, `variant`, `floating-label`, `prefix-icon`, `disabled`, `readonly`, `required`, `error`, `error-message`, `helper-text`, `name`, `full-width` | — | — | Passed through to the inner `rel-textfield` unchanged |
|
|
62
|
+
| `label`, `placeholder`, `size`, `variant`, `label-style`, `floating-label`, `field-aria-label`, `field-aria-labelledby`, `prefix-icon`, `disabled`, `readonly`, `required`, `error`, `error-message`, `helper-text`, `name`, `full-width` | — | — | Passed through to the inner `rel-textfield` unchanged |
|
|
63
|
+
| `label-style` | `"default" \| "floating" \| "none"` | — | Label presentation. `"none"` renders no label — pair it with `field-aria-label`. Unset, the deprecated `floating-label` boolean decides |
|
|
63
64
|
|
|
64
65
|
## Events
|
|
65
66
|
|
|
@@ -83,6 +84,7 @@ Two data modes:
|
|
|
83
84
|
- Combobox pattern: `role="combobox"` with `aria-expanded`, `aria-controls`, and `aria-activedescendant`; panel is `role="listbox"` with `role="option"` rows
|
|
84
85
|
- Known limitation: the combobox role sits on a wrapper around the field rather than on the `<input>` itself — the input lives in `rel-textfield`'s nested shadow root, which `aria-activedescendant` cannot reference across. This is the standard pragmatic pattern for composed shadow components
|
|
85
86
|
- Loading row uses `role="status"`; disabled/readonly never open the panel
|
|
87
|
+
- With `label-style="none"` no `<label>` renders, so `field-aria-label` (or `field-aria-labelledby`) must supply the accessible name — it also names the suggestion panel. A placeholder is not a valid naming mechanism; the inner field logs a console warning when both are missing
|
|
86
88
|
|
|
87
89
|
## Notes / Edge Cases
|
|
88
90
|
|
|
@@ -92,3 +94,4 @@ Two data modes:
|
|
|
92
94
|
- Form validation is *displayed* (`error`, `error-message`, `required`), not performed — validity logic belongs to the form layer (e.g. Angular validators)
|
|
93
95
|
- CSS parts: `panel`, `list`, `option`, plus re-exported textfield parts (`label`, `control`, `input`, `helper`, `right-icon`)
|
|
94
96
|
- Panel styling matches `rel-select`'s listbox; override via `--rel-autocomplete-bg`, `--rel-autocomplete-menu-border`, `--rel-autocomplete-menu-radius`, `--rel-autocomplete-menu-shadow`, `--rel-autocomplete-option-hover`
|
|
97
|
+
- The open panel is promoted to the browser **top layer** (native Popover API, `popover="manual"`), so it renders correctly inside transformed / `overflow: hidden` containers — a centered `rel-modal`, a bottom `rel-drawer`. Engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction for transformed ancestors
|
|
@@ -3,7 +3,12 @@ import { customElement, property, state, query } from "lit/decorators.js";
|
|
|
3
3
|
import { repeat } from "lit/directives/repeat.js";
|
|
4
4
|
import "@reliability-design/icons/rel-icon";
|
|
5
5
|
import "../rel-textfield/rel-textfield.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
RelTextfield,
|
|
8
|
+
TextFieldSize,
|
|
9
|
+
TextFieldVariant,
|
|
10
|
+
FieldLabelStyle,
|
|
11
|
+
} from "../rel-textfield/rel-textfield.js";
|
|
7
12
|
|
|
8
13
|
/** An item is a plain string or any object read through displayField/valueField. */
|
|
9
14
|
export type AutocompleteItem = string | Record<string, unknown>;
|
|
@@ -76,7 +81,15 @@ export class RelAutocomplete extends LitElement {
|
|
|
76
81
|
@property({ type: String }) placeholder: string = "";
|
|
77
82
|
@property({ type: String }) size: TextFieldSize = "medium";
|
|
78
83
|
@property({ type: String }) variant: TextFieldVariant = "default";
|
|
84
|
+
/** See `FieldLabelStyle`. Left `undefined` the deprecated `floatingLabel` decides. */
|
|
85
|
+
@property({ type: String, attribute: "label-style", reflect: true })
|
|
86
|
+
labelStyle?: FieldLabelStyle;
|
|
87
|
+
/** @deprecated Use `labelStyle="floating"`. Kept permanently as an alias. */
|
|
79
88
|
@property({ type: Boolean, attribute: "floating-label" }) floatingLabel = false;
|
|
89
|
+
/** Accessible name for the input. Required when `labelStyle="none"`. */
|
|
90
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
91
|
+
/** IDREF list of external element(s) naming the input. Wins over `fieldAriaLabel`. */
|
|
92
|
+
@property({ type: String, attribute: "field-aria-labelledby" }) fieldAriaLabelledby?: string;
|
|
80
93
|
@property({ type: String, attribute: "prefix-icon" }) prefixIcon?: string;
|
|
81
94
|
@property({ type: Boolean }) disabled = false;
|
|
82
95
|
@property({ type: Boolean }) readonly = false;
|
|
@@ -105,6 +118,12 @@ export class RelAutocomplete extends LitElement {
|
|
|
105
118
|
* doesn't overwrite the input text the way it does for external writes. */
|
|
106
119
|
private internalValueWrite = false;
|
|
107
120
|
|
|
121
|
+
/** Name used for the suggestion panel — the visible label, or fieldAriaLabel
|
|
122
|
+
* once labelStyle="none" hides it. Mirrors rel-textfield's own resolution. */
|
|
123
|
+
private get accessibleName() {
|
|
124
|
+
return (this.labelStyle === "none" ? this.fieldAriaLabel : this.label) || "";
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
static styles = css`
|
|
109
128
|
:host {
|
|
110
129
|
color: var(--rel-text-default);
|
|
@@ -124,6 +143,15 @@ export class RelAutocomplete extends LitElement {
|
|
|
124
143
|
position: fixed;
|
|
125
144
|
z-index: 1001;
|
|
126
145
|
display: none;
|
|
146
|
+
/* Reset native [popover] UA styles (the panel is promoted to the
|
|
147
|
+
top layer so transformed ancestors — e.g. a centered rel-modal
|
|
148
|
+
panel — can't hijack its containing block or clip it). */
|
|
149
|
+
inset: auto;
|
|
150
|
+
margin: 0;
|
|
151
|
+
padding: 0;
|
|
152
|
+
width: auto;
|
|
153
|
+
height: auto;
|
|
154
|
+
color: inherit;
|
|
127
155
|
box-sizing: border-box;
|
|
128
156
|
background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
|
|
129
157
|
border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
|
|
@@ -203,6 +231,7 @@ export class RelAutocomplete extends LitElement {
|
|
|
203
231
|
disconnectedCallback() {
|
|
204
232
|
super.disconnectedCallback();
|
|
205
233
|
this.removeGlobalListeners();
|
|
234
|
+
this.hideNativePopover();
|
|
206
235
|
clearTimeout(this.debounceTimer);
|
|
207
236
|
}
|
|
208
237
|
|
|
@@ -290,7 +319,10 @@ export class RelAutocomplete extends LitElement {
|
|
|
290
319
|
this.open = true;
|
|
291
320
|
this.highlightedIndex = -1;
|
|
292
321
|
this.addGlobalListeners();
|
|
293
|
-
requestAnimationFrame(() =>
|
|
322
|
+
requestAnimationFrame(() => {
|
|
323
|
+
this.showNativePopover();
|
|
324
|
+
this.updatePosition();
|
|
325
|
+
});
|
|
294
326
|
}
|
|
295
327
|
|
|
296
328
|
private closePanel() {
|
|
@@ -298,6 +330,35 @@ export class RelAutocomplete extends LitElement {
|
|
|
298
330
|
this.open = false;
|
|
299
331
|
this.highlightedIndex = -1;
|
|
300
332
|
this.removeGlobalListeners();
|
|
333
|
+
this.hideNativePopover();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* A transformed ancestor (e.g. rel-modal's centered panel with
|
|
337
|
+
* translate(-50%,-50%)) becomes the containing block for position:fixed
|
|
338
|
+
* descendants, breaking viewport-based coordinates and letting the
|
|
339
|
+
* panel's overflow:hidden clip the suggestions. Promoting the panel to
|
|
340
|
+
* the browser top layer sidesteps both — same fix as rel-popover.
|
|
341
|
+
* `manual` keeps light-dismiss and Escape handling under this
|
|
342
|
+
* component's own control. */
|
|
343
|
+
|
|
344
|
+
private showNativePopover() {
|
|
345
|
+
const p = this.panelEl;
|
|
346
|
+
if (!p || typeof p.showPopover !== "function") return;
|
|
347
|
+
try {
|
|
348
|
+
if (!p.matches(":popover-open")) p.showPopover();
|
|
349
|
+
} catch {
|
|
350
|
+
/* older engines / detached element — fixed-position fallback applies */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private hideNativePopover() {
|
|
355
|
+
const p = this.panelEl;
|
|
356
|
+
if (!p || typeof p.hidePopover !== "function") return;
|
|
357
|
+
try {
|
|
358
|
+
if (p.matches(":popover-open")) p.hidePopover();
|
|
359
|
+
} catch {
|
|
360
|
+
/* already hidden or unsupported — nothing to do */
|
|
361
|
+
}
|
|
301
362
|
}
|
|
302
363
|
|
|
303
364
|
private updatePosition = () => {
|
|
@@ -318,6 +379,18 @@ export class RelAutocomplete extends LitElement {
|
|
|
318
379
|
left = Math.max(8, window.innerWidth - t.width - 8);
|
|
319
380
|
}
|
|
320
381
|
|
|
382
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
383
|
+
// block for position:fixed, shifting the panel away from the viewport
|
|
384
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
385
|
+
this.panelEl.style.top = `${top}px`;
|
|
386
|
+
this.panelEl.style.left = `${left}px`;
|
|
387
|
+
this.panelEl.style.width = `${t.width}px`;
|
|
388
|
+
const applied = this.panelEl.getBoundingClientRect();
|
|
389
|
+
top -= applied.top - top;
|
|
390
|
+
left -= applied.left - left;
|
|
391
|
+
this.panelEl.style.top = `${top}px`;
|
|
392
|
+
this.panelEl.style.left = `${left}px`;
|
|
393
|
+
|
|
321
394
|
this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
|
|
322
395
|
};
|
|
323
396
|
|
|
@@ -540,7 +613,10 @@ export class RelAutocomplete extends LitElement {
|
|
|
540
613
|
.value="${this.inputText}"
|
|
541
614
|
.size="${this.size}"
|
|
542
615
|
.variant="${this.variant}"
|
|
616
|
+
.labelStyle="${this.labelStyle}"
|
|
543
617
|
.floatingLabel="${this.floatingLabel}"
|
|
618
|
+
.fieldAriaLabel="${this.fieldAriaLabel}"
|
|
619
|
+
.fieldAriaLabelledby="${this.fieldAriaLabelledby}"
|
|
544
620
|
.prefixIcon="${this.prefixIcon}"
|
|
545
621
|
.disabled="${this.disabled}"
|
|
546
622
|
.readonly="${this.readonly}"
|
|
@@ -561,8 +637,9 @@ export class RelAutocomplete extends LitElement {
|
|
|
561
637
|
id="${panelId}"
|
|
562
638
|
part="panel list"
|
|
563
639
|
class="ac-panel"
|
|
640
|
+
popover="manual"
|
|
564
641
|
role="listbox"
|
|
565
|
-
aria-label="${this.
|
|
642
|
+
aria-label="${this.accessibleName || "Suggestions"}"
|
|
566
643
|
style="${this.panelStyle}"
|
|
567
644
|
?data-open="${this.open}"
|
|
568
645
|
>
|
|
@@ -191,8 +191,8 @@ export class RelButton extends LitElement {
|
|
|
191
191
|
pointer-events:none on the HOST would make it transparent to the
|
|
192
192
|
pointer and suppress the cursor entirely. */
|
|
193
193
|
cursor: not-allowed;
|
|
194
|
-
/* The disabled tokens are full-contrast pairs (light:
|
|
195
|
-
|
|
194
|
+
/* The disabled tokens are full-contrast pairs (light: near-black on
|
|
195
|
+
gray-300, dark: white on gray-700), so on their own they render a
|
|
196
196
|
crisp button that reads as enabled. Dulling happens here instead —
|
|
197
197
|
same opacity mechanism the rest of the library uses. */
|
|
198
198
|
opacity: 0.5;
|
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
# rel-checkbox
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label
|
|
4
|
+
A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label, optional supporting text, and optional label prefix icon, with built-in error and disabled states. Supports two sizes, two densities, and leading/trailing box alignment — matching the Figma Checkbox sheet vocabulary (Size, Density, Alignment) verbatim.
|
|
5
5
|
|
|
6
6
|
## Usage
|
|
7
7
|
```html
|
|
8
8
|
<!-- Basic example -->
|
|
9
9
|
<rel-checkbox label="Accept terms"></rel-checkbox>
|
|
10
10
|
|
|
11
|
-
<!--
|
|
11
|
+
<!-- Supporting text, compact density (default: text on its own line, indented past the box) -->
|
|
12
12
|
<rel-checkbox
|
|
13
13
|
label="Subscribe to newsletter"
|
|
14
14
|
supportingtext="We'll send you weekly updates"
|
|
15
15
|
status="unchecked"
|
|
16
16
|
state="default">
|
|
17
17
|
</rel-checkbox>
|
|
18
|
+
|
|
19
|
+
<!-- Comfortable density: label + supporting text stack beside the box -->
|
|
20
|
+
<rel-checkbox
|
|
21
|
+
density="comfortable"
|
|
22
|
+
label="Notify the broker"
|
|
23
|
+
supportingtext="Sends an email when the POD is uploaded">
|
|
24
|
+
</rel-checkbox>
|
|
25
|
+
|
|
26
|
+
<!-- Trailing alignment: box at the far edge of the row (settings-row style).
|
|
27
|
+
Trailing always renders comfortable density — compact has no trailing layout. -->
|
|
28
|
+
<rel-checkbox
|
|
29
|
+
alignment="trailing"
|
|
30
|
+
label="Email notifications"
|
|
31
|
+
supportingtext="Weekly summary of load activity">
|
|
32
|
+
</rel-checkbox>
|
|
33
|
+
|
|
34
|
+
<!-- Small size -->
|
|
35
|
+
<rel-checkbox size="small" label="Include archived"></rel-checkbox>
|
|
36
|
+
|
|
37
|
+
<!-- Indeterminate (programmatic-only; user toggle resolves to checked/unchecked) -->
|
|
38
|
+
<rel-checkbox label="Select all" status="indeterminate"></rel-checkbox>
|
|
18
39
|
```
|
|
19
40
|
|
|
20
41
|
## API
|
|
@@ -23,42 +44,62 @@ A tri-state checkbox component supporting checked, unchecked, and indeterminate
|
|
|
23
44
|
| Name | Type | Default | Description |
|
|
24
45
|
|------|------|---------|-------------|
|
|
25
46
|
| `label` | `string` | `"Label"` | Label text displayed next to the checkbox |
|
|
26
|
-
| `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon). Unset renders no icon and reserves no space |
|
|
27
|
-
| `supportingtext` | `string` | `""` | Helper text
|
|
47
|
+
| `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon; scales with `size`). Unset renders no icon and reserves no space |
|
|
48
|
+
| `supportingtext` | `string` | `""` | Helper text; placement depends on `density` |
|
|
28
49
|
| `state` | `"default" \| "error" \| "disabled"` | `"default"` | Visual and interaction state of the checkbox |
|
|
29
|
-
| `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current
|
|
50
|
+
| `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current check state. Indeterminate is programmatic-only |
|
|
51
|
+
| `checked` | `boolean` (accessor) | `false` | Convenience accessor over `status` (used by the Angular CVA) |
|
|
52
|
+
| `size` | `"small" \| "medium"` | `"medium"` | Control scale: medium = 18px box, body-lg label, body-sm supporting text; small = 14px box, body-md label, body-xs supporting text (reflected) |
|
|
53
|
+
| `density` | `"compact" \| "comfortable"` | `"compact"` | Supporting-text layout: compact puts it on its own line under the row, indented past the box; comfortable stacks label + supporting text beside the box (reflected) |
|
|
54
|
+
| `alignment` | `"leading" \| "trailing"` | `"leading"` | Box position: leading = before the label; trailing = far edge of the row. Trailing always renders comfortable density — compact+trailing is "Not available" in the Figma sheet, and a dev-mode `console.warn` fires if requested (reflected) |
|
|
55
|
+
| `value` | `string` | `""` | Value reported in the `rel-change` event detail |
|
|
56
|
+
|
|
57
|
+
### Layout matrix (mirrors the Figma sheet)
|
|
58
|
+
| Size | Density | Leading | Trailing |
|
|
59
|
+
|------|---------|---------|----------|
|
|
60
|
+
| M | Comfortable | ✓ | ✓ |
|
|
61
|
+
| M | Compact | ✓ (default) | — renders comfortable |
|
|
62
|
+
| S | Comfortable | ✓ | ✓ |
|
|
63
|
+
| S | Compact | ✓ | — renders comfortable |
|
|
30
64
|
|
|
31
65
|
## Events
|
|
32
66
|
| Event | Detail | Description |
|
|
33
67
|
|-------|--------|-------------|
|
|
34
|
-
| `change` | `{ checked: boolean,
|
|
68
|
+
| `rel-change` | `{ checked: boolean, value: string }` | Fired on user toggle (bubbles, composed) |
|
|
35
69
|
|
|
36
70
|
## Slots
|
|
37
71
|
Default slot not used.
|
|
38
72
|
|
|
39
73
|
## CSS Parts
|
|
40
|
-
|
|
74
|
+
- `base` — outer wrapper
|
|
75
|
+
- `label` — label text span
|
|
76
|
+
- `helper` — supporting text span
|
|
41
77
|
|
|
42
78
|
## Tokens / CSS Variables
|
|
43
79
|
|
|
44
80
|
### Semantic Tokens
|
|
45
81
|
- `--rel-fonts-font-family` - Font family
|
|
46
|
-
- `--rel-body-
|
|
47
|
-
- `--rel-
|
|
82
|
+
- `--rel-body-lg-regular-*` / `--rel-body-sm-regular-*` - Label and supporting text type (medium size)
|
|
83
|
+
- `--rel-body-md-regular-*` / `--rel-body-xs-regular-*` - Label and supporting text type (small size)
|
|
84
|
+
|
|
85
|
+
### Spacing Tokens
|
|
86
|
+
- `--rel-spacing-xs` - Gap between box, icon, and label
|
|
87
|
+
- `--rel-spacing-3xs` - Gap between label and supporting text (comfortable) / row and supporting text (compact)
|
|
48
88
|
|
|
49
89
|
### Color Tokens
|
|
50
|
-
- `--rel-text-
|
|
51
|
-
- `--rel-
|
|
52
|
-
- `--rel-
|
|
53
|
-
- `--rel-text-
|
|
54
|
-
- `--rel-
|
|
55
|
-
- `--rel-border-
|
|
90
|
+
- `--rel-text-default` - Box border and label text color (rest)
|
|
91
|
+
- `--rel-text-body` - Hover color for box, label, and icon
|
|
92
|
+
- `--rel-bg-neutral-inverse-2` - Checked box fill and indeterminate dash
|
|
93
|
+
- `--rel-text-inverse` - Checkmark color
|
|
94
|
+
- `--rel-text-caption` - Supporting text color
|
|
95
|
+
- `--rel-border-focus` - Focus outline color
|
|
96
|
+
- `--rel-border-danger` / `--rel-action-danger-fill` / `--rel-text-danger` - Error state colors
|
|
56
97
|
|
|
57
98
|
## Accessibility
|
|
58
99
|
- Uses visually hidden native checkbox input for screen reader support
|
|
59
100
|
- Custom visual checkbox with proper focus indicators
|
|
60
101
|
- Focus visible state with outline offset for keyboard users
|
|
61
|
-
- Label click targets the checkbox for larger interaction area
|
|
102
|
+
- Label click targets the checkbox for larger interaction area (in trailing alignment the whole row is clickable)
|
|
62
103
|
- Disabled state communicated through opacity reduction and cursor change
|
|
63
104
|
|
|
64
105
|
### Keyboard Interactions
|
|
@@ -67,12 +108,18 @@ None exposed.
|
|
|
67
108
|
|
|
68
109
|
### Focus Behavior
|
|
69
110
|
- Focus outline appears with offset around the custom checkbox box
|
|
70
|
-
- Uses
|
|
111
|
+
- Uses focus border color for high contrast
|
|
71
112
|
|
|
72
113
|
## Notes / Edge Cases
|
|
73
|
-
-
|
|
114
|
+
- User toggling is binary: indeterminate is never entered by interaction — toggling from indeterminate resolves to checked. Set `status="indeterminate"` programmatically (e.g. a Select-All parent driven by app state)
|
|
74
115
|
- When `state="disabled"`, click events are blocked and opacity is reduced to 0.55
|
|
75
|
-
- The
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
|
|
116
|
+
- The host is `display: block`, so a trailing checkbox stretches to its container width; constrain the parent to control row width
|
|
117
|
+
- Comfortable density with no supporting text is visually identical to compact
|
|
118
|
+
- The hidden native input is absolutely positioned against the `.row` containing block; keep `position: relative` on `.row` (Tab-scroll page-jump fix, shared with rel-radio)
|
|
119
|
+
|
|
120
|
+
## Recipes (intentionally NOT component props)
|
|
121
|
+
- **Selectable list row**: `alignment="trailing"` checkbox inside a padded, hover-highlighted container
|
|
122
|
+
- **Select All / nested group**: parent checkbox with `status="indeterminate"` wired to children in app state
|
|
123
|
+
- **Selectable card**: `rel-card` wrapping a checkbox; toggle a selected border on `rel-change`
|
|
124
|
+
|
|
125
|
+
See the Storybook Checkbox stories for working recipe markup.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, type PropertyValues } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
3
|
import "@reliability-design/icons/rel-icon";
|
|
4
4
|
|
|
5
5
|
type CheckboxState = "default" | "error" | "disabled";
|
|
6
6
|
type CheckboxStatus = "unchecked" | "checked" | "indeterminate";
|
|
7
|
+
type CheckboxSize = "small" | "medium";
|
|
8
|
+
type CheckboxDensity = "compact" | "comfortable";
|
|
9
|
+
type CheckboxAlignment = "leading" | "trailing";
|
|
7
10
|
|
|
8
11
|
@customElement("rel-checkbox")
|
|
9
12
|
export class RelCheckbox extends LitElement {
|
|
@@ -15,6 +18,21 @@ export class RelCheckbox extends LitElement {
|
|
|
15
18
|
@property({ type: String }) status: CheckboxStatus = "unchecked";
|
|
16
19
|
@property({ type: String }) value = "";
|
|
17
20
|
|
|
21
|
+
/** Control scale (Figma Size). `"medium"` (default) = 18px box with body-lg
|
|
22
|
+
* label; `"small"` = 14px box with body-md label. */
|
|
23
|
+
@property({ type: String, reflect: true }) size: CheckboxSize = "medium";
|
|
24
|
+
|
|
25
|
+
/** Supporting-text layout (Figma Density). `"compact"` (default) puts the
|
|
26
|
+
* supporting text on its own line under the row, indented past the box;
|
|
27
|
+
* `"comfortable"` stacks label + supporting text beside the box. */
|
|
28
|
+
@property({ type: String, reflect: true }) density: CheckboxDensity = "compact";
|
|
29
|
+
|
|
30
|
+
/** Box position (Figma Alignment). `"leading"` (default) renders the box
|
|
31
|
+
* before the label; `"trailing"` pushes it to the far edge of the row.
|
|
32
|
+
* Trailing has no compact layout in the spec, so it always renders with
|
|
33
|
+
* comfortable density. */
|
|
34
|
+
@property({ type: String, reflect: true }) alignment: CheckboxAlignment = "leading";
|
|
35
|
+
|
|
18
36
|
get checked(): boolean {
|
|
19
37
|
return this.status === "checked";
|
|
20
38
|
}
|
|
@@ -53,6 +71,29 @@ export class RelCheckbox extends LitElement {
|
|
|
53
71
|
cursor: pointer;
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
/* Trailing alignment — box moves to the far edge, label stack to the
|
|
75
|
+
start. row-reverse keeps the box first in DOM order (input + .box
|
|
76
|
+
adjacency drives the focus ring) while space-between stretches the
|
|
77
|
+
row to its container. */
|
|
78
|
+
:host([alignment="trailing"]) .row {
|
|
79
|
+
flex-direction: row-reverse;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Comfortable density — label and supporting text stack beside the box. */
|
|
84
|
+
.content {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: var(--rel-spacing-3xs);
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.label-line {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--rel-spacing-xs);
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
input {
|
|
57
98
|
position: absolute;
|
|
58
99
|
inset: 0;
|
|
@@ -195,7 +236,6 @@ export class RelCheckbox extends LitElement {
|
|
|
195
236
|
|
|
196
237
|
.text {
|
|
197
238
|
color: var(--rel-text-caption);
|
|
198
|
-
padding-left: 28px;
|
|
199
239
|
|
|
200
240
|
font-family: var(--rel-body-sm-regular-font-family);
|
|
201
241
|
|
|
@@ -208,8 +248,63 @@ export class RelCheckbox extends LitElement {
|
|
|
208
248
|
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
209
249
|
|
|
210
250
|
}
|
|
251
|
+
|
|
252
|
+
/* Compact density — supporting text sits under the whole row, indented
|
|
253
|
+
past the box (box width + row gap, matching the pre-existing 28px). */
|
|
254
|
+
.text-below {
|
|
255
|
+
padding-left: 28px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Size variant (medium is the base scale above). */
|
|
259
|
+
:host([size="small"]) .box {
|
|
260
|
+
width: 14px;
|
|
261
|
+
height: 14px;
|
|
262
|
+
border-radius: 3px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
:host([size="small"]) svg {
|
|
266
|
+
width: 10px;
|
|
267
|
+
height: 10px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:host([size="small"]) .label {
|
|
271
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
272
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
273
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
274
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
275
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
:host([size="small"]) .text {
|
|
279
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
280
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
281
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
282
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
283
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
:host([size="small"]) .text-below {
|
|
287
|
+
padding-left: 24px;
|
|
288
|
+
}
|
|
211
289
|
`;
|
|
212
290
|
|
|
291
|
+
protected willUpdate(changed: PropertyValues<this>) {
|
|
292
|
+
if (
|
|
293
|
+
(changed.has("alignment") || changed.has("density")) &&
|
|
294
|
+
this.alignment === "trailing" &&
|
|
295
|
+
this.density === "compact"
|
|
296
|
+
) {
|
|
297
|
+
console.warn(
|
|
298
|
+
'[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Trailing has no compact layout in the spec — trailing wins. */
|
|
304
|
+
private get effectiveDensity(): CheckboxDensity {
|
|
305
|
+
return this.alignment === "trailing" ? "comfortable" : this.density;
|
|
306
|
+
}
|
|
307
|
+
|
|
213
308
|
private toggle() {
|
|
214
309
|
if (this.state === "disabled") return;
|
|
215
310
|
|
|
@@ -256,15 +351,27 @@ export class RelCheckbox extends LitElement {
|
|
|
256
351
|
</svg>
|
|
257
352
|
</span>
|
|
258
353
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
354
|
+
<span class="content">
|
|
355
|
+
<span class="label-line">
|
|
356
|
+
${this.icon
|
|
357
|
+
? html`<rel-icon
|
|
358
|
+
name="${this.icon}"
|
|
359
|
+
size="${this.size === "small" ? "sm" : "md"}"
|
|
360
|
+
class="icon-prefix"
|
|
361
|
+
></rel-icon>`
|
|
362
|
+
: null}
|
|
363
|
+
|
|
364
|
+
<span part="label" class="label">${this.label}</span>
|
|
365
|
+
</span>
|
|
366
|
+
|
|
367
|
+
${this.effectiveDensity === "comfortable" && this.supportingtext
|
|
368
|
+
? html`<span part="helper" class="text">${this.supportingtext}</span>`
|
|
369
|
+
: null}
|
|
370
|
+
</span>
|
|
264
371
|
</label>
|
|
265
372
|
|
|
266
|
-
${this.supportingtext
|
|
267
|
-
? html`<span part="helper" class="text">${this.supportingtext}</span>`
|
|
373
|
+
${this.effectiveDensity === "compact" && this.supportingtext
|
|
374
|
+
? html`<span part="helper" class="text text-below">${this.supportingtext}</span>`
|
|
268
375
|
: null}
|
|
269
376
|
</div>
|
|
270
377
|
`;
|
|
@@ -377,6 +377,20 @@ All interactions disabled:
|
|
|
377
377
|
<rel-pagination totalPages="10" page="5" disabled></rel-pagination>
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
### Very Large Page Counts
|
|
381
|
+
|
|
382
|
+
Page buttons auto-widen to fit their number (min-width 40px). Far first/last
|
|
383
|
+
buttons with numbers ≥ 10,000 are abbreviated so the control stays compact:
|
|
384
|
+
`10000` → `10k`, `368178` → `368k`, `1234567` → `1.2M`. The exact page number
|
|
385
|
+
is preserved in the button's `aria-label` and `title` (hover tooltip). The
|
|
386
|
+
current page and its immediate neighbors always render exact — abbreviating
|
|
387
|
+
them would make adjacent pages indistinguishable.
|
|
388
|
+
|
|
389
|
+
```html
|
|
390
|
+
<rel-pagination totalPages="368178" page="7"></rel-pagination>
|
|
391
|
+
<!-- [<] [1] ... [6] [7] [8] ... [368k] [>] -->
|
|
392
|
+
```
|
|
393
|
+
|
|
380
394
|
## Browser Support
|
|
381
395
|
|
|
382
396
|
Compatible with all modern browsers supporting:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
@customElement("rel-pagination")
|
|
@@ -133,9 +133,10 @@ export class RelPagination extends LitElement {
|
|
|
133
133
|
display: inline-flex;
|
|
134
134
|
align-items: center;
|
|
135
135
|
justify-content: center;
|
|
136
|
-
width:
|
|
136
|
+
width: auto;
|
|
137
|
+
min-width: 40px;
|
|
137
138
|
height: 40px;
|
|
138
|
-
padding: var(--rel-spacing-xs);
|
|
139
|
+
padding: 0 var(--rel-spacing-xs);
|
|
139
140
|
border-radius: var(--rel-border-radius-sm);
|
|
140
141
|
border: 1px solid var(--rel-border-default);
|
|
141
142
|
background: var(--rel-bg-neutral);
|
|
@@ -376,6 +377,15 @@ export class RelPagination extends LitElement {
|
|
|
376
377
|
return pages;
|
|
377
378
|
}
|
|
378
379
|
|
|
380
|
+
/** Abbreviate large page numbers (368178 → "368k") so buttons stay compact.
|
|
381
|
+
Exact below 10,000; floored to k up to 1M; then M with one decimal. */
|
|
382
|
+
private formatPageLabel(n: number): string {
|
|
383
|
+
if (n < 10_000) return String(n);
|
|
384
|
+
if (n < 1_000_000) return `${Math.floor(n / 1000)}k`;
|
|
385
|
+
const m = Math.floor(n / 100_000) / 10;
|
|
386
|
+
return `${Number.isInteger(m) ? m.toFixed(0) : m}M`;
|
|
387
|
+
}
|
|
388
|
+
|
|
379
389
|
private handlePageChange(newPage: number) {
|
|
380
390
|
const total = this.computedTotalPages;
|
|
381
391
|
if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
|
|
@@ -427,17 +437,23 @@ export class RelPagination extends LitElement {
|
|
|
427
437
|
<div class="pag-stack">
|
|
428
438
|
${this.renderNavBtn("prev")}
|
|
429
439
|
<div class="pag-numbers">
|
|
430
|
-
${pages.map((p) =>
|
|
431
|
-
p === -1
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
440
|
+
${pages.map((p) => {
|
|
441
|
+
if (p === -1) return html`<span class="pag-ellipsis">...</span>`;
|
|
442
|
+
/* Only far first/last buttons abbreviate. Anything in the
|
|
443
|
+
visible window stays exact — abbreviating window pages like
|
|
444
|
+
368174/368175 would render identical "368k" buttons. */
|
|
445
|
+
const isNear = Math.abs(p - current) <= 1;
|
|
446
|
+
const isEdge = p === 1 || p === total;
|
|
447
|
+
const label = isEdge && !isNear ? this.formatPageLabel(p) : String(p);
|
|
448
|
+
return html`<button
|
|
449
|
+
type="button"
|
|
450
|
+
class="pag-page-btn ${isNear ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
|
|
451
|
+
aria-label="Page ${p}"
|
|
452
|
+
title="${label === String(p) ? nothing : `Page ${p}`}"
|
|
453
|
+
aria-current="${p === current ? "page" : "false"}"
|
|
454
|
+
@click="${() => this.handlePageChange(p)}"
|
|
455
|
+
>${label}</button>`;
|
|
456
|
+
})}
|
|
441
457
|
</div>
|
|
442
458
|
${this.renderNavBtn("next")}
|
|
443
459
|
</div>
|