@mystravil/hardpoint 0.0.0-stage → 0.1.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 +18 -0
- package/LICENSE +25 -0
- package/README.md +64 -3
- package/dist/base.css +346 -0
- package/dist/components/AppShell.svelte +698 -0
- package/dist/components/AppShell.svelte.d.ts +77 -0
- package/dist/components/Button.svelte +174 -0
- package/dist/components/Button.svelte.d.ts +20 -0
- package/dist/components/Checkbox.svelte +198 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/CheckboxGroup.svelte +99 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
- package/dist/components/Combobox.svelte +824 -0
- package/dist/components/Combobox.svelte.d.ts +63 -0
- package/dist/components/Dialog.svelte +356 -0
- package/dist/components/Dialog.svelte.d.ts +43 -0
- package/dist/components/Disclosure.svelte +201 -0
- package/dist/components/Disclosure.svelte.d.ts +27 -0
- package/dist/components/EmptyState.svelte +88 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/ErrorState.svelte +135 -0
- package/dist/components/ErrorState.svelte.d.ts +26 -0
- package/dist/components/KeyValueList.svelte +114 -0
- package/dist/components/KeyValueList.svelte.d.ts +43 -0
- package/dist/components/LoadRegion.svelte +219 -0
- package/dist/components/LoadRegion.svelte.d.ts +27 -0
- package/dist/components/MediaFrame.svelte +110 -0
- package/dist/components/MediaFrame.svelte.d.ts +20 -0
- package/dist/components/MediaImage.svelte +256 -0
- package/dist/components/MediaImage.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +258 -0
- package/dist/components/Menu.svelte.d.ts +36 -0
- package/dist/components/MenuCheckboxItem.svelte +119 -0
- package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
- package/dist/components/MenuGroup.svelte +50 -0
- package/dist/components/MenuGroup.svelte.d.ts +15 -0
- package/dist/components/MenuItem.svelte +94 -0
- package/dist/components/MenuItem.svelte.d.ts +19 -0
- package/dist/components/MenuRadioGroup.svelte +62 -0
- package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/components/MenuRadioItem.svelte +110 -0
- package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
- package/dist/components/MenuSeparator.svelte +28 -0
- package/dist/components/MenuSeparator.svelte.d.ts +6 -0
- package/dist/components/NativeSelect.svelte +119 -0
- package/dist/components/NativeSelect.svelte.d.ts +46 -0
- package/dist/components/Pagination.svelte +132 -0
- package/dist/components/Pagination.svelte.d.ts +18 -0
- package/dist/components/Popover.svelte +222 -0
- package/dist/components/Popover.svelte.d.ts +35 -0
- package/dist/components/Progress.svelte +157 -0
- package/dist/components/Progress.svelte.d.ts +34 -0
- package/dist/components/Select.svelte +405 -0
- package/dist/components/Select.svelte.d.ts +42 -0
- package/dist/components/Skeleton.svelte +149 -0
- package/dist/components/Skeleton.svelte.d.ts +24 -0
- package/dist/components/Spinner.svelte +117 -0
- package/dist/components/Spinner.svelte.d.ts +13 -0
- package/dist/components/Stat.svelte +253 -0
- package/dist/components/Stat.svelte.d.ts +46 -0
- package/dist/components/Steps.svelte +189 -0
- package/dist/components/Steps.svelte.d.ts +31 -0
- package/dist/components/Switch.svelte +222 -0
- package/dist/components/Switch.svelte.d.ts +18 -0
- package/dist/components/Table.svelte +504 -0
- package/dist/components/Table.svelte.d.ts +92 -0
- package/dist/components/Tabs.svelte +246 -0
- package/dist/components/Tabs.svelte.d.ts +44 -0
- package/dist/components/Tag.svelte +40 -0
- package/dist/components/Tag.svelte.d.ts +15 -0
- package/dist/components/TextField.svelte +256 -0
- package/dist/components/TextField.svelte.d.ts +32 -0
- package/dist/components/Timeline.svelte +156 -0
- package/dist/components/Timeline.svelte.d.ts +30 -0
- package/dist/components/Toaster.svelte +411 -0
- package/dist/components/Toaster.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +200 -0
- package/dist/components/Tooltip.svelte.d.ts +38 -0
- package/dist/components/TooltipGroup.svelte +46 -0
- package/dist/components/TooltipGroup.svelte.d.ts +29 -0
- package/dist/components/internal/Scrim.svelte +52 -0
- package/dist/components/internal/Scrim.svelte.d.ts +15 -0
- package/dist/components/internal/Sheet.svelte +263 -0
- package/dist/components/internal/Sheet.svelte.d.ts +36 -0
- package/dist/components/internal/fade-in.d.ts +10 -0
- package/dist/components/internal/fade-in.js +20 -0
- package/dist/components/internal/resolve.d.ts +8 -0
- package/dist/components/internal/resolve.js +247 -0
- package/dist/components/internal/swipe-dismiss.d.ts +47 -0
- package/dist/components/internal/swipe-dismiss.js +220 -0
- package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
- package/dist/components/internal/visual-viewport.svelte.js +79 -0
- package/dist/components/toast.svelte.d.ts +57 -0
- package/dist/components/toast.svelte.js +84 -0
- package/dist/fonts/OFL-geist-pixel.txt +92 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/fonts/geist-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-pixel-square.woff2 +0 -0
- package/dist/fonts.css +71 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +39 -0
- package/dist/motion/Motion.svelte +15 -0
- package/dist/motion/Motion.svelte.d.ts +9 -0
- package/dist/motion/context.d.ts +18 -0
- package/dist/motion/context.js +26 -0
- package/dist/motion/index.d.ts +3 -0
- package/dist/motion/index.js +3 -0
- package/dist/motion/transitions.d.ts +31 -0
- package/dist/motion/transitions.js +112 -0
- package/dist/tokens.css +279 -0
- package/package.json +76 -6
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* One place the app's navigation leads to. The same list serves every
|
|
5
|
+
* navigation form: the bottom bar or the drawer on compact widths, the
|
|
6
|
+
* rail and the sidebar from 600px.
|
|
7
|
+
*/
|
|
8
|
+
export type AppShellDestination = {
|
|
9
|
+
/** Where the link goes. A plain `<a href>`, so any router handles it. */
|
|
10
|
+
href: string;
|
|
11
|
+
/**
|
|
12
|
+
* A short fixed label, set in caps. About six letters fit five across a
|
|
13
|
+
* 320px phone and the 80px rail; a longer label ends in an ellipsis.
|
|
14
|
+
*/
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* A glyph beside or above the label: an inline `<svg>` drawn in
|
|
18
|
+
* `currentColor` with `aria-hidden="true"`. It fills a 20px square, so
|
|
19
|
+
* give it a `viewBox` and no `width` or `height`.
|
|
20
|
+
*/
|
|
21
|
+
icon?: Snippet;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The navigation below 600px: a bottom bar of the destinations, or a
|
|
25
|
+
* drawer that a menu button at the left of the header opens.
|
|
26
|
+
*/
|
|
27
|
+
export type AppShellCompactNav = 'bar' | 'drawer';
|
|
28
|
+
/**
|
|
29
|
+
* The navigation from 900px: a sidebar of rows, or the rail that every
|
|
30
|
+
* width from 600 to 899px shows.
|
|
31
|
+
*/
|
|
32
|
+
export type AppShellWideNav = 'sidebar' | 'rail';
|
|
33
|
+
export type AppShellProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
|
|
34
|
+
/** The view title: a fixed string the app defines, set in caps in the header. */
|
|
35
|
+
title: string;
|
|
36
|
+
/**
|
|
37
|
+
* The left of the header: the app's wordmark, usually a link home. Give
|
|
38
|
+
* a link here at least `--hit-target` height.
|
|
39
|
+
*/
|
|
40
|
+
brand?: Snippet;
|
|
41
|
+
/**
|
|
42
|
+
* Page controls: filters or the row of page actions. At the right of the
|
|
43
|
+
* header row from 1200px; below that, their own row under the header.
|
|
44
|
+
*/
|
|
45
|
+
controls?: Snippet;
|
|
46
|
+
/**
|
|
47
|
+
* Three to five destinations: the bottom bar or the drawer below 600px,
|
|
48
|
+
* the rail from 600px, the sidebar or the rail from 900px.
|
|
49
|
+
*/
|
|
50
|
+
destinations?: AppShellDestination[];
|
|
51
|
+
/**
|
|
52
|
+
* The `href` of the current destination; it gets `aria-current="page"`.
|
|
53
|
+
* The app decides which destination a nested page belongs to.
|
|
54
|
+
*/
|
|
55
|
+
current?: string;
|
|
56
|
+
/** Below 600px: `bar` (default) or `drawer`. */
|
|
57
|
+
compactNav?: AppShellCompactNav;
|
|
58
|
+
/** From 900px: `sidebar` (default) or `rail`. */
|
|
59
|
+
wideNav?: AppShellWideNav;
|
|
60
|
+
/** Whether the drawer is open. It closes when the window reaches 600px. */
|
|
61
|
+
drawerOpen?: boolean;
|
|
62
|
+
/** The accessible name of the navigation. */
|
|
63
|
+
navLabel?: string;
|
|
64
|
+
/** The menu button's accessible name and the drawer's caps title. */
|
|
65
|
+
menuLabel?: string;
|
|
66
|
+
/** The accessible name of the drawer's close button. */
|
|
67
|
+
closeLabel?: string;
|
|
68
|
+
/** The id of `<main>`, the skip link's target. */
|
|
69
|
+
mainId?: string;
|
|
70
|
+
/** The skip link's text, shown on keyboard focus. */
|
|
71
|
+
skipLabel?: string;
|
|
72
|
+
/** The view's content, rendered inside `<main>`. */
|
|
73
|
+
children: Snippet;
|
|
74
|
+
};
|
|
75
|
+
declare const AppShell: import("svelte").Component<AppShellProps, {}, "drawerOpen">;
|
|
76
|
+
type AppShell = ReturnType<typeof AppShell>;
|
|
77
|
+
export default AppShell;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { Button as BitsButton } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
|
5
|
+
|
|
6
|
+
export type ButtonProps = BitsButton.RootProps & {
|
|
7
|
+
/**
|
|
8
|
+
* `primary`: solid titanium block, for the view's one group of solid
|
|
9
|
+
* blocks. `secondary` (default): outlined. `tertiary`: caps label only.
|
|
10
|
+
*/
|
|
11
|
+
variant?: ButtonVariant;
|
|
12
|
+
/** Ends the label in a small ▸: the button opens another view. */
|
|
13
|
+
opens?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A square, icon-only button (height and width are `--control-height`).
|
|
16
|
+
* It has no visible label, so it needs an accessible name: pass
|
|
17
|
+
* `aria-label`.
|
|
18
|
+
*/
|
|
19
|
+
icon?: boolean;
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
let {
|
|
25
|
+
variant = 'secondary',
|
|
26
|
+
opens = false,
|
|
27
|
+
icon = false,
|
|
28
|
+
class: className,
|
|
29
|
+
ref = $bindable(null),
|
|
30
|
+
children,
|
|
31
|
+
...rest
|
|
32
|
+
}: ButtonProps = $props();
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<!--
|
|
36
|
+
An <a> when `href` is set, a <button> otherwise (Bits UI Button.Root). A
|
|
37
|
+
disabled link renders without href, with aria-disabled="true" and
|
|
38
|
+
tabindex -1; a disabled <button> is not focusable.
|
|
39
|
+
-->
|
|
40
|
+
<BitsButton.Root
|
|
41
|
+
bind:ref
|
|
42
|
+
class={['hp-button', 'hp-label', className]}
|
|
43
|
+
data-variant={variant}
|
|
44
|
+
data-opens={opens || undefined}
|
|
45
|
+
data-icon={icon || undefined}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
{@render children?.()}
|
|
49
|
+
</BitsButton.Root>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
/* Bits UI renders the element, so the root class reaches it from the
|
|
53
|
+
outside: the rules are :global, keyed on .hp-button and its data-*. */
|
|
54
|
+
:global(.hp-button) {
|
|
55
|
+
display: inline-flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: var(--space-4);
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
height: var(--control-height);
|
|
61
|
+
/* The visible box is the hit area: 36px fine, 44px coarse, and never
|
|
62
|
+
narrower than --hit-target for a short label. */
|
|
63
|
+
min-width: var(--hit-target);
|
|
64
|
+
padding: 0 var(--space-3);
|
|
65
|
+
border: 0;
|
|
66
|
+
background: transparent;
|
|
67
|
+
color: var(--text);
|
|
68
|
+
font-family: var(--sans);
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
-webkit-tap-highlight-color: transparent;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---- Variants ---------------------------------------------------- */
|
|
76
|
+
|
|
77
|
+
/* Solid titanium block with a black label. */
|
|
78
|
+
:global(.hp-button[data-variant='primary']) {
|
|
79
|
+
background: var(--accent);
|
|
80
|
+
color: var(--canvas);
|
|
81
|
+
}
|
|
82
|
+
/* Outlined: a 3:1 boundary, titanium label, black fill. */
|
|
83
|
+
:global(.hp-button[data-variant='secondary']) {
|
|
84
|
+
border: 1px solid var(--border-control);
|
|
85
|
+
background: var(--canvas);
|
|
86
|
+
}
|
|
87
|
+
/* The label alone. */
|
|
88
|
+
:global(.hp-button[data-variant='tertiary']) {
|
|
89
|
+
background: transparent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---- Opens another view: a small ▸ drawn in CSS ------------------ */
|
|
93
|
+
|
|
94
|
+
:global(.hp-button[data-opens]::after) {
|
|
95
|
+
content: '';
|
|
96
|
+
flex: none;
|
|
97
|
+
width: 6px;
|
|
98
|
+
height: 8px;
|
|
99
|
+
background: currentColor;
|
|
100
|
+
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ---- Icon only: a square ----------------------------------------- */
|
|
104
|
+
|
|
105
|
+
:global(.hp-button[data-icon]) {
|
|
106
|
+
justify-content: center;
|
|
107
|
+
width: var(--control-height);
|
|
108
|
+
padding: 0;
|
|
109
|
+
}
|
|
110
|
+
/* The icon is the consumer's inline <svg>; it draws in currentColor. */
|
|
111
|
+
:global(.hp-button svg) {
|
|
112
|
+
flex: none;
|
|
113
|
+
}
|
|
114
|
+
:global(.hp-button[data-icon] svg) {
|
|
115
|
+
width: var(--space-4);
|
|
116
|
+
height: var(--space-4);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ---- Hover: fine pointers only ----------------------------------- */
|
|
120
|
+
|
|
121
|
+
@media (hover: hover) and (pointer: fine) {
|
|
122
|
+
:global(.hp-button) {
|
|
123
|
+
transition:
|
|
124
|
+
background-color var(--dur-hover) var(--settle),
|
|
125
|
+
color var(--dur-hover) var(--settle);
|
|
126
|
+
}
|
|
127
|
+
:global(.hp-button:active:not(:disabled):not([aria-disabled='true'])) {
|
|
128
|
+
transition-duration: var(--dur-press);
|
|
129
|
+
}
|
|
130
|
+
:global(.hp-button[data-variant='primary']:hover:not(:disabled):not([aria-disabled='true'])) {
|
|
131
|
+
background: var(--muted);
|
|
132
|
+
}
|
|
133
|
+
:global(.hp-button[data-variant='secondary']:hover:not(:disabled):not([aria-disabled='true'])),
|
|
134
|
+
:global(.hp-button[data-variant='tertiary']:hover:not(:disabled):not([aria-disabled='true'])) {
|
|
135
|
+
background: var(--surface-hover);
|
|
136
|
+
}
|
|
137
|
+
/* The tint alone barely shows (1.03:1), so the label also underlines. */
|
|
138
|
+
:global(.hp-button[data-variant='tertiary']:hover:not(:disabled):not([aria-disabled='true'])) {
|
|
139
|
+
text-decoration: underline;
|
|
140
|
+
text-decoration-thickness: 1px;
|
|
141
|
+
text-underline-offset: 0.3em;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ---- Press: after hover, so it wins while the pointer is down ----- */
|
|
146
|
+
|
|
147
|
+
:global(.hp-button[data-variant='primary']:active:not(:disabled):not([aria-disabled='true'])) {
|
|
148
|
+
background: var(--muted);
|
|
149
|
+
}
|
|
150
|
+
/* One step past the hover tint, so a press reads on a phone too. */
|
|
151
|
+
:global(.hp-button[data-variant='secondary']:active:not(:disabled):not([aria-disabled='true'])),
|
|
152
|
+
:global(.hp-button[data-variant='tertiary']:active:not(:disabled):not([aria-disabled='true'])) {
|
|
153
|
+
background: var(--surface-raised);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---- Disabled: muted ink. A <button> is not focusable; a link has
|
|
157
|
+
no href. -------------------------------------------------------- */
|
|
158
|
+
|
|
159
|
+
:global(.hp-button:disabled),
|
|
160
|
+
:global(.hp-button[aria-disabled='true']) {
|
|
161
|
+
color: var(--muted);
|
|
162
|
+
cursor: not-allowed;
|
|
163
|
+
}
|
|
164
|
+
:global(.hp-button[data-variant='primary']:disabled),
|
|
165
|
+
:global(.hp-button[data-variant='primary'][aria-disabled='true']) {
|
|
166
|
+
/* --muted is 4.9:1 on the band. */
|
|
167
|
+
background: var(--surface-band);
|
|
168
|
+
}
|
|
169
|
+
:global(.hp-button[data-variant='secondary']:disabled),
|
|
170
|
+
:global(.hp-button[data-variant='secondary'][aria-disabled='true']) {
|
|
171
|
+
/* An off control needs no 3:1 boundary. */
|
|
172
|
+
border-color: var(--border);
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Button as BitsButton } from 'bits-ui';
|
|
2
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
|
|
3
|
+
export type ButtonProps = BitsButton.RootProps & {
|
|
4
|
+
/**
|
|
5
|
+
* `primary`: solid titanium block, for the view's one group of solid
|
|
6
|
+
* blocks. `secondary` (default): outlined. `tertiary`: caps label only.
|
|
7
|
+
*/
|
|
8
|
+
variant?: ButtonVariant;
|
|
9
|
+
/** Ends the label in a small ▸: the button opens another view. */
|
|
10
|
+
opens?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* A square, icon-only button (height and width are `--control-height`).
|
|
13
|
+
* It has no visible label, so it needs an accessible name: pass
|
|
14
|
+
* `aria-label`.
|
|
15
|
+
*/
|
|
16
|
+
icon?: boolean;
|
|
17
|
+
};
|
|
18
|
+
declare const Button: import("svelte").Component<ButtonProps, {}, "ref">;
|
|
19
|
+
type Button = ReturnType<typeof Button>;
|
|
20
|
+
export default Button;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { Checkbox as BitsCheckbox } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
/** Attributes and handlers not listed here go to the checkbox button. */
|
|
5
|
+
export type CheckboxProps = Omit<BitsCheckbox.RootProps, 'child' | 'children' | 'ref'> & {
|
|
6
|
+
/**
|
|
7
|
+
* The text beside the box, as written (never caps). Pressing it toggles
|
|
8
|
+
* the box. Without it, give the box an accessible name with `aria-label`.
|
|
9
|
+
*/
|
|
10
|
+
label?: string;
|
|
11
|
+
/** A second line in `--muted` under the label, read out as the box's description. */
|
|
12
|
+
hint?: string;
|
|
13
|
+
/** The checkbox button. */
|
|
14
|
+
ref?: HTMLElement | null;
|
|
15
|
+
};
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
const uid = $props.id();
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
checked = $bindable(false),
|
|
23
|
+
indeterminate = $bindable(false),
|
|
24
|
+
label,
|
|
25
|
+
hint,
|
|
26
|
+
id = uid,
|
|
27
|
+
ref = $bindable(null),
|
|
28
|
+
class: className,
|
|
29
|
+
'aria-describedby': describedBy,
|
|
30
|
+
...rest
|
|
31
|
+
}: CheckboxProps = $props();
|
|
32
|
+
|
|
33
|
+
const hintId = $derived(`${id}-hint`);
|
|
34
|
+
const describedByIds = $derived([hint ? hintId : null, describedBy].filter(Boolean).join(' ') || undefined);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<!--
|
|
38
|
+
A Bits UI Checkbox: a <button role="checkbox"> with aria-checked true,
|
|
39
|
+
false or mixed, and its state in data-state (checked, unchecked,
|
|
40
|
+
indeterminate) and data-disabled. Pressing the box or its label toggles it;
|
|
41
|
+
an indeterminate box becomes checked. Inside a CheckboxGroup, give it a
|
|
42
|
+
`value`. Attributes, handlers and `class` go to the button.
|
|
43
|
+
-->
|
|
44
|
+
<div class="hp-checkbox">
|
|
45
|
+
<span class="control">
|
|
46
|
+
<BitsCheckbox.Root bind:checked bind:indeterminate bind:ref {id} aria-describedby={describedByIds} {...rest}>
|
|
47
|
+
{#snippet child({ props })}
|
|
48
|
+
<button {...props} class={['box', className]}>
|
|
49
|
+
<svg class="mark mixed" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 6h8" /></svg>
|
|
50
|
+
<svg class="mark check" viewBox="0 0 12 12" aria-hidden="true"><path d="M1.5 6.5l3 3 6-6" /></svg>
|
|
51
|
+
</button>
|
|
52
|
+
{/snippet}
|
|
53
|
+
</BitsCheckbox.Root>
|
|
54
|
+
</span>
|
|
55
|
+
{#if label}
|
|
56
|
+
<label class="hp-dense" for={id}>{label}</label>
|
|
57
|
+
{/if}
|
|
58
|
+
{#if hint}
|
|
59
|
+
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
/* The box, then the label as written. The first line is one hit-target
|
|
65
|
+
band (24px fine, 44px coarse): the box sits centred in it and the
|
|
66
|
+
label's first line too, so a label that wraps keeps the box beside its
|
|
67
|
+
first line. The hint runs under the label, aligned with its text.
|
|
68
|
+
Without a label the root is as wide as the box. */
|
|
69
|
+
.hp-checkbox {
|
|
70
|
+
/* The label's line height, as .hp-dense sets it. */
|
|
71
|
+
--_line: 24px;
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: auto;
|
|
74
|
+
justify-content: start;
|
|
75
|
+
align-items: start;
|
|
76
|
+
}
|
|
77
|
+
.hp-checkbox:has(> label) {
|
|
78
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
79
|
+
column-gap: var(--space-3);
|
|
80
|
+
}
|
|
81
|
+
@media (pointer: fine) {
|
|
82
|
+
.hp-checkbox {
|
|
83
|
+
--_line: 20px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.control {
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
height: var(--hit-target);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* A black box with a 3:1 boundary. Checked or indeterminate, the edge and
|
|
95
|
+
the mark turn titanium; the box stays black, because selection is never
|
|
96
|
+
a fill. Focus is base.css's contour 4px outside, so it never looks like
|
|
97
|
+
the checked edge. */
|
|
98
|
+
.box {
|
|
99
|
+
position: relative;
|
|
100
|
+
display: grid;
|
|
101
|
+
place-items: center;
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
width: 20px;
|
|
104
|
+
height: 20px;
|
|
105
|
+
margin: 0;
|
|
106
|
+
padding: 0;
|
|
107
|
+
border: 1px solid var(--border-control);
|
|
108
|
+
background: var(--canvas);
|
|
109
|
+
color: var(--accent);
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
-webkit-tap-highlight-color: transparent;
|
|
112
|
+
}
|
|
113
|
+
/* The hit area: at least --hit-target square, centred on the box. */
|
|
114
|
+
.box::before {
|
|
115
|
+
content: '';
|
|
116
|
+
position: absolute;
|
|
117
|
+
top: 50%;
|
|
118
|
+
left: 50%;
|
|
119
|
+
width: max(100%, var(--hit-target));
|
|
120
|
+
height: max(100%, var(--hit-target));
|
|
121
|
+
translate: -50% -50%;
|
|
122
|
+
}
|
|
123
|
+
.box[data-state='checked'],
|
|
124
|
+
.box[data-state='indeterminate'] {
|
|
125
|
+
border-color: var(--accent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Both marks are always there, each at opacity 0 until its state comes.
|
|
129
|
+
The mark a change brings fades in from 0 at its place over --dur-mark
|
|
130
|
+
(BRIEF, short motion); the mark it takes away goes at once. A CSS
|
|
131
|
+
transition never runs on an element's first style, so a box that
|
|
132
|
+
renders checked or mixed shows its mark at once. 0ms is a snap.
|
|
133
|
+
Opacity only: nothing moves, so reduced motion keeps it. */
|
|
134
|
+
.mark {
|
|
135
|
+
grid-area: 1 / 1;
|
|
136
|
+
width: 12px;
|
|
137
|
+
height: 12px;
|
|
138
|
+
fill: none;
|
|
139
|
+
stroke: currentColor;
|
|
140
|
+
stroke-width: 2px;
|
|
141
|
+
opacity: 0;
|
|
142
|
+
}
|
|
143
|
+
.box[data-state='indeterminate'] .mixed,
|
|
144
|
+
.box[data-state='checked'] .check {
|
|
145
|
+
opacity: 1;
|
|
146
|
+
transition: opacity var(--dur-mark) var(--settle);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
label {
|
|
150
|
+
justify-self: start;
|
|
151
|
+
padding: calc((var(--hit-target) - var(--_line)) / 2) 0;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
-webkit-tap-highlight-color: transparent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Under the label's text; without a label, under the box. */
|
|
157
|
+
.hint {
|
|
158
|
+
grid-column: -2;
|
|
159
|
+
color: var(--muted);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ---- Hover: fine pointers only, over the box or its label ---------- */
|
|
163
|
+
|
|
164
|
+
@media (hover: hover) and (pointer: fine) {
|
|
165
|
+
.box {
|
|
166
|
+
transition:
|
|
167
|
+
background-color var(--dur-hover) var(--settle),
|
|
168
|
+
border-color var(--dur-hover) var(--settle);
|
|
169
|
+
}
|
|
170
|
+
.hp-checkbox:has(> label:hover, .box:hover) .box:not(:disabled) {
|
|
171
|
+
background-color: var(--surface-hover);
|
|
172
|
+
}
|
|
173
|
+
.hp-checkbox:has(> label:hover, .box:hover) .box[data-state='unchecked']:not(:disabled) {
|
|
174
|
+
border-color: var(--muted);
|
|
175
|
+
}
|
|
176
|
+
.hp-checkbox:has(> label:active, .box:active) .box:not(:disabled) {
|
|
177
|
+
transition-duration: var(--dur-press);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ---- Press: one step past hover, so it reads on a phone too -------- */
|
|
182
|
+
|
|
183
|
+
.hp-checkbox:has(> label:active, .box:active) .box:not(:disabled) {
|
|
184
|
+
background-color: var(--surface-raised);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ---- Disabled: the quieter --border and muted ink ------------------ */
|
|
188
|
+
|
|
189
|
+
.box:disabled {
|
|
190
|
+
border-color: var(--border);
|
|
191
|
+
color: var(--muted);
|
|
192
|
+
cursor: not-allowed;
|
|
193
|
+
}
|
|
194
|
+
.hp-checkbox:has(.box:disabled) label {
|
|
195
|
+
color: var(--muted);
|
|
196
|
+
cursor: not-allowed;
|
|
197
|
+
}
|
|
198
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Checkbox as BitsCheckbox } from 'bits-ui';
|
|
2
|
+
/** Attributes and handlers not listed here go to the checkbox button. */
|
|
3
|
+
export type CheckboxProps = Omit<BitsCheckbox.RootProps, 'child' | 'children' | 'ref'> & {
|
|
4
|
+
/**
|
|
5
|
+
* The text beside the box, as written (never caps). Pressing it toggles
|
|
6
|
+
* the box. Without it, give the box an accessible name with `aria-label`.
|
|
7
|
+
*/
|
|
8
|
+
label?: string;
|
|
9
|
+
/** A second line in `--muted` under the label, read out as the box's description. */
|
|
10
|
+
hint?: string;
|
|
11
|
+
/** The checkbox button. */
|
|
12
|
+
ref?: HTMLElement | null;
|
|
13
|
+
};
|
|
14
|
+
declare const Checkbox: import("svelte").Component<CheckboxProps, {}, "ref" | "checked" | "indeterminate">;
|
|
15
|
+
type Checkbox = ReturnType<typeof Checkbox>;
|
|
16
|
+
export default Checkbox;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
|
4
|
+
|
|
5
|
+
export type CheckboxGroupItem = {
|
|
6
|
+
value: string;
|
|
7
|
+
/** The text beside the box, as written. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** A second line in `--muted` under the label. */
|
|
10
|
+
hint?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Attributes and handlers not listed here go to the group element. */
|
|
15
|
+
export type CheckboxGroupProps = Omit<CheckboxPrimitive.GroupProps, 'child' | 'children' | 'ref'> & {
|
|
16
|
+
/** The caps label that names the group. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** A line in `--muted` under the label, read out as the group's description. */
|
|
19
|
+
hint?: string;
|
|
20
|
+
/** The choices. Without it, pass `Checkbox` components with a `value` as children. */
|
|
21
|
+
items?: CheckboxGroupItem[];
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
/** The group element. */
|
|
24
|
+
ref?: HTMLElement | null;
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import { Checkbox as BitsCheckbox } from 'bits-ui';
|
|
30
|
+
import Checkbox from './Checkbox.svelte';
|
|
31
|
+
|
|
32
|
+
const uid = $props.id();
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
value = $bindable([]),
|
|
36
|
+
label,
|
|
37
|
+
hint,
|
|
38
|
+
items,
|
|
39
|
+
id = uid,
|
|
40
|
+
ref = $bindable(null),
|
|
41
|
+
class: className,
|
|
42
|
+
'aria-describedby': describedBy,
|
|
43
|
+
children,
|
|
44
|
+
...rest
|
|
45
|
+
}: CheckboxGroupProps = $props();
|
|
46
|
+
|
|
47
|
+
const hintId = $derived(`${id}-hint`);
|
|
48
|
+
const describedByIds = $derived([hint ? hintId : null, describedBy].filter(Boolean).join(' ') || undefined);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<!--
|
|
52
|
+
Several independent choices under one caps label: a Bits UI Checkbox.Group,
|
|
53
|
+
a role="group" element named by the label. `bind:value` holds the values of
|
|
54
|
+
the checked boxes. `disabled`, `required`, `readonly` and `name` apply to
|
|
55
|
+
every box. Attributes, handlers and `class` go to the group element.
|
|
56
|
+
-->
|
|
57
|
+
<BitsCheckbox.Group bind:value bind:ref {id} aria-describedby={describedByIds} {...rest}>
|
|
58
|
+
{#snippet child({ props })}
|
|
59
|
+
<div {...props} class={['hp-checkbox-group', className]}>
|
|
60
|
+
<BitsCheckbox.GroupLabel>
|
|
61
|
+
{#snippet child({ props: labelProps })}
|
|
62
|
+
<span {...labelProps} class="hp-label group-label">{label}</span>
|
|
63
|
+
{/snippet}
|
|
64
|
+
</BitsCheckbox.GroupLabel>
|
|
65
|
+
{#if hint}
|
|
66
|
+
<p class="hint hp-caption" id={hintId}>{hint}</p>
|
|
67
|
+
{/if}
|
|
68
|
+
<div class="items">
|
|
69
|
+
{#if items}
|
|
70
|
+
{#each items as item (item.value)}
|
|
71
|
+
<Checkbox value={item.value} label={item.label} hint={item.hint} disabled={item.disabled} />
|
|
72
|
+
{/each}
|
|
73
|
+
{:else}
|
|
74
|
+
{@render children?.()}
|
|
75
|
+
{/if}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{/snippet}
|
|
79
|
+
</BitsCheckbox.Group>
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
/* The caps label, its hint, then the boxes, 8px apart so neighbouring
|
|
83
|
+
targets never touch. Rows keep their own height when a layout makes
|
|
84
|
+
the group taller. */
|
|
85
|
+
.hp-checkbox-group {
|
|
86
|
+
display: grid;
|
|
87
|
+
align-content: start;
|
|
88
|
+
gap: var(--space-1);
|
|
89
|
+
min-width: 0;
|
|
90
|
+
}
|
|
91
|
+
.group-label,
|
|
92
|
+
.hint {
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
}
|
|
95
|
+
.items {
|
|
96
|
+
display: grid;
|
|
97
|
+
gap: var(--space-2);
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
|
3
|
+
export type CheckboxGroupItem = {
|
|
4
|
+
value: string;
|
|
5
|
+
/** The text beside the box, as written. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** A second line in `--muted` under the label. */
|
|
8
|
+
hint?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Attributes and handlers not listed here go to the group element. */
|
|
12
|
+
export type CheckboxGroupProps = Omit<CheckboxPrimitive.GroupProps, 'child' | 'children' | 'ref'> & {
|
|
13
|
+
/** The caps label that names the group. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** A line in `--muted` under the label, read out as the group's description. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
/** The choices. Without it, pass `Checkbox` components with a `value` as children. */
|
|
18
|
+
items?: CheckboxGroupItem[];
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
/** The group element. */
|
|
21
|
+
ref?: HTMLElement | null;
|
|
22
|
+
};
|
|
23
|
+
declare const CheckboxGroup: import("svelte").Component<CheckboxGroupProps, {}, "value" | "ref">;
|
|
24
|
+
type CheckboxGroup = ReturnType<typeof CheckboxGroup>;
|
|
25
|
+
export default CheckboxGroup;
|