@mystravil/hardpoint 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +5 -5
- package/dist/base.css +279 -0
- package/dist/components/Checkbox.svelte +46 -9
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +131 -23
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +104 -27
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +4 -1
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +135 -119
- package/dist/components/NativeSelect.svelte.d.ts +6 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Popover.svelte +6 -6
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +89 -23
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Spinner.svelte +3 -2
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/Toaster.svelte +6 -6
- package/dist/components/Tooltip.svelte +5 -5
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Hardpoint follows semantic versioning. Before 1.0.0, a breaking change raises the minor version.
|
|
4
4
|
|
|
5
|
+
## 0.2.0 (2026-11-11)
|
|
6
|
+
|
|
7
|
+
- **Added**:
|
|
8
|
+
- `Meter`: a measurement inside a known range as separate segments, coloured by `warnAt` and `dangerAt` or by an app-set `tone`, with an optional `marker` and `detail` line.
|
|
9
|
+
- `Notice`: a persistent message in the page flow with an optional tone mark, title, text, up to two actions and a close button.
|
|
10
|
+
- `TextArea`: a multi-line text field that grows from `rows` to `maxRows` lines, then scrolls inside, with TextField's label, hint and error.
|
|
11
|
+
- `RadioGroup`: one choice from a set of rows, each with an optional hint and tag, named by a caps `label` or a `legend`, with a group `error`.
|
|
12
|
+
- `CheckboxGroup`: `legend` (a name set as written, instead of `label`), `error` and an item `tag`.
|
|
13
|
+
- `Checkbox`: `tag`. The whole row is now the target: a press on the hint, the tag or the gap counts as a press on the label.
|
|
14
|
+
- `ListDetail`: a list column beside the open item from `splitFrom` (`medium`, `expanded` by default, or `large`), one pane at a time below it, picked by a bindable `showDetail`.
|
|
15
|
+
- `ImageViewer`: one image full screen at every width over a Bits Dialog, with pinch zoom, pan, double-tap and wheel zoom, a Download button, a loading spinner, and Close, Escape or a swipe down at scale 1 to leave.
|
|
16
|
+
- `.hp-prose` in `base.css`, with `.hp-prose-code`, `.hp-prose-copy` and `.hp-prose-scroll`: styles for rendered markdown or sanitised HTML, at zero specificity. It covers headings, lists, task lists, quotes, code blocks with a copy button, tables that scroll inside their wrapper, and images.
|
|
17
|
+
- Grouped options: `group` on `SelectItem`, `ComboboxItem` and `NativeSelectOption`. Select and Combobox show a sticky heading per group, and NativeSelect renders `<optgroup>`.
|
|
18
|
+
- The token `--list-detail-width: 320px` (ListDetail's list column).
|
|
19
|
+
- **Changed**:
|
|
20
|
+
- `--popover-width`, `--tooltip-width` and `--toast-width` can now be set from an app's class and stylesheet rule. Before, the component's own rule won over an app's one-class rule.
|
|
21
|
+
- The Dialog body sets `scroll-padding-block: var(--space-2)`, so a revealed caret stays 8 px clear of the action row.
|
|
22
|
+
- Combobox's default filter also matches an item's `group`, drops the heading of a group with no match, and scrolls the list to the top as you type.
|
|
23
|
+
- **Checked**: every exported component, `toast()` and `Motion` run under `style-src 'self'` with no policy violation.
|
|
24
|
+
|
|
5
25
|
## 0.1.0 (2026-11-11)
|
|
6
26
|
|
|
7
27
|
The first public release. It needs Svelte 5.57 or newer.
|
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ The package imports nothing from SvelteKit, so plain Svelte apps can use it. The
|
|
|
8
8
|
|
|
9
9
|
## Components
|
|
10
10
|
|
|
11
|
-
- Actions and forms: `Button`, `TextField`, `NativeSelect`, `Select`, `Combobox`, `Checkbox`, `CheckboxGroup`, `Switch`.
|
|
12
|
-
- Overlays: `Dialog` (centred, bottom sheet or full screen), `Popover`, `Menu` with `MenuItem`, `MenuCheckboxItem`, `MenuRadioGroup`, `MenuRadioItem`, `MenuGroup` and `MenuSeparator`, `Tooltip` with `TooltipGroup`,
|
|
13
|
-
- Navigation: `AppShell` (header, bottom bar or drawer, rail or sidebar), `Tabs`, `Steps`, `Pagination`.
|
|
14
|
-
- Data and display: `Table`, `KeyValueList`, `Stat`, `Tag`, `Timeline`, `MediaFrame` with `MediaImage`, `Disclosure`.
|
|
15
|
-
- Feedback: `Progress`, `Spinner`, `Skeleton`, `LoadRegion`, `EmptyState`, `ErrorState`.
|
|
11
|
+
- Actions and forms: `Button`, `TextField`, `TextArea`, `NativeSelect`, `Select`, `Combobox`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`.
|
|
12
|
+
- Overlays: `Dialog` (centred, bottom sheet or full screen), `Popover`, `Menu` with `MenuItem`, `MenuCheckboxItem`, `MenuRadioGroup`, `MenuRadioItem`, `MenuGroup` and `MenuSeparator`, `Tooltip` with `TooltipGroup`, `Toaster` with `toast()`, and `ImageViewer` (one image full screen, with pinch zoom).
|
|
13
|
+
- Navigation and layout: `AppShell` (header, bottom bar or drawer, rail or sidebar), `ListDetail` (a list beside the open item, one pane at a time on narrow screens), `Tabs`, `Steps`, `Pagination`.
|
|
14
|
+
- Data and display: `Table`, `KeyValueList`, `Stat`, `Tag`, `Timeline`, `MediaFrame` with `MediaImage`, `Disclosure`, and `.hp-prose` in `base.css` for rendered markdown.
|
|
15
|
+
- Feedback: `Progress`, `Meter`, `Notice`, `Spinner`, `Skeleton`, `LoadRegion`, `EmptyState`, `ErrorState`.
|
|
16
16
|
|
|
17
17
|
The source repository has a showcase app with a docs page for each component, the foundations, the motion tokens and a specimen app.
|
|
18
18
|
|
package/dist/base.css
CHANGED
|
@@ -330,6 +330,285 @@ body {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
/* ---- Prose ------------------------------------------------------------- */
|
|
334
|
+
|
|
335
|
+
/* Rendered markdown or sanitised HTML that an app cannot scope styles into:
|
|
336
|
+
put .hp-prose on its container. Every rule is :where(.hp-prose) …, so it
|
|
337
|
+
has no specificity and any app rule wins. Sizes in em follow the font
|
|
338
|
+
size the app sets on the container (16px on 24px lines by default); the
|
|
339
|
+
two heading sizes are the type tokens. Blocks do not stop at --measure:
|
|
340
|
+
the container's width decides, so give it a readable one.
|
|
341
|
+
|
|
342
|
+
Gaps: 1em between blocks, 0.5em between a heading and what it
|
|
343
|
+
introduces, 0.5em between blocks inside a list item and 0.25em between
|
|
344
|
+
items, so each gap between blocks is at least twice the gap inside one.
|
|
345
|
+
|
|
346
|
+
The ink is --text, with --muted for markers, quotes and the small
|
|
347
|
+
headings. Nothing here is a hue. */
|
|
348
|
+
|
|
349
|
+
:where(.hp-prose) {
|
|
350
|
+
/* In a flex or grid column the box may shrink below a long word. */
|
|
351
|
+
min-width: 0;
|
|
352
|
+
font-family: var(--sans);
|
|
353
|
+
font-size: var(--type-body);
|
|
354
|
+
line-height: 1.5;
|
|
355
|
+
/* A long address or word breaks inside the column; tables and code
|
|
356
|
+
blocks set their own rules below. */
|
|
357
|
+
overflow-wrap: anywhere;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Block gaps: space above each block, none below, so the first and last
|
|
361
|
+
child need no special case and a margin never doubles. */
|
|
362
|
+
:where(.hp-prose) :where(p, ul, ol, pre, blockquote, table, h1, h2, h3, h4, h5, h6, .hp-prose-code, .hp-prose-scroll) {
|
|
363
|
+
margin-block: 1em 0;
|
|
364
|
+
margin-inline: 0;
|
|
365
|
+
}
|
|
366
|
+
:where(.hp-prose) :where(p) {
|
|
367
|
+
max-width: none;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* Headings as written, never caps. h1 and h2 take the page-title and
|
|
371
|
+
section-title roles; h3 to h6 stay at body size and step down in weight,
|
|
372
|
+
then in ink. More space above a heading than below it groups it with what
|
|
373
|
+
follows. */
|
|
374
|
+
:where(.hp-prose) :where(h1) {
|
|
375
|
+
margin-block-start: 1.5em;
|
|
376
|
+
font-size: var(--type-page);
|
|
377
|
+
font-weight: 500;
|
|
378
|
+
line-height: 1.25;
|
|
379
|
+
line-height: round(up, 1.25em, 4px);
|
|
380
|
+
letter-spacing: -0.015em;
|
|
381
|
+
}
|
|
382
|
+
:where(.hp-prose) :where(h2) {
|
|
383
|
+
margin-block-start: 1.6em;
|
|
384
|
+
font-size: var(--type-section);
|
|
385
|
+
font-weight: 500;
|
|
386
|
+
line-height: 1.25;
|
|
387
|
+
line-height: round(up, 1.25em, 4px);
|
|
388
|
+
letter-spacing: -0.01em;
|
|
389
|
+
}
|
|
390
|
+
:where(.hp-prose) :where(h3, h4, h5, h6) {
|
|
391
|
+
margin-block-start: 1.5em;
|
|
392
|
+
font-size: 1em;
|
|
393
|
+
font-weight: 500;
|
|
394
|
+
}
|
|
395
|
+
:where(.hp-prose) :where(h3) {
|
|
396
|
+
font-weight: 600;
|
|
397
|
+
}
|
|
398
|
+
:where(.hp-prose) :where(h5, h6) {
|
|
399
|
+
color: var(--muted);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
:where(.hp-prose) :where(strong, b) {
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
}
|
|
405
|
+
:where(.hp-prose) :where(del, s) {
|
|
406
|
+
text-decoration-line: line-through;
|
|
407
|
+
text-decoration-thickness: 1px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Lists: square markers on bulleted lists (square corners everywhere),
|
|
411
|
+
tabular figures on numbered ones, both in --muted. The markers hang
|
|
412
|
+
outside the text, so the text edge lines up with the paragraphs. A
|
|
413
|
+
start number comes from the list's own attribute. */
|
|
414
|
+
:where(.hp-prose) :where(ul, ol) {
|
|
415
|
+
padding-inline-start: 1.5em;
|
|
416
|
+
}
|
|
417
|
+
:where(.hp-prose) :where(ol) {
|
|
418
|
+
padding-inline-start: 2em;
|
|
419
|
+
list-style-type: decimal;
|
|
420
|
+
}
|
|
421
|
+
:where(.hp-prose) :where(ul) {
|
|
422
|
+
list-style-type: square;
|
|
423
|
+
}
|
|
424
|
+
:where(.hp-prose li)::marker {
|
|
425
|
+
color: var(--muted);
|
|
426
|
+
}
|
|
427
|
+
:where(.hp-prose ol > li)::marker {
|
|
428
|
+
font-variant-numeric: tabular-nums;
|
|
429
|
+
}
|
|
430
|
+
/* Tight inside a list: items 0.25em apart, a nested list 0.25em under its
|
|
431
|
+
item's text, other blocks in an item 0.5em apart. */
|
|
432
|
+
:where(.hp-prose li) + :where(li) {
|
|
433
|
+
margin-block-start: 0.25em;
|
|
434
|
+
}
|
|
435
|
+
:where(.hp-prose li > * + *) {
|
|
436
|
+
margin-block-start: 0.5em;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Task lists. A checkbox in an item is drawn as Hardpoint's checkbox box
|
|
440
|
+
in its disabled form: a black box with the quiet --border edge and a
|
|
441
|
+
--muted mark, and it takes the marker's place. It is out of the flow,
|
|
442
|
+
so the item's text keeps the list's text edge. */
|
|
443
|
+
:where(.hp-prose li:has(> input[type='checkbox'], > p:first-child > input[type='checkbox'])) {
|
|
444
|
+
position: relative;
|
|
445
|
+
list-style: none;
|
|
446
|
+
}
|
|
447
|
+
:where(.hp-prose li input[type='checkbox']) {
|
|
448
|
+
appearance: none;
|
|
449
|
+
position: absolute;
|
|
450
|
+
inset-block-start: calc((1lh - 1.25em) / 2);
|
|
451
|
+
inset-inline-start: -1.5em;
|
|
452
|
+
display: grid;
|
|
453
|
+
place-items: center;
|
|
454
|
+
box-sizing: border-box;
|
|
455
|
+
width: 1.25em;
|
|
456
|
+
height: 1.25em;
|
|
457
|
+
margin: 0;
|
|
458
|
+
padding: 0;
|
|
459
|
+
border: 1px solid var(--border);
|
|
460
|
+
border-radius: 0;
|
|
461
|
+
background: var(--canvas);
|
|
462
|
+
color: var(--muted);
|
|
463
|
+
}
|
|
464
|
+
/* The mark is the box's 12px check as a clipped square, so it takes the
|
|
465
|
+
text colour and needs no image. */
|
|
466
|
+
:where(.hp-prose li input[type='checkbox']:checked)::before {
|
|
467
|
+
content: '';
|
|
468
|
+
width: 0.75em;
|
|
469
|
+
height: 0.75em;
|
|
470
|
+
background: currentColor;
|
|
471
|
+
clip-path: polygon(6.6% 60.1%, 37.5% 91%, 93.4% 35.1%, 81.6% 23.3%, 37.5% 67.4%, 18.4% 48.3%);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* Quotes: a 3px edge in --border, since titanium marks selection, and the
|
|
475
|
+
text in --muted. */
|
|
476
|
+
:where(.hp-prose) :where(blockquote) {
|
|
477
|
+
padding-inline-start: 0.75em;
|
|
478
|
+
border-inline-start: 3px solid var(--border);
|
|
479
|
+
color: var(--muted);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
:where(.hp-prose) :where(hr) {
|
|
483
|
+
height: 0;
|
|
484
|
+
margin-block: 1.5em;
|
|
485
|
+
margin-inline: 0;
|
|
486
|
+
border: 0;
|
|
487
|
+
border-block-start: 1px solid var(--border);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* A markdown image sits in a paragraph, which has its own gap; one that
|
|
491
|
+
sits directly in the container gets the block gap too. */
|
|
492
|
+
:where(.hp-prose) :where(img) {
|
|
493
|
+
display: block;
|
|
494
|
+
max-width: 100%;
|
|
495
|
+
height: auto;
|
|
496
|
+
}
|
|
497
|
+
:where(.hp-prose) > :where(img) {
|
|
498
|
+
margin-block-start: 1em;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Code. Inline code is a little smaller than the text around it, because
|
|
502
|
+
Geist Mono looks larger at the same size. A block is Geist Mono on a 20px
|
|
503
|
+
rhythm, never wraps and scrolls sideways inside its own box. It has a
|
|
504
|
+
1px --border hairline instead of a fill: --surface-hover cannot be seen
|
|
505
|
+
on black, --surface-band is heavy under a long listing, and the line
|
|
506
|
+
reads in both themes and marks where the scrolling area ends. */
|
|
507
|
+
:where(.hp-prose) :where(code, kbd, samp) {
|
|
508
|
+
font-size: 0.875em;
|
|
509
|
+
}
|
|
510
|
+
:where(.hp-prose) :where(pre) {
|
|
511
|
+
padding: var(--space-3);
|
|
512
|
+
overflow-x: auto;
|
|
513
|
+
overscroll-behavior-x: contain;
|
|
514
|
+
border: 1px solid var(--border);
|
|
515
|
+
font-size: 0.875em;
|
|
516
|
+
line-height: calc(20 / 14);
|
|
517
|
+
white-space: pre;
|
|
518
|
+
overflow-wrap: normal;
|
|
519
|
+
tab-size: 4;
|
|
520
|
+
}
|
|
521
|
+
:where(.hp-prose pre) :where(code, kbd, samp) {
|
|
522
|
+
font-size: inherit;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* A code block with a copy button: wrap the pre and the app's Button
|
|
526
|
+
(class hp-prose-copy) in .hp-prose-code. The wrapper draws the hairline and
|
|
527
|
+
the button sits at the top end, outside the text, so a renderer that
|
|
528
|
+
replaces the pre keeps it. The pre leaves a column for the button, so
|
|
529
|
+
text, which scrolls sideways under any overlay, never runs beneath it. */
|
|
530
|
+
:where(.hp-prose .hp-prose-code) {
|
|
531
|
+
position: relative;
|
|
532
|
+
border: 1px solid var(--border);
|
|
533
|
+
}
|
|
534
|
+
:where(.hp-prose .hp-prose-code > pre) {
|
|
535
|
+
margin: 0;
|
|
536
|
+
margin-inline-end: calc(var(--control-height) + var(--space-2));
|
|
537
|
+
border: 0;
|
|
538
|
+
}
|
|
539
|
+
:where(.hp-prose .hp-prose-copy) {
|
|
540
|
+
position: absolute;
|
|
541
|
+
inset-block-start: var(--space-1);
|
|
542
|
+
inset-inline-end: var(--space-1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Tables. The header band is --surface-band with cells as written, never
|
|
546
|
+
caps. Cells are Geist, not Geist Mono: they hold words. Text that wraps
|
|
547
|
+
makes rows of several lines, and without a line between rows the eye
|
|
548
|
+
cannot tell where one ends, so every row has a 1px --border divider (the
|
|
549
|
+
table's ruled form), and no zebra. A table is as wide as its content or
|
|
550
|
+
its box, whichever is wider; wrap each in .hp-prose-scroll and a table
|
|
551
|
+
wider than the column scrolls sideways inside it. Alignment comes from
|
|
552
|
+
the cell's align attribute or its own text-align. */
|
|
553
|
+
:where(.hp-prose .hp-prose-scroll) {
|
|
554
|
+
max-width: 100%;
|
|
555
|
+
overflow-x: auto;
|
|
556
|
+
overscroll-behavior-x: contain;
|
|
557
|
+
}
|
|
558
|
+
:where(.hp-prose) :where(table) {
|
|
559
|
+
min-width: 100%;
|
|
560
|
+
border-collapse: collapse;
|
|
561
|
+
font-variant-numeric: tabular-nums;
|
|
562
|
+
}
|
|
563
|
+
:where(.hp-prose) :where(th, td) {
|
|
564
|
+
padding: var(--space-2) var(--space-3);
|
|
565
|
+
border-block-end: 1px solid var(--border);
|
|
566
|
+
overflow-wrap: normal;
|
|
567
|
+
text-align: start;
|
|
568
|
+
vertical-align: top;
|
|
569
|
+
}
|
|
570
|
+
:where(.hp-prose th) {
|
|
571
|
+
border-block-end: 0;
|
|
572
|
+
background: var(--surface-band);
|
|
573
|
+
font-weight: 500;
|
|
574
|
+
}
|
|
575
|
+
/* The cell's align attribute: the browser maps it to text-align at the
|
|
576
|
+
lowest priority, so the start alignment above would hide it. */
|
|
577
|
+
:where(.hp-prose :is(th, td)[align='left']) {
|
|
578
|
+
text-align: left;
|
|
579
|
+
}
|
|
580
|
+
:where(.hp-prose :is(th, td)[align='center']) {
|
|
581
|
+
text-align: center;
|
|
582
|
+
}
|
|
583
|
+
:where(.hp-prose :is(th, td)[align='right']) {
|
|
584
|
+
text-align: right;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@media (pointer: fine) {
|
|
588
|
+
:where(.hp-prose pre, .hp-prose .hp-prose-scroll) {
|
|
589
|
+
scrollbar-width: thin;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* Gaps that depend on neighbours come last, so they win over the block gap:
|
|
594
|
+
a heading keeps 0.5em from what it introduces, and the first and last
|
|
595
|
+
child of any box touch its edges. */
|
|
596
|
+
:where(.hp-prose) :where(h1, h2, h3, h4, h5, h6) + * {
|
|
597
|
+
margin-block-start: 0.5em;
|
|
598
|
+
}
|
|
599
|
+
:where(.hp-prose :first-child) {
|
|
600
|
+
margin-block-start: 0;
|
|
601
|
+
}
|
|
602
|
+
:where(.hp-prose :last-child) {
|
|
603
|
+
margin-block-end: 0;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* A nested list follows its item's text, which is not an element, so it is
|
|
607
|
+
that item's first child: it takes its 0.25em back after the rule above. */
|
|
608
|
+
:where(.hp-prose li > :is(ul, ol)) {
|
|
609
|
+
margin-block-start: 0.25em;
|
|
610
|
+
}
|
|
611
|
+
|
|
333
612
|
/* ---- Utilities --------------------------------------------------------- */
|
|
334
613
|
|
|
335
614
|
/* Hidden from sight, kept in the accessibility tree. */
|
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
/** Attributes and handlers not listed here go to the checkbox button. */
|
|
5
5
|
export type CheckboxProps = Omit<BitsCheckbox.RootProps, 'child' | 'children' | 'ref'> & {
|
|
6
6
|
/**
|
|
7
|
-
* The text beside the box, as written (never caps). Pressing it
|
|
8
|
-
* the box. Without it, give the box
|
|
7
|
+
* The text beside the box, as written (never caps). Pressing it, or
|
|
8
|
+
* anywhere on the box's row, toggles the box. Without it, give the box
|
|
9
|
+
* an accessible name with `aria-label`.
|
|
9
10
|
*/
|
|
10
11
|
label?: string;
|
|
12
|
+
/**
|
|
13
|
+
* A Tag after the label, such as "Recommended": a short fixed string the
|
|
14
|
+
* app defines, set in caps. It is part of the box's accessible name and
|
|
15
|
+
* shows only with a `label`.
|
|
16
|
+
*/
|
|
17
|
+
tag?: string;
|
|
11
18
|
/** A second line in `--muted` under the label, read out as the box's description. */
|
|
12
19
|
hint?: string;
|
|
13
20
|
/** The checkbox button. */
|
|
@@ -16,12 +23,15 @@
|
|
|
16
23
|
</script>
|
|
17
24
|
|
|
18
25
|
<script lang="ts">
|
|
26
|
+
import Tag from './Tag.svelte';
|
|
27
|
+
|
|
19
28
|
const uid = $props.id();
|
|
20
29
|
|
|
21
30
|
let {
|
|
22
31
|
checked = $bindable(false),
|
|
23
32
|
indeterminate = $bindable(false),
|
|
24
33
|
label,
|
|
34
|
+
tag,
|
|
25
35
|
hint,
|
|
26
36
|
id = uid,
|
|
27
37
|
ref = $bindable(null),
|
|
@@ -37,9 +47,10 @@
|
|
|
37
47
|
<!--
|
|
38
48
|
A Bits UI Checkbox: a <button role="checkbox"> with aria-checked true,
|
|
39
49
|
false or mixed, and its state in data-state (checked, unchecked,
|
|
40
|
-
indeterminate) and data-disabled. Pressing the box or
|
|
41
|
-
|
|
42
|
-
`value`. Attributes, handlers
|
|
50
|
+
indeterminate) and data-disabled. Pressing the box or anywhere on its row
|
|
51
|
+
(the label, the tag, the hint) toggles it; an indeterminate box becomes
|
|
52
|
+
checked. Inside a CheckboxGroup, give it a `value`. Attributes, handlers
|
|
53
|
+
and `class` go to the button.
|
|
43
54
|
-->
|
|
44
55
|
<div class="hp-checkbox">
|
|
45
56
|
<span class="control">
|
|
@@ -53,7 +64,9 @@
|
|
|
53
64
|
</BitsCheckbox.Root>
|
|
54
65
|
</span>
|
|
55
66
|
{#if label}
|
|
56
|
-
<label class="hp-dense" for={id}
|
|
67
|
+
<label class="hp-dense" class:tagged={!!tag} for={id}
|
|
68
|
+
>{label}{#if tag}{' '}<Tag as="span">{tag}</Tag>{/if}</label
|
|
69
|
+
>
|
|
57
70
|
{/if}
|
|
58
71
|
{#if hint}
|
|
59
72
|
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
@@ -65,14 +78,21 @@
|
|
|
65
78
|
band (24px fine, 44px coarse): the box sits centred in it and the
|
|
66
79
|
label's first line too, so a label that wraps keeps the box beside its
|
|
67
80
|
first line. The hint runs under the label, aligned with its text.
|
|
68
|
-
|
|
81
|
+
The root is as wide as its content (without a label, as the box), and
|
|
82
|
+
with a label all of it is the target: the label's ::after covers the
|
|
83
|
+
root, so a press on the hint, the tag or the space between them is a
|
|
84
|
+
press on the label. The box sits above that cover with its own hit
|
|
85
|
+
area. */
|
|
69
86
|
.hp-checkbox {
|
|
70
87
|
/* The label's line height, as .hp-dense sets it. */
|
|
71
88
|
--_line: 24px;
|
|
89
|
+
position: relative;
|
|
72
90
|
display: grid;
|
|
73
91
|
grid-template-columns: auto;
|
|
74
92
|
justify-content: start;
|
|
75
93
|
align-items: start;
|
|
94
|
+
width: fit-content;
|
|
95
|
+
max-width: 100%;
|
|
76
96
|
}
|
|
77
97
|
.hp-checkbox:has(> label) {
|
|
78
98
|
grid-template-columns: auto minmax(0, 1fr);
|
|
@@ -86,6 +106,7 @@
|
|
|
86
106
|
|
|
87
107
|
.control {
|
|
88
108
|
position: relative;
|
|
109
|
+
z-index: 1;
|
|
89
110
|
display: flex;
|
|
90
111
|
align-items: center;
|
|
91
112
|
height: var(--hit-target);
|
|
@@ -152,6 +173,19 @@
|
|
|
152
173
|
cursor: pointer;
|
|
153
174
|
-webkit-tap-highlight-color: transparent;
|
|
154
175
|
}
|
|
176
|
+
label::after {
|
|
177
|
+
content: '';
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset: 0;
|
|
180
|
+
}
|
|
181
|
+
/* The tag sits after the label when both fit on one line; otherwise it
|
|
182
|
+
wraps under the label whole. */
|
|
183
|
+
.tagged {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-wrap: wrap;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: var(--space-1) var(--space-2);
|
|
188
|
+
}
|
|
155
189
|
|
|
156
190
|
/* Under the label's text; without a label, under the box. */
|
|
157
191
|
.hint {
|
|
@@ -159,7 +193,7 @@
|
|
|
159
193
|
color: var(--muted);
|
|
160
194
|
}
|
|
161
195
|
|
|
162
|
-
/* ---- Hover: fine pointers only, over
|
|
196
|
+
/* ---- Hover: fine pointers only, over any part of the row ----------- */
|
|
163
197
|
|
|
164
198
|
@media (hover: hover) and (pointer: fine) {
|
|
165
199
|
.box {
|
|
@@ -184,7 +218,7 @@
|
|
|
184
218
|
background-color: var(--surface-raised);
|
|
185
219
|
}
|
|
186
220
|
|
|
187
|
-
/* ---- Disabled: the quieter --border and muted ink
|
|
221
|
+
/* ---- Disabled: the quieter --border and muted ink, the tag too ----- */
|
|
188
222
|
|
|
189
223
|
.box:disabled {
|
|
190
224
|
border-color: var(--border);
|
|
@@ -195,4 +229,7 @@
|
|
|
195
229
|
color: var(--muted);
|
|
196
230
|
cursor: not-allowed;
|
|
197
231
|
}
|
|
232
|
+
.hp-checkbox:has(.box:disabled) label > :global(.hp-tag) {
|
|
233
|
+
background: var(--muted);
|
|
234
|
+
}
|
|
198
235
|
</style>
|
|
@@ -2,10 +2,17 @@ import { Checkbox as BitsCheckbox } from 'bits-ui';
|
|
|
2
2
|
/** Attributes and handlers not listed here go to the checkbox button. */
|
|
3
3
|
export type CheckboxProps = Omit<BitsCheckbox.RootProps, 'child' | 'children' | 'ref'> & {
|
|
4
4
|
/**
|
|
5
|
-
* The text beside the box, as written (never caps). Pressing it
|
|
6
|
-
* the box. Without it, give the box
|
|
5
|
+
* The text beside the box, as written (never caps). Pressing it, or
|
|
6
|
+
* anywhere on the box's row, toggles the box. Without it, give the box
|
|
7
|
+
* an accessible name with `aria-label`.
|
|
7
8
|
*/
|
|
8
9
|
label?: string;
|
|
10
|
+
/**
|
|
11
|
+
* A Tag after the label, such as "Recommended": a short fixed string the
|
|
12
|
+
* app defines, set in caps. It is part of the box's accessible name and
|
|
13
|
+
* shows only with a `label`.
|
|
14
|
+
*/
|
|
15
|
+
tag?: string;
|
|
9
16
|
/** A second line in `--muted` under the label, read out as the box's description. */
|
|
10
17
|
hint?: string;
|
|
11
18
|
/** The checkbox button. */
|