@rak200/ui 0.2.9 → 0.2.11

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/README.md CHANGED
@@ -34,6 +34,21 @@ npm install @rak200/ui
34
34
  <span slot="help">In BRL, two decimals.</span>
35
35
  </ui-field>
36
36
 
37
+ <ui-field>
38
+ <label slot="label">Currency</label>
39
+ <ui-select>
40
+ <select name="currency">
41
+ <option value="brl">Real</option>
42
+ <option value="usd">Dollar</option>
43
+ </select>
44
+ </ui-select>
45
+ </ui-field>
46
+
47
+ <ui-field>
48
+ <label slot="label">Email notifications</label>
49
+ <ui-switch><input type="checkbox" name="notify" checked /></ui-switch>
50
+ </ui-field>
51
+
37
52
  <ui-button>Save</ui-button>
38
53
  <ui-button variant="secondary">Cancel</ui-button>
39
54
  <ui-button disabled>Unavailable</ui-button>
@@ -71,8 +86,8 @@ components; `docs/` is what describes them.
71
86
 
72
87
  ## Status
73
88
 
74
- **v0.** Five components — `<ui-button>`, `<ui-field>`, `<ui-dialog>`, `<ui-input>` and
75
- `<ui-textarea>` — and the token layer under them, built to the ecosystem's full quality bar rather than sketched: type-checked at the strictest available
89
+ **v0.** Eight components — `<ui-button>`, `<ui-field>`, `<ui-dialog>`, `<ui-input>`,
90
+ `<ui-textarea>`, `<ui-checkbox>`, `<ui-switch>` and `<ui-select>` — and the token layer under them, built to the ecosystem's full quality bar rather than sketched: type-checked at the strictest available
76
91
  setting, formatted, tested in a real browser and **asserted against axe** for WCAG A/AA, 100%
77
92
  coverage and **100% mutation score**, scanned, and every public symbol documented. The v0 surface in
78
93
  RFC 0016 grows from here — see [ROADMAP.md](ROADMAP.md).
