@mystravil/hardpoint 0.1.0 → 0.2.1
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 +42 -18
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -346
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -198
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +207 -99
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +901 -824
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +359 -356
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +18 -2
- 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 +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -405
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -117
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/fonts.css +71 -71
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +283 -279
- package/package.json +81 -76
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Attributes and handlers not listed here go to the item element (Bits UI
|
|
7
|
-
* `DropdownMenu.RadioItem`), among them `disabled`, `onSelect` and
|
|
8
|
-
* `closeOnSelect` (default true; pass false to keep the menu open, so the
|
|
9
|
-
* change shows). Place it inside a MenuRadioGroup.
|
|
10
|
-
*/
|
|
11
|
-
export type MenuRadioItemProps = Omit<BitsDropdownMenu.RadioItemProps, 'child' | 'children'> & {
|
|
12
|
-
/** Chosen when the group's `value` equals it. */
|
|
13
|
-
value: string;
|
|
14
|
-
/** The label, as written (never caps). */
|
|
15
|
-
children: Snippet;
|
|
16
|
-
};
|
|
17
|
-
</script>
|
|
18
|
-
|
|
19
|
-
<script lang="ts">
|
|
20
|
-
import { DropdownMenu } from 'bits-ui';
|
|
21
|
-
|
|
22
|
-
let { ref = $bindable(null), class: className, children, ...rest }: MenuRadioItemProps = $props();
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<!--
|
|
26
|
-
A Bits UI radio item, role="menuitemradio", with aria-checked and
|
|
27
|
-
data-state (checked, unchecked). A press, Enter or Space chooses it.
|
|
28
|
-
-->
|
|
29
|
-
<DropdownMenu.RadioItem bind:ref {...rest}>
|
|
30
|
-
{#snippet child({ props })}
|
|
31
|
-
<div {...props} class={['hp-menu-radio-item', className]}>
|
|
32
|
-
<span class="hp-menu-box" aria-hidden="true">
|
|
33
|
-
<span class="hp-menu-radio-mark"></span>
|
|
34
|
-
</span>
|
|
35
|
-
<span class="hp-menu-item-label">{@render children()}</span>
|
|
36
|
-
</div>
|
|
37
|
-
{/snippet}
|
|
38
|
-
</DropdownMenu.RadioItem>
|
|
39
|
-
|
|
40
|
-
<style>
|
|
41
|
-
/* The row: the same as MenuItem's. Only the mark fades (below). */
|
|
42
|
-
.hp-menu-radio-item {
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
gap: var(--space-3);
|
|
46
|
-
box-sizing: border-box;
|
|
47
|
-
min-height: var(--control-height);
|
|
48
|
-
padding: var(--space-1) var(--menu-inset, var(--space-3));
|
|
49
|
-
font: var(--type-body) / 24px var(--sans);
|
|
50
|
-
overflow-wrap: anywhere;
|
|
51
|
-
cursor: pointer;
|
|
52
|
-
user-select: none;
|
|
53
|
-
touch-action: manipulation;
|
|
54
|
-
-webkit-tap-highlight-color: transparent;
|
|
55
|
-
outline: none;
|
|
56
|
-
}
|
|
57
|
-
.hp-menu-item-label {
|
|
58
|
-
flex: 1 1 auto;
|
|
59
|
-
min-width: 0;
|
|
60
|
-
}
|
|
61
|
-
.hp-menu-radio-item[data-highlighted] {
|
|
62
|
-
background: var(--surface-hover);
|
|
63
|
-
outline: var(--menu-contour, none);
|
|
64
|
-
outline-offset: -2px;
|
|
65
|
-
}
|
|
66
|
-
.hp-menu-radio-item:active:not([data-disabled]) {
|
|
67
|
-
background: var(--surface-raised);
|
|
68
|
-
}
|
|
69
|
-
.hp-menu-radio-item[data-disabled] {
|
|
70
|
-
color: var(--muted);
|
|
71
|
-
cursor: not-allowed;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* The checkbox item's black box; chosen, a titanium edge and a solid
|
|
75
|
-
8px titanium square, square like every other mark. */
|
|
76
|
-
.hp-menu-box {
|
|
77
|
-
flex: none;
|
|
78
|
-
display: grid;
|
|
79
|
-
place-items: center;
|
|
80
|
-
box-sizing: border-box;
|
|
81
|
-
width: 20px;
|
|
82
|
-
height: 20px;
|
|
83
|
-
border: 1px solid var(--border-control);
|
|
84
|
-
background: var(--canvas);
|
|
85
|
-
color: var(--accent);
|
|
86
|
-
}
|
|
87
|
-
.hp-menu-radio-item[data-state='checked'] .hp-menu-box {
|
|
88
|
-
border-color: var(--accent);
|
|
89
|
-
}
|
|
90
|
-
.hp-menu-radio-item[data-disabled] .hp-menu-box {
|
|
91
|
-
border-color: var(--border);
|
|
92
|
-
color: var(--muted);
|
|
93
|
-
}
|
|
94
|
-
/* The mark is always there, at opacity 0 until the item is chosen.
|
|
95
|
-
Choosing fades it in from 0 over --dur-mark (BRIEF, short motion);
|
|
96
|
-
the item that loses the choice drops its mark at once. A CSS
|
|
97
|
-
transition never runs on an element's first style, so a menu that
|
|
98
|
-
opens shows its mark at once. 0ms is a snap. Opacity only, so reduced
|
|
99
|
-
motion keeps it. */
|
|
100
|
-
.hp-menu-radio-mark {
|
|
101
|
-
width: 8px;
|
|
102
|
-
height: 8px;
|
|
103
|
-
background: currentColor;
|
|
104
|
-
opacity: 0;
|
|
105
|
-
}
|
|
106
|
-
.hp-menu-radio-item[data-state='checked'] .hp-menu-radio-mark {
|
|
107
|
-
opacity: 1;
|
|
108
|
-
transition: opacity var(--dur-mark) var(--settle);
|
|
109
|
-
}
|
|
110
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attributes and handlers not listed here go to the item element (Bits UI
|
|
7
|
+
* `DropdownMenu.RadioItem`), among them `disabled`, `onSelect` and
|
|
8
|
+
* `closeOnSelect` (default true; pass false to keep the menu open, so the
|
|
9
|
+
* change shows). Place it inside a MenuRadioGroup.
|
|
10
|
+
*/
|
|
11
|
+
export type MenuRadioItemProps = Omit<BitsDropdownMenu.RadioItemProps, 'child' | 'children'> & {
|
|
12
|
+
/** Chosen when the group's `value` equals it. */
|
|
13
|
+
value: string;
|
|
14
|
+
/** The label, as written (never caps). */
|
|
15
|
+
children: Snippet;
|
|
16
|
+
};
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { DropdownMenu } from 'bits-ui';
|
|
21
|
+
|
|
22
|
+
let { ref = $bindable(null), class: className, children, ...rest }: MenuRadioItemProps = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!--
|
|
26
|
+
A Bits UI radio item, role="menuitemradio", with aria-checked and
|
|
27
|
+
data-state (checked, unchecked). A press, Enter or Space chooses it.
|
|
28
|
+
-->
|
|
29
|
+
<DropdownMenu.RadioItem bind:ref {...rest}>
|
|
30
|
+
{#snippet child({ props })}
|
|
31
|
+
<div {...props} class={['hp-menu-radio-item', className]}>
|
|
32
|
+
<span class="hp-menu-box" aria-hidden="true">
|
|
33
|
+
<span class="hp-menu-radio-mark"></span>
|
|
34
|
+
</span>
|
|
35
|
+
<span class="hp-menu-item-label">{@render children()}</span>
|
|
36
|
+
</div>
|
|
37
|
+
{/snippet}
|
|
38
|
+
</DropdownMenu.RadioItem>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
/* The row: the same as MenuItem's. Only the mark fades (below). */
|
|
42
|
+
.hp-menu-radio-item {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: var(--space-3);
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
min-height: var(--control-height);
|
|
48
|
+
padding: var(--space-1) var(--menu-inset, var(--space-3));
|
|
49
|
+
font: var(--type-body) / 24px var(--sans);
|
|
50
|
+
overflow-wrap: anywhere;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
user-select: none;
|
|
53
|
+
touch-action: manipulation;
|
|
54
|
+
-webkit-tap-highlight-color: transparent;
|
|
55
|
+
outline: none;
|
|
56
|
+
}
|
|
57
|
+
.hp-menu-item-label {
|
|
58
|
+
flex: 1 1 auto;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
}
|
|
61
|
+
.hp-menu-radio-item[data-highlighted] {
|
|
62
|
+
background: var(--surface-hover);
|
|
63
|
+
outline: var(--menu-contour, none);
|
|
64
|
+
outline-offset: -2px;
|
|
65
|
+
}
|
|
66
|
+
.hp-menu-radio-item:active:not([data-disabled]) {
|
|
67
|
+
background: var(--surface-raised);
|
|
68
|
+
}
|
|
69
|
+
.hp-menu-radio-item[data-disabled] {
|
|
70
|
+
color: var(--muted);
|
|
71
|
+
cursor: not-allowed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The checkbox item's black box; chosen, a titanium edge and a solid
|
|
75
|
+
8px titanium square, square like every other mark. */
|
|
76
|
+
.hp-menu-box {
|
|
77
|
+
flex: none;
|
|
78
|
+
display: grid;
|
|
79
|
+
place-items: center;
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
width: 20px;
|
|
82
|
+
height: 20px;
|
|
83
|
+
border: 1px solid var(--border-control);
|
|
84
|
+
background: var(--canvas);
|
|
85
|
+
color: var(--accent);
|
|
86
|
+
}
|
|
87
|
+
.hp-menu-radio-item[data-state='checked'] .hp-menu-box {
|
|
88
|
+
border-color: var(--accent);
|
|
89
|
+
}
|
|
90
|
+
.hp-menu-radio-item[data-disabled] .hp-menu-box {
|
|
91
|
+
border-color: var(--border);
|
|
92
|
+
color: var(--muted);
|
|
93
|
+
}
|
|
94
|
+
/* The mark is always there, at opacity 0 until the item is chosen.
|
|
95
|
+
Choosing fades it in from 0 over --dur-mark (BRIEF, short motion);
|
|
96
|
+
the item that loses the choice drops its mark at once. A CSS
|
|
97
|
+
transition never runs on an element's first style, so a menu that
|
|
98
|
+
opens shows its mark at once. 0ms is a snap. Opacity only, so reduced
|
|
99
|
+
motion keeps it. */
|
|
100
|
+
.hp-menu-radio-mark {
|
|
101
|
+
width: 8px;
|
|
102
|
+
height: 8px;
|
|
103
|
+
background: currentColor;
|
|
104
|
+
opacity: 0;
|
|
105
|
+
}
|
|
106
|
+
.hp-menu-radio-item[data-state='checked'] .hp-menu-radio-mark {
|
|
107
|
+
opacity: 1;
|
|
108
|
+
transition: opacity var(--dur-mark) var(--settle);
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
|
|
3
|
-
|
|
4
|
-
/** Attributes go to the separator element (Bits UI `DropdownMenu.Separator`). */
|
|
5
|
-
export type MenuSeparatorProps = Omit<BitsDropdownMenu.SeparatorProps, 'child' | 'children'>;
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import { DropdownMenu } from 'bits-ui';
|
|
10
|
-
|
|
11
|
-
let { ref = $bindable(null), class: className, ...rest }: MenuSeparatorProps = $props();
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<!-- A Bits UI separator, role="separator", between groups of items. -->
|
|
15
|
-
<DropdownMenu.Separator bind:ref {...rest}>
|
|
16
|
-
{#snippet child({ props })}
|
|
17
|
-
<div {...props} class={['hp-menu-separator', className]}></div>
|
|
18
|
-
{/snippet}
|
|
19
|
-
</DropdownMenu.Separator>
|
|
20
|
-
|
|
21
|
-
<style>
|
|
22
|
-
/* A 1px divider across the whole menu, in the decorative line colour. */
|
|
23
|
-
.hp-menu-separator {
|
|
24
|
-
height: 1px;
|
|
25
|
-
margin-block: var(--space-1);
|
|
26
|
-
background: var(--border);
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
/** Attributes go to the separator element (Bits UI `DropdownMenu.Separator`). */
|
|
5
|
+
export type MenuSeparatorProps = Omit<BitsDropdownMenu.SeparatorProps, 'child' | 'children'>;
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { DropdownMenu } from 'bits-ui';
|
|
10
|
+
|
|
11
|
+
let { ref = $bindable(null), class: className, ...rest }: MenuSeparatorProps = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- A Bits UI separator, role="separator", between groups of items. -->
|
|
15
|
+
<DropdownMenu.Separator bind:ref {...rest}>
|
|
16
|
+
{#snippet child({ props })}
|
|
17
|
+
<div {...props} class={['hp-menu-separator', className]}></div>
|
|
18
|
+
{/snippet}
|
|
19
|
+
</DropdownMenu.Separator>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
/* A 1px divider across the whole menu, in the decorative line colour. */
|
|
23
|
+
.hp-menu-separator {
|
|
24
|
+
height: 1px;
|
|
25
|
+
margin-block: var(--space-1);
|
|
26
|
+
background: var(--border);
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { Meter as BitsMeter } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
/** A status hue for the bar: `ok` fine, `warn` needs attention, `danger` bad. */
|
|
5
|
+
export type MeterTone = 'ok' | 'warn' | 'danger';
|
|
6
|
+
|
|
7
|
+
export type MeterProps = Omit<BitsMeter.RootProps, 'value' | 'child' | 'children'> & {
|
|
8
|
+
/**
|
|
9
|
+
* The measurement, from `min` to `max`. Pass the real figure each time
|
|
10
|
+
* it changes: the bar never moves on its own. A value outside the
|
|
11
|
+
* range is held at its ends for the bar and `aria-valuenow`. Work that
|
|
12
|
+
* is getting done shows a `Progress` instead.
|
|
13
|
+
*/
|
|
14
|
+
value: number;
|
|
15
|
+
/** The top of the range. Default 100. */
|
|
16
|
+
max?: number;
|
|
17
|
+
/** The bottom of the range. Default 0. */
|
|
18
|
+
min?: number;
|
|
19
|
+
/**
|
|
20
|
+
* How many segments the bar has (default 20, at least 2). A segment
|
|
21
|
+
* lights once the value has passed its whole share, so the bar never
|
|
22
|
+
* shows more than the value.
|
|
23
|
+
*/
|
|
24
|
+
segments?: number;
|
|
25
|
+
/**
|
|
26
|
+
* A line above the bar saying what is measured, as written (never
|
|
27
|
+
* caps: it may hold a name). It names the meter; without it, pass
|
|
28
|
+
* `aria-label` or `aria-labelledby`.
|
|
29
|
+
*/
|
|
30
|
+
label?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The value in words, such as `4.2 of 5 GB`. Shown at the end of the
|
|
33
|
+
* label line and read as `aria-valuetext`. Without it both give the
|
|
34
|
+
* share of the range, rounded down.
|
|
35
|
+
*/
|
|
36
|
+
valueText?: string;
|
|
37
|
+
/**
|
|
38
|
+
* The value at and above which the meter shows `warn`. Default 80% of
|
|
39
|
+
* the range. `null` turns the warn step off.
|
|
40
|
+
*/
|
|
41
|
+
warnAt?: number | null;
|
|
42
|
+
/**
|
|
43
|
+
* The value at and above which the meter shows `danger`. Default
|
|
44
|
+
* `max`. `null` turns the danger step off.
|
|
45
|
+
*/
|
|
46
|
+
dangerAt?: number | null;
|
|
47
|
+
/**
|
|
48
|
+
* Replaces the thresholds with a colour the app chooses. Use it where
|
|
49
|
+
* lower is worse, or for a state the value cannot show. Colour never
|
|
50
|
+
* carries meaning alone: say the reason in `valueText` or `detail`.
|
|
51
|
+
*/
|
|
52
|
+
tone?: MeterTone;
|
|
53
|
+
/**
|
|
54
|
+
* A reference value drawn as a thin mark across the bar: a target, an
|
|
55
|
+
* expected level, an earlier reading. Held inside the range. The mark
|
|
56
|
+
* is visual only and hidden from assistive technology, so say what it
|
|
57
|
+
* stands for in `detail`.
|
|
58
|
+
*/
|
|
59
|
+
marker?: number;
|
|
60
|
+
/**
|
|
61
|
+
* A muted line under the bar, as written. Assistive technology reads
|
|
62
|
+
* it as the meter's description; the app's own `aria-describedby`
|
|
63
|
+
* ids follow it.
|
|
64
|
+
*/
|
|
65
|
+
detail?: string;
|
|
66
|
+
};
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
let {
|
|
71
|
+
value,
|
|
72
|
+
max = 100,
|
|
73
|
+
min = 0,
|
|
74
|
+
segments = 20,
|
|
75
|
+
label,
|
|
76
|
+
valueText,
|
|
77
|
+
warnAt,
|
|
78
|
+
dangerAt,
|
|
79
|
+
tone,
|
|
80
|
+
marker,
|
|
81
|
+
detail,
|
|
82
|
+
id,
|
|
83
|
+
class: className,
|
|
84
|
+
ref = $bindable(null),
|
|
85
|
+
'aria-describedby': describedby,
|
|
86
|
+
...rest
|
|
87
|
+
}: MeterProps = $props();
|
|
88
|
+
|
|
89
|
+
const uid = $props.id();
|
|
90
|
+
const labelId = `${uid}-label`;
|
|
91
|
+
const detailId = `${uid}-detail`;
|
|
92
|
+
|
|
93
|
+
// The value inside its range, so the bar and aria-valuenow agree. An
|
|
94
|
+
// empty range counts as full.
|
|
95
|
+
const current = $derived(Math.min(max, Math.max(min, value)));
|
|
96
|
+
const span = $derived(max - min);
|
|
97
|
+
const count = $derived(Math.max(2, Math.floor(segments)));
|
|
98
|
+
// Round down: a segment lights only when its whole share is reached.
|
|
99
|
+
// Multiply before dividing and clear float noise before rounding down,
|
|
100
|
+
// so 58 of 100 and 0.58 of 1 are both 58% and not 57.99…
|
|
101
|
+
const share = (n: number) => Math.floor(Number((((current - min) * n) / span).toPrecision(12)));
|
|
102
|
+
const lit = $derived(current >= max || span <= 0 ? count : share(count));
|
|
103
|
+
const percent = $derived(span > 0 ? share(100) : 100);
|
|
104
|
+
const shown = $derived(valueText ?? `${percent}%`);
|
|
105
|
+
|
|
106
|
+
// Thresholds apply to the value inside the range. `undefined` takes the
|
|
107
|
+
// default; `null` turns the step off. 0.8 of the span is cleared of float
|
|
108
|
+
// noise so a value on the line counts.
|
|
109
|
+
const warnLine = $derived(
|
|
110
|
+
warnAt === undefined ? min + Number((0.8 * span).toPrecision(12)) : warnAt
|
|
111
|
+
);
|
|
112
|
+
const dangerLine = $derived(dangerAt === undefined ? max : dangerAt);
|
|
113
|
+
const shownTone = $derived<MeterTone | undefined>(
|
|
114
|
+
tone ??
|
|
115
|
+
(dangerLine !== null && current >= dangerLine
|
|
116
|
+
? 'danger'
|
|
117
|
+
: warnLine !== null && current >= warnLine
|
|
118
|
+
? 'warn'
|
|
119
|
+
: undefined)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// The marker's place across the bar, held inside the range.
|
|
123
|
+
const markerAt = $derived(
|
|
124
|
+
marker === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: span > 0
|
|
127
|
+
? Math.min(1, Math.max(0, (marker - min) / span)) * 100
|
|
128
|
+
: 0
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const describedBy = $derived([detail ? detailId : undefined, describedby].filter(Boolean).join(' ') || undefined);
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<!--
|
|
135
|
+
Bits UI Meter, rendered through its `child` snippet so the element is this
|
|
136
|
+
component's markup and its styles stay scoped. The root is the meter
|
|
137
|
+
(`role`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`); `id`, extra
|
|
138
|
+
attributes and `class` go to it. Its children are presentational to
|
|
139
|
+
assistive technology, so the label line names it through
|
|
140
|
+
`aria-labelledby`, the value as shown reaches it as `aria-valuetext`, and
|
|
141
|
+
the detail line describes it. The marker is a decoration.
|
|
142
|
+
A segment that lights, goes out or changes tone fades its colour over
|
|
143
|
+
--dur-segment; the marker moves at once.
|
|
144
|
+
-->
|
|
145
|
+
<BitsMeter.Root
|
|
146
|
+
bind:ref
|
|
147
|
+
value={current}
|
|
148
|
+
{max}
|
|
149
|
+
{min}
|
|
150
|
+
aria-labelledby={label ? labelId : undefined}
|
|
151
|
+
aria-valuetext={shown}
|
|
152
|
+
aria-describedby={describedBy}
|
|
153
|
+
{...rest}
|
|
154
|
+
>
|
|
155
|
+
{#snippet child({ props })}
|
|
156
|
+
<div
|
|
157
|
+
{...props}
|
|
158
|
+
{id}
|
|
159
|
+
class={['hp-meter', className]}
|
|
160
|
+
data-tone={shownTone}
|
|
161
|
+
data-lit={lit}
|
|
162
|
+
data-segments={count}
|
|
163
|
+
>
|
|
164
|
+
{#if label}
|
|
165
|
+
<div class="hp-meter-line">
|
|
166
|
+
<span id={labelId} class="hp-meter-label">{label}</span>
|
|
167
|
+
<span class="hp-meter-value">{shown}</span>
|
|
168
|
+
</div>
|
|
169
|
+
{/if}
|
|
170
|
+
<div class="hp-meter-bar" style:--segments={count}>
|
|
171
|
+
{#each { length: count }, i}
|
|
172
|
+
<span class="hp-meter-segment" data-lit={i < lit || undefined}></span>
|
|
173
|
+
{/each}
|
|
174
|
+
{#if markerAt !== undefined}
|
|
175
|
+
<span class="hp-meter-marker" aria-hidden="true" style:left="{markerAt}%"></span>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
{#if detail}
|
|
179
|
+
<p id={detailId} class="hp-meter-detail">{detail}</p>
|
|
180
|
+
{/if}
|
|
181
|
+
</div>
|
|
182
|
+
{/snippet}
|
|
183
|
+
</BitsMeter.Root>
|
|
184
|
+
|
|
185
|
+
<style>
|
|
186
|
+
/* The bar fills the width it is given; the app sets a narrower one. */
|
|
187
|
+
.hp-meter {
|
|
188
|
+
display: grid;
|
|
189
|
+
gap: var(--space-2);
|
|
190
|
+
min-width: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* 16px on touch, the dense 14px on fine pointers. The value keeps the
|
|
194
|
+
end of the line; a long label wraps rather than widen the page. */
|
|
195
|
+
.hp-meter-line {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: baseline;
|
|
198
|
+
justify-content: space-between;
|
|
199
|
+
gap: var(--space-4);
|
|
200
|
+
font-size: var(--type-body);
|
|
201
|
+
line-height: 24px;
|
|
202
|
+
}
|
|
203
|
+
@media (pointer: fine) {
|
|
204
|
+
.hp-meter-line {
|
|
205
|
+
font-size: var(--type-dense);
|
|
206
|
+
line-height: 20px;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
.hp-meter-label {
|
|
210
|
+
min-width: 0;
|
|
211
|
+
color: var(--text);
|
|
212
|
+
overflow-wrap: anywhere;
|
|
213
|
+
}
|
|
214
|
+
/* Muted with no tone; the tone's hue otherwise, so the words and the
|
|
215
|
+
bar agree. The value text is always there, so colour never carries the
|
|
216
|
+
meaning alone. */
|
|
217
|
+
.hp-meter-value {
|
|
218
|
+
flex: none;
|
|
219
|
+
color: var(--muted);
|
|
220
|
+
font-variant-numeric: tabular-nums;
|
|
221
|
+
}
|
|
222
|
+
.hp-meter[data-tone='ok'] .hp-meter-value {
|
|
223
|
+
color: var(--ok);
|
|
224
|
+
}
|
|
225
|
+
.hp-meter[data-tone='warn'] .hp-meter-value {
|
|
226
|
+
color: var(--warn);
|
|
227
|
+
}
|
|
228
|
+
.hp-meter[data-tone='danger'] .hp-meter-value {
|
|
229
|
+
color: var(--danger);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Separate segments as in Progress (BRIEF, visual rule 7): lit in the
|
|
233
|
+
tone's colour, the 3:1 control boundary while unlit. Square, 4px
|
|
234
|
+
apart, one row. */
|
|
235
|
+
.hp-meter-bar {
|
|
236
|
+
position: relative;
|
|
237
|
+
display: grid;
|
|
238
|
+
grid-template-columns: repeat(var(--segments), minmax(0, 1fr));
|
|
239
|
+
gap: var(--space-1);
|
|
240
|
+
height: 12px;
|
|
241
|
+
}
|
|
242
|
+
/* A segment that lights, goes out or changes tone fades its colour over
|
|
243
|
+
--dur-segment (BRIEF, short motion). A CSS transition never runs on an
|
|
244
|
+
element's first style, so a meter shows its value at once when it
|
|
245
|
+
renders. 0ms is a snap. Colour only, so reduced motion keeps it. */
|
|
246
|
+
.hp-meter-segment {
|
|
247
|
+
background: var(--border-control);
|
|
248
|
+
transition: background-color var(--dur-segment) var(--settle);
|
|
249
|
+
}
|
|
250
|
+
.hp-meter-segment[data-lit] {
|
|
251
|
+
background: var(--accent);
|
|
252
|
+
}
|
|
253
|
+
.hp-meter[data-tone='ok'] .hp-meter-segment[data-lit] {
|
|
254
|
+
background: var(--ok);
|
|
255
|
+
}
|
|
256
|
+
.hp-meter[data-tone='warn'] .hp-meter-segment[data-lit] {
|
|
257
|
+
background: var(--warn);
|
|
258
|
+
}
|
|
259
|
+
.hp-meter[data-tone='danger'] .hp-meter-segment[data-lit] {
|
|
260
|
+
background: var(--danger);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* A 2px line in the text colour, 4px past the bar at each end. A 1px
|
|
264
|
+
canvas edge on each side lets it read over lit segments of every tone,
|
|
265
|
+
unlit ones and the gaps, in both themes. It moves at once. */
|
|
266
|
+
.hp-meter-marker {
|
|
267
|
+
position: absolute;
|
|
268
|
+
top: calc(var(--space-1) * -1);
|
|
269
|
+
bottom: calc(var(--space-1) * -1);
|
|
270
|
+
width: 2px;
|
|
271
|
+
margin-left: -1px;
|
|
272
|
+
background: var(--text);
|
|
273
|
+
box-shadow: 0 0 0 1px var(--canvas);
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.hp-meter-detail {
|
|
278
|
+
margin: 0;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
font-size: var(--type-caption);
|
|
281
|
+
line-height: 18px;
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Meter as BitsMeter } from 'bits-ui';
|
|
2
|
+
/** A status hue for the bar: `ok` fine, `warn` needs attention, `danger` bad. */
|
|
3
|
+
export type MeterTone = 'ok' | 'warn' | 'danger';
|
|
4
|
+
export type MeterProps = Omit<BitsMeter.RootProps, 'value' | 'child' | 'children'> & {
|
|
5
|
+
/**
|
|
6
|
+
* The measurement, from `min` to `max`. Pass the real figure each time
|
|
7
|
+
* it changes: the bar never moves on its own. A value outside the
|
|
8
|
+
* range is held at its ends for the bar and `aria-valuenow`. Work that
|
|
9
|
+
* is getting done shows a `Progress` instead.
|
|
10
|
+
*/
|
|
11
|
+
value: number;
|
|
12
|
+
/** The top of the range. Default 100. */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** The bottom of the range. Default 0. */
|
|
15
|
+
min?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How many segments the bar has (default 20, at least 2). A segment
|
|
18
|
+
* lights once the value has passed its whole share, so the bar never
|
|
19
|
+
* shows more than the value.
|
|
20
|
+
*/
|
|
21
|
+
segments?: number;
|
|
22
|
+
/**
|
|
23
|
+
* A line above the bar saying what is measured, as written (never
|
|
24
|
+
* caps: it may hold a name). It names the meter; without it, pass
|
|
25
|
+
* `aria-label` or `aria-labelledby`.
|
|
26
|
+
*/
|
|
27
|
+
label?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The value in words, such as `4.2 of 5 GB`. Shown at the end of the
|
|
30
|
+
* label line and read as `aria-valuetext`. Without it both give the
|
|
31
|
+
* share of the range, rounded down.
|
|
32
|
+
*/
|
|
33
|
+
valueText?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The value at and above which the meter shows `warn`. Default 80% of
|
|
36
|
+
* the range. `null` turns the warn step off.
|
|
37
|
+
*/
|
|
38
|
+
warnAt?: number | null;
|
|
39
|
+
/**
|
|
40
|
+
* The value at and above which the meter shows `danger`. Default
|
|
41
|
+
* `max`. `null` turns the danger step off.
|
|
42
|
+
*/
|
|
43
|
+
dangerAt?: number | null;
|
|
44
|
+
/**
|
|
45
|
+
* Replaces the thresholds with a colour the app chooses. Use it where
|
|
46
|
+
* lower is worse, or for a state the value cannot show. Colour never
|
|
47
|
+
* carries meaning alone: say the reason in `valueText` or `detail`.
|
|
48
|
+
*/
|
|
49
|
+
tone?: MeterTone;
|
|
50
|
+
/**
|
|
51
|
+
* A reference value drawn as a thin mark across the bar: a target, an
|
|
52
|
+
* expected level, an earlier reading. Held inside the range. The mark
|
|
53
|
+
* is visual only and hidden from assistive technology, so say what it
|
|
54
|
+
* stands for in `detail`.
|
|
55
|
+
*/
|
|
56
|
+
marker?: number;
|
|
57
|
+
/**
|
|
58
|
+
* A muted line under the bar, as written. Assistive technology reads
|
|
59
|
+
* it as the meter's description; the app's own `aria-describedby`
|
|
60
|
+
* ids follow it.
|
|
61
|
+
*/
|
|
62
|
+
detail?: string;
|
|
63
|
+
};
|
|
64
|
+
declare const Meter: import("svelte").Component<MeterProps, {}, "ref">;
|
|
65
|
+
type Meter = ReturnType<typeof Meter>;
|
|
66
|
+
export default Meter;
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
export type NativeSelectOption<V = string> = {
|
|
5
5
|
value: V;
|
|
6
6
|
label: string;
|
|
7
|
+
/**
|
|
8
|
+
* The `<optgroup>` the option sits in. Options with the same group share
|
|
9
|
+
* one, in the order each group first appears; options without one come
|
|
10
|
+
* first.
|
|
11
|
+
*/
|
|
12
|
+
group?: string;
|
|
7
13
|
disabled?: boolean;
|
|
8
14
|
};
|
|
9
15
|
|
|
@@ -13,6 +19,7 @@
|
|
|
13
19
|
|
|
14
20
|
<script lang="ts" generics="V = string">
|
|
15
21
|
import type { Snippet } from 'svelte';
|
|
22
|
+
import { groupOptions } from './internal/groups.js';
|
|
16
23
|
|
|
17
24
|
const uid = $props.id();
|
|
18
25
|
|
|
@@ -39,6 +46,8 @@
|
|
|
39
46
|
labelBeside?: boolean | NativeSelectBreakpoint;
|
|
40
47
|
children?: Snippet;
|
|
41
48
|
} = $props();
|
|
49
|
+
|
|
50
|
+
const grouped = $derived(options && groupOptions(options));
|
|
42
51
|
</script>
|
|
43
52
|
|
|
44
53
|
<!--
|
|
@@ -53,10 +62,17 @@
|
|
|
53
62
|
<label class="hp-label" for={id}>{label}</label>
|
|
54
63
|
<span class="hp-well-chevron">
|
|
55
64
|
<select {...rest} {id} class={['hp-well', className]} bind:value>
|
|
56
|
-
{#if
|
|
57
|
-
{#each
|
|
65
|
+
{#if grouped}
|
|
66
|
+
{#each grouped.loose as option (option.value)}
|
|
58
67
|
<option value={option.value} disabled={option.disabled}>{option.label}</option>
|
|
59
68
|
{/each}
|
|
69
|
+
{#each grouped.groups as group (group.name)}
|
|
70
|
+
<optgroup label={group.name}>
|
|
71
|
+
{#each group.items as option (option.value)}
|
|
72
|
+
<option value={option.value} disabled={option.disabled}>{option.label}</option>
|
|
73
|
+
{/each}
|
|
74
|
+
</optgroup>
|
|
75
|
+
{/each}
|
|
60
76
|
{:else}
|
|
61
77
|
{@render children?.()}
|
|
62
78
|
{/if}
|
|
@@ -2,6 +2,12 @@ import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
|
2
2
|
export type NativeSelectOption<V = string> = {
|
|
3
3
|
value: V;
|
|
4
4
|
label: string;
|
|
5
|
+
/**
|
|
6
|
+
* The `<optgroup>` the option sits in. Options with the same group share
|
|
7
|
+
* one, in the order each group first appears; options without one come
|
|
8
|
+
* first.
|
|
9
|
+
*/
|
|
10
|
+
group?: string;
|
|
5
11
|
disabled?: boolean;
|
|
6
12
|
};
|
|
7
13
|
/** From which width the label sits beside the well: 600px, 900px or 1200px. */
|