@@ -0,0 +1,76 @@
1
+ import { LitElement, type CSSResult, type TemplateResult } from 'lit';
2
+ /**
3
+ * A boolean control, drawn from the token layer rather than replaced.
4
+ *
5
+ * The `<input type="checkbox">` is the host's own and stays in the light DOM; this element
6
+ * is the box around it. {@link UiCheckbox} and {@link UiSwitch} differ in what they draw
7
+ * and in what they announce, never in what they are made of.
8
+ */
9
+ declare class UiToggle extends LitElement {
10
+ render(): TemplateResult;
11
+ }
12
+ /**
13
+ * A checkbox, styled by the token layer rather than replaced.
14
+ *
15
+ * **The `<input>` is yours.** You write it, you set its attributes, and it stays in the
16
+ * light DOM — so `name`, `checked`, `required` and `disabled` are the platform's business,
17
+ * and it reaches a form submit because it is a native control inside a `<form>`. The same
18
+ * shape `<ui-input>` has, for the same reason.
19
+ *
20
+ * **The indeterminate state is drawn, and that is not a feature being added.**
21
+ * `appearance: none` takes the platform's dash away with the rest of the drawing, so a
22
+ * control the host set `indeterminate` on would render as *unchecked* — a wrong answer
23
+ * rather than a missing one. The dash below is what stops that, and nothing here invites
24
+ * a tri-state that APG says is rare.
25
+ *
26
+ * Composes with {@link UiField}, which finds the control through this wrapper and wires
27
+ * the label, the help, the error and `aria-invalid` to it.
28
+ *
29
+ * @example
30
+ * ```html
31
+ * <ui-field>
32
+ * <label slot="label">Send a receipt</label>
33
+ * <ui-checkbox><input type="checkbox" name="receipt" /></ui-checkbox>
34
+ * </ui-field>
35
+ * ```
36
+ */
37
+ export declare class UiCheckbox extends UiToggle {
38
+ static readonly styles: CSSResult[];
39
+ }
40
+ /**
41
+ * A switch, which is a checkbox that says *on* and *off* rather than *checked*.
42
+ *
43
+ * **The difference is semantic and the drawing follows it**, which is the order that
44
+ * matters: a switch takes effect immediately and a checkbox is a value you submit, so the
45
+ * two are not one component with two skins. `role="switch"` is what carries that to a
46
+ * screen reader, and this element sets it on the slotted control rather than asking the
47
+ * host to remember — forgetting it would leave a control that looks like a switch and
48
+ * announces as a checkbox, with nothing anywhere to read. A role the host wrote is never
49
+ * overwritten, the same way {@link UiField} never overwrites an `id` it did not generate.
50
+ *
51
+ * There is no native switch to delegate to: `<input type="checkbox" switch>` is
52
+ * unsupported in the engine this suite measures, so the element is a checkbox with a role
53
+ * and a drawing. A host who wants the mixed state wants {@link UiCheckbox} — `switch` has
54
+ * no third value, so this element does not draw one.
55
+ *
56
+ * @example
57
+ * ```html
58
+ * <ui-field>
59
+ * <label slot="label">Email notifications</label>
60
+ * <ui-switch><input type="checkbox" name="notify" checked /></ui-switch>
61
+ * </ui-field>
62
+ * ```
63
+ */
64
+ export declare class UiSwitch extends UiToggle {
65
+ #private;
66
+ static readonly styles: CSSResult[];
67
+ render(): TemplateResult;
68
+ }
69
+ declare global {
70
+ interface HTMLElementTagNameMap {
71
+ 'ui-checkbox': UiCheckbox;
72
+ 'ui-switch': UiSwitch;
73
+ }
74
+ }
75
+ export {};
76
+ //# sourceMappingURL=checkbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../src/checkbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AA6IjF;;;;;;GAMG;AACH,cAAM,QAAS,SAAQ,UAAU;IACpB,MAAM,IAAI,cAAc;CAGpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,UAAW,SAAQ,QAAQ;IACpC,gBAAyB,MAAM,EAAE,SAAS,EAAE,CA+B1C;CACL;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,QAAS,SAAQ,QAAQ;;IAClC,gBAAyB,MAAM,EAAE,SAAS,EAAE,CAuC1C;IAEO,MAAM,IAAI,cAAc;CAmBpC;AAWD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,aAAa,EAAE,UAAU,CAAC;QAC1B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
@@ -0,0 +1,298 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { reference } from './reference.js';
3
+ /**
4
+ * The size both controls are drawn at, and a floor rather than a preference.
5
+ *
6
+ * A native checkbox is 13x13 in this engine, measured, which is under WCAG 2.2's
7
+ * **2.5.8 Target Size (Minimum)** of 24x24 — and escapes it only through that criterion's
8
+ * *user agent control* exception, which covers a target whose size the author has not
9
+ * modified. Drawing it ourselves gives that exception up, so the floor becomes ours to
10
+ * hold, and `max()` is what holds it: a host who shrinks `--ui-space` restyles everything
11
+ * else and cannot shrink a control below the floor without saying so in their own CSS.
12
+ */
13
+ const size = css `max(24px, calc(${reference('--ui-space')} * 3))`;
14
+ /**
15
+ * The tick, as a mask rather than a picture — and that is what keeps it a token decision.
16
+ *
17
+ * A `data:` URI freezes whatever is inside it, so an SVG drawn with `stroke='#fff'` would
18
+ * be one hardcoded colour in a package whose rule is that a host can override every visual
19
+ * decision. **A mask has no colour**: only its alpha is read, so the black below is a
20
+ * shape and never a value. What the tick shows is the surface behind the control, punched
21
+ * out of the accent fill by `mask-composite: exclude`.
22
+ *
23
+ * The alternative was a mark rendered into the shadow root, and it is not available:
24
+ * `:host(:has(input:checked))` is invalid in this engine — `CSS.supports` says so, so it
25
+ * is a selector that does not exist rather than one that fails to update — and shadow CSS
26
+ * has no other way to read a slotted control's state. Only `::slotted(input:checked)`
27
+ * does, which means the control has to paint its own mark.
28
+ */
29
+ const tick = css `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E")`;
30
+ /** A mask layer covering the whole control, which the mark is then subtracted from. */
31
+ const whole = css `linear-gradient(#000, #000)`;
32
+ /**
33
+ * What a drawn boolean control looks like, which both elements share.
34
+ *
35
+ * The control is the host's own `<input type="checkbox">`, slotted, for the reason
36
+ * `src/input.ts` gives at length: an IDREF does not cross a shadow boundary, so a control
37
+ * rendered in here could not be labelled by the `<label>` beside it. Everything below
38
+ * reaches it through `::slotted`.
39
+ *
40
+ * **`appearance: none` is the whole cost of this component**, and it is taken knowingly.
41
+ * It buys one drawing for the pair — the same border, radius, focus ring and accent the
42
+ * rest of the kit already uses — and the alternative buys none: there is no native switch
43
+ * to delegate to. `<input type="checkbox" switch>` is unsupported in this engine,
44
+ * measured, so a switch is drawn whatever the checkbox does, and a delegated checkbox
45
+ * beside a drawn switch would argue cosmetically for a difference that is semantic.
46
+ *
47
+ * What it gives up is named where it is paid back: the indeterminate mark below, the
48
+ * target-size floor at {@link size}, and the forced-colors block at the end.
49
+ */
50
+ const toggle = css `
51
+ :host {
52
+ display: inline-flex;
53
+ }
54
+
55
+ ::slotted(input) {
56
+ appearance: none;
57
+ box-sizing: border-box;
58
+ /* The user agent puts 3px around a checkbox. Left in, it would sit inside the
59
+ host box and put the control off centre in any layout that aligns to it. */
60
+ margin: 0;
61
+ block-size: ${size};
62
+ background-color: ${reference('--ui-color-surface')};
63
+ background-repeat: no-repeat;
64
+ border: 1px solid ${reference('--ui-color-border')};
65
+ cursor: pointer;
66
+ /* The focus ring is deliberately not in this list, for the reason src/button.ts
67
+ gives beside its own: delaying the affordance that says *this is where you are*
68
+ is the opposite of what it exists to do. */
69
+ transition:
70
+ background-color ${reference('--ui-duration-state')} ${reference('--ui-easing-state')},
71
+ border-color ${reference('--ui-duration-state')} ${reference('--ui-easing-state')};
72
+ }
73
+
74
+ ::slotted(input:checked) {
75
+ background-color: ${reference('--ui-color-accent')};
76
+ border-color: ${reference('--ui-color-accent')};
77
+ }
78
+
79
+ /* Unchecked, the boundary finishes the mix it already started — the same hover
80
+ ui-input has, because it is the same boundary. Checked, the boundary is not what
81
+ the eye is on, so the fill moves instead, the way ui-button's does.
82
+
83
+ The disabled guard is measured rather than assumed: a disabled control still
84
+ matches :hover. */
85
+ ::slotted(input:hover:not(:disabled)) {
86
+ border-color: ${reference('--ui-color-text')};
87
+ }
88
+
89
+ ::slotted(input:checked:hover:not(:disabled)) {
90
+ background-color: ${reference('--ui-color-accent-hover')};
91
+ border-color: ${reference('--ui-color-accent-hover')};
92
+ }
93
+
94
+ /* A visible focus ring is not decoration: removing it is the single most common way a
95
+ component stops being usable by keyboard. */
96
+ ::slotted(input:focus-visible) {
97
+ outline: 2px solid ${reference('--ui-color-focus')};
98
+ outline-offset: 2px;
99
+ }
100
+
101
+ ::slotted(input:disabled) {
102
+ cursor: not-allowed;
103
+ opacity: 0.5;
104
+ }
105
+
106
+ /* The error state is not this component's to decide: ui-field sets aria-invalid on
107
+ the control as part of the wiring it already owns, and this rule reads it — so the
108
+ message under the field and the control cannot disagree about whether there is an
109
+ error. Only the boundary moves; a red fill on a switch would read as *on*. */
110
+ ::slotted(input[aria-invalid='true']) {
111
+ border-color: ${reference('--ui-color-danger')};
112
+ }
113
+
114
+ /* Forced colors replaces every author colour with one from the user's palette, so
115
+ the accent that says *checked* becomes the same Canvas as the surface that says
116
+ *not* — the state disappears, silently, for the people who turned the mode on to
117
+ see states more clearly. Measured in this engine. System colours are the ones
118
+ forced colors keeps, so the checked states name Highlight and the difference
119
+ survives; the mark keeps working untouched, because a mask has no colour to force.
120
+
121
+ Opacity is not a colour and is not forced, so the disabled control would still be
122
+ half-transparent against a palette chosen for contrast. GrayText is what that mode
123
+ has for *unavailable*, and it is a colour rather than a veil. */
124
+ @media (forced-colors: active) {
125
+ ::slotted(input:checked),
126
+ ::slotted(input:indeterminate) {
127
+ background-color: Highlight;
128
+ border-color: Highlight;
129
+ }
130
+
131
+ ::slotted(input:disabled) {
132
+ border-color: GrayText;
133
+ opacity: 1;
134
+ }
135
+ }
136
+ `;
137
+ /**
138
+ * A boolean control, drawn from the token layer rather than replaced.
139
+ *
140
+ * The `<input type="checkbox">` is the host's own and stays in the light DOM; this element
141
+ * is the box around it. {@link UiCheckbox} and {@link UiSwitch} differ in what they draw
142
+ * and in what they announce, never in what they are made of.
143
+ */
144
+ class UiToggle extends LitElement {
145
+ render() {
146
+ return html `<slot></slot>`;
147
+ }
148
+ }
149
+ /**
150
+ * A checkbox, styled by the token layer rather than replaced.
151
+ *
152
+ * **The `<input>` is yours.** You write it, you set its attributes, and it stays in the
153
+ * light DOM — so `name`, `checked`, `required` and `disabled` are the platform's business,
154
+ * and it reaches a form submit because it is a native control inside a `<form>`. The same
155
+ * shape `<ui-input>` has, for the same reason.
156
+ *
157
+ * **The indeterminate state is drawn, and that is not a feature being added.**
158
+ * `appearance: none` takes the platform's dash away with the rest of the drawing, so a
159
+ * control the host set `indeterminate` on would render as *unchecked* — a wrong answer
160
+ * rather than a missing one. The dash below is what stops that, and nothing here invites
161
+ * a tri-state that APG says is rare.
162
+ *
163
+ * Composes with {@link UiField}, which finds the control through this wrapper and wires
164
+ * the label, the help, the error and `aria-invalid` to it.
165
+ *
166
+ * @example
167
+ * ```html
168
+ * <ui-field>
169
+ * <label slot="label">Send a receipt</label>
170
+ * <ui-checkbox><input type="checkbox" name="receipt" /></ui-checkbox>
171
+ * </ui-field>
172
+ * ```
173
+ */
174
+ export class UiCheckbox extends UiToggle {
175
+ static { this.styles = [
176
+ toggle,
177
+ css `
178
+ ::slotted(input) {
179
+ inline-size: ${size};
180
+ border-radius: ${reference('--ui-radius')};
181
+ }
182
+
183
+ /* The mark is a hole: the whole control, minus the shape, so what shows
184
+ through is whatever the control sits on. The docblock on the mask constant
185
+ above says why a hole rather than a colour. */
186
+ ::slotted(input:checked) {
187
+ mask-image: ${whole}, ${tick};
188
+ mask-composite: exclude;
189
+ mask-size: 100% 100%;
190
+ mask-repeat: no-repeat;
191
+ }
192
+
193
+ /* A dash needs no drawing: a rectangle is a gradient with a size. */
194
+ ::slotted(input:indeterminate) {
195
+ background-color: ${reference('--ui-color-accent')};
196
+ border-color: ${reference('--ui-color-accent')};
197
+ mask-image: ${whole}, ${whole};
198
+ mask-composite: exclude;
199
+ mask-size:
200
+ 100% 100%,
201
+ calc(${size} / 2) calc(${size} / 12);
202
+ mask-position: center;
203
+ mask-repeat: no-repeat;
204
+ }
205
+ `,
206
+ ]; }
207
+ }
208
+ /**
209
+ * A switch, which is a checkbox that says *on* and *off* rather than *checked*.
210
+ *
211
+ * **The difference is semantic and the drawing follows it**, which is the order that
212
+ * matters: a switch takes effect immediately and a checkbox is a value you submit, so the
213
+ * two are not one component with two skins. `role="switch"` is what carries that to a
214
+ * screen reader, and this element sets it on the slotted control rather than asking the
215
+ * host to remember — forgetting it would leave a control that looks like a switch and
216
+ * announces as a checkbox, with nothing anywhere to read. A role the host wrote is never
217
+ * overwritten, the same way {@link UiField} never overwrites an `id` it did not generate.
218
+ *
219
+ * There is no native switch to delegate to: `<input type="checkbox" switch>` is
220
+ * unsupported in the engine this suite measures, so the element is a checkbox with a role
221
+ * and a drawing. A host who wants the mixed state wants {@link UiCheckbox} — `switch` has
222
+ * no third value, so this element does not draw one.
223
+ *
224
+ * @example
225
+ * ```html
226
+ * <ui-field>
227
+ * <label slot="label">Email notifications</label>
228
+ * <ui-switch><input type="checkbox" name="notify" checked /></ui-switch>
229
+ * </ui-field>
230
+ * ```
231
+ */
232
+ export class UiSwitch extends UiToggle {
233
+ static { this.styles = [
234
+ toggle,
235
+ css `
236
+ ::slotted(input) {
237
+ inline-size: calc(${size} * 5 / 3);
238
+ /* Any radius at or above half the block size rounds the ends fully, so
239
+ the whole size is a pill at every size this control can take. */
240
+ border-radius: ${size};
241
+ /* The track carries the boundary a checkbox gets from its border, so it
242
+ is the border token: the value chosen to clear 3:1 against the
243
+ surface, which is what WCAG 1.4.11 asks of a control's boundary. The
244
+ border is left to the shared sheet, which already paints it that exact
245
+ colour — restating it here changed no pixel, which is how it was
246
+ found: as a mutant no test could possibly kill. */
247
+ background-color: ${reference('--ui-color-border')};
248
+ /* The thumb is a background layer rather than an element, because an
249
+ element would have to live in the shadow root and could not be told
250
+ the control is on. A closest-side radius comes from the layer's
251
+ own size, so one number moves the thumb. */
252
+ background-image: radial-gradient(
253
+ circle closest-side,
254
+ ${reference('--ui-color-surface')} 100%,
255
+ transparent 100%
256
+ );
257
+ background-size: calc(${size} * 2 / 3);
258
+ background-position: calc(${size} / 6) center;
259
+ transition:
260
+ background-color ${reference('--ui-duration-state')}
261
+ ${reference('--ui-easing-state')},
262
+ border-color ${reference('--ui-duration-state')}
263
+ ${reference('--ui-easing-state')},
264
+ background-position ${reference('--ui-duration-state')}
265
+ ${reference('--ui-easing-state')};
266
+ }
267
+
268
+ ::slotted(input:checked) {
269
+ background-position: calc(100% - ${size} / 6) center;
270
+ }
271
+ `,
272
+ ]; }
273
+ render() {
274
+ return html `<slot @slotchange=${this.#announce}></slot>`;
275
+ }
276
+ /**
277
+ * Marks the slotted control a switch for anything reading the accessibility tree.
278
+ *
279
+ * On `slotchange` rather than once, because the host may replace the control — a
280
+ * framework re-render is indistinguishable from that here, and a switch that
281
+ * announces itself correctly only until the first re-render is worse than one that
282
+ * never did.
283
+ */
284
+ #announce() {
285
+ const control = this.querySelector('input');
286
+ if (control !== null && !control.hasAttribute('role')) {
287
+ control.setAttribute('role', 'switch');
288
+ }
289
+ }
290
+ }
291
+ // Stryker disable next-line StringLiteral: the registration runs once, at import, inside
292
+ // the warm process Stryker switches mutants in — so by the time a mutant on this line is
293
+ // active the element is already defined under the original name, and no test can observe
294
+ // the change. Outside the runner's reach, not an equivalent mutant.
295
+ customElements.define('ui-checkbox', UiCheckbox);
296
+ // Stryker disable next-line StringLiteral: the same, for the same reason.
297
+ customElements.define('ui-switch', UiSwitch);
298
+ //# sourceMappingURL=checkbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkbox.js","sourceRoot":"","sources":["../src/checkbox.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { UiButton, type ButtonVariant } from './button.js';
2
+ export { UiCheckbox, UiSwitch } from './checkbox.js';
2
3
  export { UiDialog } from './dialog.js';
3
4
  export { UiField } from './field.js';
4
5
  export { UiInput, UiTextarea } from './input.js';
6
+ export { UiSelect } from './select.js';
5
7
  export { tokens, derivedTokens, defaults, formulas, darkScheme, tokenStyleSheet, type Token, type DerivedToken, } from './tokens.js';
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { UiButton } from './button.js';
2
+ export { UiCheckbox, UiSwitch } from './checkbox.js';
2
3
  export { UiDialog } from './dialog.js';
3
4
  export { UiField } from './field.js';
4
5
  export { UiInput, UiTextarea } from './input.js';
6
+ export { UiSelect } from './select.js';
5
7
  export { tokens, derivedTokens, defaults, formulas, darkScheme, tokenStyleSheet, } from './tokens.js';
6
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { LitElement, type CSSResult, type TemplateResult } from 'lit';
2
+ /**
3
+ * A native `<select>`, styled by the token layer rather than replaced.
4
+ *
5
+ * **The `<select>` is yours.** You write it and its `<option>`s, and it stays in the light
6
+ * DOM — the same shape {@link UiInput} has, and forced by the same constraint: an ARIA
7
+ * relationship by IDREF does not cross a shadow boundary, so a control rendered in here
8
+ * could not be labelled by the `<label>` beside it. It reaches a form submit because it is
9
+ * a native control inside a `<form>`.
10
+ *
11
+ * **The native element is the decision, not a shortcut.** A custom listbox is an
12
+ * accessibility project of its own, and it would have to reimplement the platform picker
13
+ * a phone already opens — which is the part a consumer notices most and a library gets
14
+ * wrong most. RFC 0016 defers that listbox, and this element is not a step toward it.
15
+ *
16
+ * **What the platform still refuses is documented rather than worked around**, in
17
+ * `docs/select.md`: the drop-down list is drawn by the operating system and no rule here
18
+ * reaches inside it.
19
+ *
20
+ * ## Why the box is written out rather than shared
21
+ *
22
+ * `src/input.ts` carries the same box and says that duplicating it would be *two copies of
23
+ * a contract with nothing comparing them*. That objection is answered on its own terms
24
+ * rather than dodged: `tests/select.test.ts` mounts both and asserts they agree on the
25
+ * boundary, the radius, the padding, the font and the focus ring. Something compares them,
26
+ * and it fails when they drift.
27
+ *
28
+ * Sharing was the alternative and it costs the thing this file exists for — a consumer
29
+ * looking for `<ui-select>` finds `select.ts`, `select.stories.ts`, `select.test.ts` and
30
+ * `select.md`, rather than a select folded into the input's page under the input's name.
31
+ *
32
+ * @example
33
+ * ```html
34
+ * <ui-field>
35
+ * <label slot="label">Currency</label>
36
+ * <ui-select>
37
+ * <select name="currency">
38
+ * <option value="brl">Real</option>
39
+ * <option value="usd">Dollar</option>
40
+ * </select>
41
+ * </ui-select>
42
+ * </ui-field>
43
+ * ```
44
+ */
45
+ export declare class UiSelect extends LitElement {
46
+ static readonly styles: CSSResult;
47
+ render(): TemplateResult;
48
+ }
49
+ declare global {
50
+ interface HTMLElementTagNameMap {
51
+ 'ui-select': UiSelect;
52
+ }
53
+ }
54
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../src/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAqBjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,QAAS,SAAQ,UAAU;IACpC,gBAAyB,MAAM,EAAE,SAAS,CA8FxC;IAEO,MAAM,IAAI,cAAc;CAGpC;AAQD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
package/dist/select.js ADDED
@@ -0,0 +1,167 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { reference } from './reference.js';
3
+ /**
4
+ * One arm of the caret, and the gap it keeps from the edge.
5
+ *
6
+ * A fraction of the space token rather than a literal, so the mark scales with everything
7
+ * else a host retunes — and small enough that the two arms plus their gap fit inside the
8
+ * padding reserved for them below.
9
+ */
10
+ const arm = css `calc(${reference('--ui-space')} * 0.75)`;
11
+ /**
12
+ * The room the caret needs, which the text must not run into.
13
+ *
14
+ * The caret ends one space from the edge and is two arms wide, so this is that plus one
15
+ * more space of air. Written as the sum rather than as a number, because a number would be
16
+ * right at the default space token and wrong at every other.
17
+ */
18
+ const clearance = css `calc(${reference('--ui-space')} * 2 + ${arm} * 2)`;
19
+ /**
20
+ * A native `<select>`, styled by the token layer rather than replaced.
21
+ *
22
+ * **The `<select>` is yours.** You write it and its `<option>`s, and it stays in the light
23
+ * DOM — the same shape {@link UiInput} has, and forced by the same constraint: an ARIA
24
+ * relationship by IDREF does not cross a shadow boundary, so a control rendered in here
25
+ * could not be labelled by the `<label>` beside it. It reaches a form submit because it is
26
+ * a native control inside a `<form>`.
27
+ *
28
+ * **The native element is the decision, not a shortcut.** A custom listbox is an
29
+ * accessibility project of its own, and it would have to reimplement the platform picker
30
+ * a phone already opens — which is the part a consumer notices most and a library gets
31
+ * wrong most. RFC 0016 defers that listbox, and this element is not a step toward it.
32
+ *
33
+ * **What the platform still refuses is documented rather than worked around**, in
34
+ * `docs/select.md`: the drop-down list is drawn by the operating system and no rule here
35
+ * reaches inside it.
36
+ *
37
+ * ## Why the box is written out rather than shared
38
+ *
39
+ * `src/input.ts` carries the same box and says that duplicating it would be *two copies of
40
+ * a contract with nothing comparing them*. That objection is answered on its own terms
41
+ * rather than dodged: `tests/select.test.ts` mounts both and asserts they agree on the
42
+ * boundary, the radius, the padding, the font and the focus ring. Something compares them,
43
+ * and it fails when they drift.
44
+ *
45
+ * Sharing was the alternative and it costs the thing this file exists for — a consumer
46
+ * looking for `<ui-select>` finds `select.ts`, `select.stories.ts`, `select.test.ts` and
47
+ * `select.md`, rather than a select folded into the input's page under the input's name.
48
+ *
49
+ * @example
50
+ * ```html
51
+ * <ui-field>
52
+ * <label slot="label">Currency</label>
53
+ * <ui-select>
54
+ * <select name="currency">
55
+ * <option value="brl">Real</option>
56
+ * <option value="usd">Dollar</option>
57
+ * </select>
58
+ * </ui-select>
59
+ * </ui-field>
60
+ * ```
61
+ */
62
+ export class UiSelect extends LitElement {
63
+ static { this.styles = css `
64
+ :host {
65
+ display: block;
66
+ }
67
+
68
+ /* The box, which src/input.ts also draws and tests/select.test.ts compares. */
69
+ ::slotted(select) {
70
+ box-sizing: border-box;
71
+ inline-size: 100%;
72
+ font: inherit;
73
+ font-family: ${reference('--ui-font')};
74
+ color: ${reference('--ui-color-text')};
75
+ background-color: ${reference('--ui-color-surface')};
76
+ border: 1px solid ${reference('--ui-color-border')};
77
+ border-radius: ${reference('--ui-radius')};
78
+ padding: ${reference('--ui-space')};
79
+ /* A select opens something when clicked, so it is a pointer rather than a
80
+ caret — the one place this box disagrees with the input's on purpose. */
81
+ cursor: pointer;
82
+ /* Only the boundary moves. The focus ring is deliberately not in this list,
83
+ for the reason src/button.ts gives beside its own. */
84
+ transition: border-color ${reference('--ui-duration-state')}
85
+ ${reference('--ui-easing-state')};
86
+ }
87
+
88
+ /* Taking the platform's own drawing off is what makes the box possible at all: a
89
+ select left at appearance: auto keeps the operating system's chevron and, off
90
+ this engine, ignores much of what is set above — so the control beside a text
91
+ field would stop matching it exactly where a kit is supposed to. What that
92
+ costs is the chevron, which is drawn below.
93
+
94
+ It does NOT cost the picker. appearance: none changes how the closed control
95
+ is painted and nothing about what opens, so a phone still opens its own wheel. */
96
+ ::slotted(select) {
97
+ appearance: none;
98
+ padding-inline-end: ${clearance};
99
+ background-repeat: no-repeat;
100
+ }
101
+
102
+ /* The caret is two triangles rather than a picture, and that is what keeps its
103
+ colour a token: gradients take var(), and an SVG in a data: URI would
104
+ freeze whatever colour was drawn into it. src/checkbox.ts solved the same
105
+ problem the other way, by making its mark a hole — which is not available here,
106
+ because a mask would clip the option text with it.
107
+
108
+ A multiple select is a list rather than a drop-down, and a caret on a list
109
+ points at nothing, so it is guarded rather than drawn everywhere. */
110
+ ::slotted(select:not([multiple])) {
111
+ background-image:
112
+ linear-gradient(45deg, transparent 50%, ${reference('--ui-color-text-muted')} 50%),
113
+ linear-gradient(135deg, ${reference('--ui-color-text-muted')} 50%, transparent 50%);
114
+ background-size: ${arm} ${arm};
115
+ background-position:
116
+ right calc(${reference('--ui-space')} + ${arm}) center,
117
+ right ${reference('--ui-space')} center;
118
+ }
119
+
120
+ /* Direction is the control's own, not the page's, and background-position has no
121
+ logical form — so the one physical thing in this sheet is mirrored explicitly.
122
+ The padding needs no rule: padding-inline-end already follows. Measured, both
123
+ halves. */
124
+ ::slotted(select:not([multiple]):dir(rtl)) {
125
+ background-position:
126
+ left ${reference('--ui-space')} center,
127
+ left calc(${reference('--ui-space')} + ${arm}) center;
128
+ }
129
+
130
+ /* The boundary completes the mix it already started, the same way ui-input's does
131
+ and against the same token.
132
+
133
+ There is no [readonly] guard here and its absence is measured rather than
134
+ forgotten: readOnly is not a property of a select at all, so the input's
135
+ second guard would be a rule about an attribute the platform never sets. */
136
+ ::slotted(select:hover:not(:disabled)) {
137
+ border-color: ${reference('--ui-color-text')};
138
+ }
139
+
140
+ /* A visible focus ring is not decoration: removing it is the single most common
141
+ way a component stops being usable by keyboard. */
142
+ ::slotted(select:focus-visible) {
143
+ outline: 2px solid ${reference('--ui-color-focus')};
144
+ outline-offset: 2px;
145
+ }
146
+
147
+ ::slotted(select:disabled) {
148
+ cursor: not-allowed;
149
+ opacity: 0.5;
150
+ }
151
+
152
+ /* The error state is not this component's to decide: ui-field sets aria-invalid on
153
+ the control as part of the wiring it already owns, and this rule reads it. */
154
+ ::slotted(select[aria-invalid='true']) {
155
+ border-color: ${reference('--ui-color-danger')};
156
+ }
157
+ `; }
158
+ render() {
159
+ return html `<slot></slot>`;
160
+ }
161
+ }
162
+ // Stryker disable next-line StringLiteral: the registration runs once, at import, inside
163
+ // the warm process Stryker switches mutants in — so by the time a mutant on this line is
164
+ // active the element is already defined under the original name, and no test can observe
165
+ // the change. Outside the runner's reach, not an equivalent mutant.
166
+ customElements.define('ui-select', UiSelect);
167
+ //# sourceMappingURL=select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.js","sourceRoot":"","sources":["../src/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuC,MAAM,KAAK,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,GAAG,GAAG,GAAG,CAAA,QAAQ,SAAS,CAAC,YAAY,CAAC,UAAU,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,GAAG,CAAA,QAAQ,SAAS,CAAC,YAAY,CAAC,UAAU,GAAG,OAAO,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;aACX,WAAM,GAAc,GAAG,CAAA;;;;;;;;;;2BAUzB,SAAS,CAAC,WAAW,CAAC;qBAC5B,SAAS,CAAC,iBAAiB,CAAC;gCACjB,SAAS,CAAC,oBAAoB,CAAC;gCAC/B,SAAS,CAAC,mBAAmB,CAAC;6BACjC,SAAS,CAAC,aAAa,CAAC;uBAC9B,SAAS,CAAC,YAAY,CAAC;;;;;;uCAMP,SAAS,CAAC,qBAAqB,CAAC;kBACrD,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;kCAad,SAAS;;;;;;;;;;;;;;0DAce,SAAS,CAAC,uBAAuB,CAAC;0CAClD,SAAS,CAAC,uBAAuB,CAAC;+BAC7C,GAAG,IAAI,GAAG;;6BAEZ,SAAS,CAAC,YAAY,CAAC,MAAM,GAAG;wBACrC,SAAS,CAAC,YAAY,CAAC;;;;;;;;;uBASxB,SAAS,CAAC,YAAY,CAAC;4BAClB,SAAS,CAAC,YAAY,CAAC,MAAM,GAAG;;;;;;;;;;4BAUhC,SAAS,CAAC,iBAAiB,CAAC;;;;;;iCAMvB,SAAS,CAAC,kBAAkB,CAAC;;;;;;;;;;;;4BAYlC,SAAS,CAAC,mBAAmB,CAAC;;KAErD,CAAC;IAEO,MAAM;QACX,OAAO,IAAI,CAAA,eAAe,CAAC;IAC/B,CAAC;;AAGL,yFAAyF;AACzF,yFAAyF;AACzF,yFAAyF;AACzF,oEAAoE;AACpE,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rak200/ui",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Host-agnostic custom-element UI components for the rak200 ecosystem",
5
5
  "license": "MIT",
6
6
  "author": "rak200 <rak.ricardo@windowslive.com>",