@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,117 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
5
|
+
/**
|
|
6
|
+
* What the app is waiting for, such as `Loading projects`. Shown beside
|
|
7
|
+
* the glyph and announced politely once the spinner appears.
|
|
8
|
+
*/
|
|
9
|
+
label: string;
|
|
10
|
+
/** The root element. */
|
|
11
|
+
ref?: HTMLSpanElement | null;
|
|
12
|
+
};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { duration } from '../motion/transitions.js';
|
|
17
|
+
|
|
18
|
+
// The reel's line spinner (da3-10): bar, slash, dash, backslash.
|
|
19
|
+
const glyphs = ['|', '/', '-', '\\'];
|
|
20
|
+
|
|
21
|
+
let { label, class: className, ref = $bindable(null), ...rest }: SpinnerProps = $props();
|
|
22
|
+
|
|
23
|
+
let shown = $state(false);
|
|
24
|
+
let step = $state(0);
|
|
25
|
+
|
|
26
|
+
// A timer, not CSS keyframes (keyframes are for expressive entrances
|
|
27
|
+
// only). It reads both durations from the element, so an app can retune
|
|
28
|
+
// them, also per spinner (`style="--spinner-delay: 0ms"`). It keeps
|
|
29
|
+
// stepping under reduced motion: spinners and progress stay.
|
|
30
|
+
function run(node: HTMLElement) {
|
|
31
|
+
const delay = duration(node, '--spinner-delay');
|
|
32
|
+
const every = duration(node, '--spinner-step');
|
|
33
|
+
let interval: ReturnType<typeof setInterval> | undefined;
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
shown = true;
|
|
36
|
+
if (every > 0) interval = setInterval(() => (step = (step + 1) % glyphs.length), every);
|
|
37
|
+
}, delay);
|
|
38
|
+
return () => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
clearInterval(interval);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<!--
|
|
46
|
+
A stepped spinner (BRIEF, visual rule 7): `|`, `/`, `-`, `\` in Geist
|
|
47
|
+
Pixel Square at 26.32px, one state per `--spinner-step` (167ms). Nothing
|
|
48
|
+
shows until `--spinner-delay` (300ms) has passed, so fast work never
|
|
49
|
+
flashes it; the row keeps its height meanwhile. The glyph is aria-hidden
|
|
50
|
+
and sits outside the live region; the label is a `role="status"` region
|
|
51
|
+
that is empty until the spinner appears, so screen readers announce it
|
|
52
|
+
once. Extra attributes and `class` go to the root `span.hp-spinner`, which
|
|
53
|
+
carries `data-state` waiting | visible. Place it start-aligned: centring
|
|
54
|
+
it in a wider box can put the glyph between pixels.
|
|
55
|
+
-->
|
|
56
|
+
<span
|
|
57
|
+
{...rest}
|
|
58
|
+
bind:this={ref}
|
|
59
|
+
class={['hp-spinner', className]}
|
|
60
|
+
data-state={shown ? 'visible' : 'waiting'}
|
|
61
|
+
{@attach run}
|
|
62
|
+
>
|
|
63
|
+
<span class="hp-spinner-glyph" aria-hidden="true" data-step={shown ? step : undefined}
|
|
64
|
+
>{shown ? glyphs[step] : ''}</span
|
|
65
|
+
>
|
|
66
|
+
<span class="hp-spinner-label" role="status">{shown ? label : ''}</span>
|
|
67
|
+
</span>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
/* One row the height of the pixel line, so the spinner does not move
|
|
71
|
+
anything when it appears. The label line sits an even number of pixels
|
|
72
|
+
from the top, so the glyph's top edge stays on a whole pixel. */
|
|
73
|
+
.hp-spinner {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: var(--space-3);
|
|
77
|
+
min-height: var(--type-pixel-leading);
|
|
78
|
+
vertical-align: top;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Geist Pixel is proportional: at 26.32px (a 1px module) the four glyphs
|
|
82
|
+
advance 6, 11, 10 and 11px. A fixed 12px box keeps the label still
|
|
83
|
+
while the glyph changes. Each glyph starts at the box's edge plus a
|
|
84
|
+
whole-pixel inset that centres it as nearly as whole pixels allow,
|
|
85
|
+
because centring in odd free space would put its edges on half pixels
|
|
86
|
+
(BRIEF, Crisp pixel text). The insets are font metrics. */
|
|
87
|
+
.hp-spinner-glyph {
|
|
88
|
+
flex: none;
|
|
89
|
+
width: 12px;
|
|
90
|
+
font-family: var(--pixel);
|
|
91
|
+
font-size: var(--type-pixel);
|
|
92
|
+
font-weight: 400;
|
|
93
|
+
line-height: var(--type-pixel-leading);
|
|
94
|
+
letter-spacing: 0;
|
|
95
|
+
text-align: start;
|
|
96
|
+
color: var(--text);
|
|
97
|
+
}
|
|
98
|
+
.hp-spinner-glyph[data-step='0'] {
|
|
99
|
+
padding-inline-start: 3px;
|
|
100
|
+
}
|
|
101
|
+
.hp-spinner-glyph[data-step='2'] {
|
|
102
|
+
padding-inline-start: 1px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* 16px on touch, the dense 14px on fine pointers. */
|
|
106
|
+
.hp-spinner-label {
|
|
107
|
+
color: var(--muted);
|
|
108
|
+
font-size: var(--type-body);
|
|
109
|
+
line-height: 24px;
|
|
110
|
+
}
|
|
111
|
+
@media (pointer: fine) {
|
|
112
|
+
.hp-spinner-label {
|
|
113
|
+
font-size: var(--type-dense);
|
|
114
|
+
line-height: 20px;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
3
|
+
/**
|
|
4
|
+
* What the app is waiting for, such as `Loading projects`. Shown beside
|
|
5
|
+
* the glyph and announced politely once the spinner appears.
|
|
6
|
+
*/
|
|
7
|
+
label: string;
|
|
8
|
+
/** The root element. */
|
|
9
|
+
ref?: HTMLSpanElement | null;
|
|
10
|
+
};
|
|
11
|
+
declare const Spinner: import("svelte").Component<SpinnerProps, {}, "ref">;
|
|
12
|
+
type Spinner = ReturnType<typeof Spinner>;
|
|
13
|
+
export default Spinner;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `standard` (default): the size for a row of several stats. `display`:
|
|
6
|
+
* the 32 to 56px display size, for the view's one display subject.
|
|
7
|
+
*/
|
|
8
|
+
export type StatVariant = 'standard' | 'display';
|
|
9
|
+
|
|
10
|
+
/** A status hue for the delta: `ok` good, `warn` needs attention, `danger` bad. */
|
|
11
|
+
export type StatTone = 'ok' | 'warn' | 'danger';
|
|
12
|
+
|
|
13
|
+
export type StatProps = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
|
|
14
|
+
/** The caps label. A fixed string the app defines. */
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* The value in Geist with tabular figures. A string is shown as given
|
|
18
|
+
* and changes at once. A number is shown through `format`; when it
|
|
19
|
+
* changes after the first render, it ticks from the shown figure to
|
|
20
|
+
* the new one (BRIEF, motion rule 5).
|
|
21
|
+
*/
|
|
22
|
+
value: string | number;
|
|
23
|
+
/**
|
|
24
|
+
* Formats a number `value` at rest and at every step of a tick, for
|
|
25
|
+
* example `(n) => n.toLocaleString('en-US')`. Each step is rounded to
|
|
26
|
+
* the larger number of decimal places of the old and new value before
|
|
27
|
+
* it is formatted. Without `format` a number shows as `String(value)`,
|
|
28
|
+
* and the steps show those decimal places.
|
|
29
|
+
*/
|
|
30
|
+
format?: (value: number) => string;
|
|
31
|
+
/** A unit after the value, smaller and in the muted ink: `EUR`, `ms`, `tasks`. */
|
|
32
|
+
unit?: string;
|
|
33
|
+
/**
|
|
34
|
+
* The change, in the muted ink. A number gets its sign: `+12`, `−3`
|
|
35
|
+
* (a true minus), `0`. A string is shown as written, sign included.
|
|
36
|
+
*/
|
|
37
|
+
delta?: string | number;
|
|
38
|
+
/** What the delta compares with, after it: `since last week`. */
|
|
39
|
+
deltaNote?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Colours the delta in a status hue. Without it the delta stays muted.
|
|
42
|
+
* The label or `deltaNote` must say the same, because colour is never
|
|
43
|
+
* the only signal.
|
|
44
|
+
*/
|
|
45
|
+
tone?: StatTone;
|
|
46
|
+
variant?: StatVariant;
|
|
47
|
+
};
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<script lang="ts">
|
|
51
|
+
import { untrack } from 'svelte';
|
|
52
|
+
import { Tween } from 'svelte/motion';
|
|
53
|
+
import { curve, duration, useMotion } from '../motion/index.js';
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
label,
|
|
57
|
+
value,
|
|
58
|
+
format,
|
|
59
|
+
unit,
|
|
60
|
+
delta,
|
|
61
|
+
deltaNote,
|
|
62
|
+
tone,
|
|
63
|
+
variant = 'standard',
|
|
64
|
+
class: className,
|
|
65
|
+
...rest
|
|
66
|
+
}: StatProps = $props();
|
|
67
|
+
|
|
68
|
+
const change = $derived(
|
|
69
|
+
typeof delta === 'number'
|
|
70
|
+
? delta > 0
|
|
71
|
+
? `+${delta}`
|
|
72
|
+
: delta < 0
|
|
73
|
+
? `\u2212${-delta}`
|
|
74
|
+
: String(delta)
|
|
75
|
+
: delta
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ---- Tick (BRIEF, motion rule 5) ---------------------------------------
|
|
79
|
+
|
|
80
|
+
// A number value that changes after the first render ticks from the
|
|
81
|
+
// figure on screen to the new one. Quiet motion, so it runs in both tiers.
|
|
82
|
+
// Strings, reduced motion and live regions change at once.
|
|
83
|
+
const LIVE = '[aria-live]:not([aria-live="off"]), [role="status"], [role="log"], [role="alert"]';
|
|
84
|
+
|
|
85
|
+
const motion = useMotion();
|
|
86
|
+
let valueEl: HTMLElement | undefined;
|
|
87
|
+
// Starts on the first value, so nothing moves on the first render.
|
|
88
|
+
const tween = new Tween(untrack(() => (typeof value === 'number' ? value : 0)));
|
|
89
|
+
let last = untrack(() => value);
|
|
90
|
+
// The decimal places of the running tick's steps.
|
|
91
|
+
let places = $state(0);
|
|
92
|
+
// The figure on screen when the tick began. A hidden copy keeps its width.
|
|
93
|
+
let from = $state('');
|
|
94
|
+
|
|
95
|
+
/** The decimal places of `n` as `String()` writes it, exponent included. */
|
|
96
|
+
function decimals(n: number): number {
|
|
97
|
+
const [digits, exp = '0'] = String(n).split('e');
|
|
98
|
+
return Math.min(100, Math.max(0, (digits.split('.')[1]?.length ?? 0) - Number(exp)));
|
|
99
|
+
}
|
|
100
|
+
const atRest = (n: number) => (format ? format(n) : String(n));
|
|
101
|
+
/** One step: rounded to `p` places, never `-0`, then formatted. */
|
|
102
|
+
function atStep(n: number, p: number): string {
|
|
103
|
+
const r = Number(n.toFixed(p)) || 0;
|
|
104
|
+
return format ? format(r) : r.toFixed(p);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const text = $derived(typeof value === 'number' ? atRest(value) : value);
|
|
108
|
+
const ticking = $derived(
|
|
109
|
+
typeof value === 'number' && !Number.isNaN(value) && tween.current !== value
|
|
110
|
+
);
|
|
111
|
+
const step = $derived(ticking ? atStep(tween.current, places) : '');
|
|
112
|
+
|
|
113
|
+
// Before the DOM updates, so the first frame after a change still shows
|
|
114
|
+
// the old figure. Duration and curve are read from the value element at
|
|
115
|
+
// the change, so a retuned token applies. A change during a tick starts
|
|
116
|
+
// from the step on screen.
|
|
117
|
+
$effect.pre(() => {
|
|
118
|
+
const to = value;
|
|
119
|
+
untrack(() => {
|
|
120
|
+
const was = last;
|
|
121
|
+
last = to;
|
|
122
|
+
if (typeof to !== 'number' || to === was) return;
|
|
123
|
+
const resting = tween.current === tween.target;
|
|
124
|
+
const shown = resting ? tween.current : Number(tween.current.toFixed(places)) || 0;
|
|
125
|
+
const ms =
|
|
126
|
+
valueEl && typeof was === 'number' && !motion.reduced && !valueEl.closest(LIVE)
|
|
127
|
+
? duration(valueEl, '--dur-tick')
|
|
128
|
+
: 0;
|
|
129
|
+
if (!valueEl || ms <= 0 || !Number.isFinite(shown) || !Number.isFinite(to)) {
|
|
130
|
+
void tween.set(to, { duration: 0 });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
from = resting ? atRest(shown) : atStep(shown, places);
|
|
134
|
+
places = Math.max(decimals(shown), decimals(to));
|
|
135
|
+
// Tween would run the old tick for one more frame before the new
|
|
136
|
+
// one starts; stop it on the step on screen first.
|
|
137
|
+
if (!resting) void tween.set(shown, { duration: 0 });
|
|
138
|
+
void tween.set(to, { duration: ms, easing: curve(valueEl, '--tick') });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<!--
|
|
144
|
+
A caps label over a key figure in Geist with tabular figures (BRIEF, visual
|
|
145
|
+
rules 3 and 4; da3-65). A number value ticks to a new value over
|
|
146
|
+
`--dur-tick` on the `--tick` curve, never on the first render. The final
|
|
147
|
+
value stays in the accessible text; the moving steps are aria-hidden. The
|
|
148
|
+
root carries `data-ticking` during a tick. Extra attributes and `class` go
|
|
149
|
+
to the <dl>.
|
|
150
|
+
-->
|
|
151
|
+
<dl
|
|
152
|
+
{...rest}
|
|
153
|
+
class={['hp-stat', className]}
|
|
154
|
+
data-variant={variant}
|
|
155
|
+
data-tone={tone}
|
|
156
|
+
data-ticking={ticking ? '' : undefined}
|
|
157
|
+
>
|
|
158
|
+
<dt class="hp-stat-label hp-label">{label}</dt>
|
|
159
|
+
<dd
|
|
160
|
+
bind:this={valueEl}
|
|
161
|
+
class={['hp-stat-value', variant === 'display' ? 'hp-display' : 'hp-page-title hp-num']}
|
|
162
|
+
>
|
|
163
|
+
<span class="hp-stat-figure"
|
|
164
|
+
><span class="hp-stat-final">{text}</span>{#if ticking}<span
|
|
165
|
+
class="hp-stat-from"
|
|
166
|
+
aria-hidden="true">{from}</span
|
|
167
|
+
><span class="hp-stat-step" aria-hidden="true">{step}</span>{/if}</span
|
|
168
|
+
>{#if unit}{' '}<span class="hp-stat-unit">{unit}</span>{/if}
|
|
169
|
+
</dd>
|
|
170
|
+
{#if change !== undefined && change !== ''}
|
|
171
|
+
<dd class="hp-stat-delta">
|
|
172
|
+
<span class="hp-stat-change">{change}</span>{#if deltaNote}{' '}{deltaNote}{/if}
|
|
173
|
+
</dd>
|
|
174
|
+
{/if}
|
|
175
|
+
</dl>
|
|
176
|
+
|
|
177
|
+
<style>
|
|
178
|
+
.hp-stat {
|
|
179
|
+
display: grid;
|
|
180
|
+
align-content: start;
|
|
181
|
+
gap: var(--space-1);
|
|
182
|
+
min-width: 0;
|
|
183
|
+
}
|
|
184
|
+
.hp-stat[data-variant='display'] {
|
|
185
|
+
gap: var(--space-2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.hp-stat-label {
|
|
189
|
+
color: var(--muted);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Geist even inside a mono context such as a table cell. The size comes
|
|
193
|
+
from base.css: .hp-page-title (24px, 20px on compact) with .hp-num, or
|
|
194
|
+
.hp-display. A long figure breaks rather than widen a phone page. */
|
|
195
|
+
.hp-stat-value {
|
|
196
|
+
font-family: var(--sans);
|
|
197
|
+
overflow-wrap: anywhere;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* During a tick the figure stacks three layers in one grid cell: the
|
|
201
|
+
final value (transparent, read by screen readers), a hidden copy of
|
|
202
|
+
the figure shown when the tick began, and the moving step. The cell is
|
|
203
|
+
as wide as the wider of the old and new figure, so the unit after it
|
|
204
|
+
holds still; the steps lie between the two in value, and with tabular
|
|
205
|
+
figures in width too. */
|
|
206
|
+
.hp-stat[data-ticking] .hp-stat-figure {
|
|
207
|
+
display: inline-grid;
|
|
208
|
+
}
|
|
209
|
+
.hp-stat[data-ticking] .hp-stat-figure > * {
|
|
210
|
+
grid-area: 1 / 1;
|
|
211
|
+
}
|
|
212
|
+
.hp-stat[data-ticking] .hp-stat-final {
|
|
213
|
+
opacity: 0;
|
|
214
|
+
}
|
|
215
|
+
.hp-stat-from {
|
|
216
|
+
visibility: hidden;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.hp-stat-unit {
|
|
220
|
+
color: var(--muted);
|
|
221
|
+
font-size: var(--type-body);
|
|
222
|
+
font-weight: 400;
|
|
223
|
+
letter-spacing: 0;
|
|
224
|
+
}
|
|
225
|
+
.hp-stat[data-variant='display'] .hp-stat-unit {
|
|
226
|
+
font-size: var(--type-section);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* 16px on touch, the dense 14px on fine pointers. */
|
|
230
|
+
.hp-stat-delta {
|
|
231
|
+
color: var(--muted);
|
|
232
|
+
font-size: var(--type-body);
|
|
233
|
+
line-height: 24px;
|
|
234
|
+
font-variant-numeric: tabular-nums;
|
|
235
|
+
overflow-wrap: anywhere;
|
|
236
|
+
}
|
|
237
|
+
@media (pointer: fine) {
|
|
238
|
+
.hp-stat-delta {
|
|
239
|
+
font-size: var(--type-dense);
|
|
240
|
+
line-height: 20px;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.hp-stat[data-tone='ok'] .hp-stat-change {
|
|
245
|
+
color: var(--ok);
|
|
246
|
+
}
|
|
247
|
+
.hp-stat[data-tone='warn'] .hp-stat-change {
|
|
248
|
+
color: var(--warn);
|
|
249
|
+
}
|
|
250
|
+
.hp-stat[data-tone='danger'] .hp-stat-change {
|
|
251
|
+
color: var(--danger);
|
|
252
|
+
}
|
|
253
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
/**
|
|
3
|
+
* `standard` (default): the size for a row of several stats. `display`:
|
|
4
|
+
* the 32 to 56px display size, for the view's one display subject.
|
|
5
|
+
*/
|
|
6
|
+
export type StatVariant = 'standard' | 'display';
|
|
7
|
+
/** A status hue for the delta: `ok` good, `warn` needs attention, `danger` bad. */
|
|
8
|
+
export type StatTone = 'ok' | 'warn' | 'danger';
|
|
9
|
+
export type StatProps = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
|
|
10
|
+
/** The caps label. A fixed string the app defines. */
|
|
11
|
+
label: string;
|
|
12
|
+
/**
|
|
13
|
+
* The value in Geist with tabular figures. A string is shown as given
|
|
14
|
+
* and changes at once. A number is shown through `format`; when it
|
|
15
|
+
* changes after the first render, it ticks from the shown figure to
|
|
16
|
+
* the new one (BRIEF, motion rule 5).
|
|
17
|
+
*/
|
|
18
|
+
value: string | number;
|
|
19
|
+
/**
|
|
20
|
+
* Formats a number `value` at rest and at every step of a tick, for
|
|
21
|
+
* example `(n) => n.toLocaleString('en-US')`. Each step is rounded to
|
|
22
|
+
* the larger number of decimal places of the old and new value before
|
|
23
|
+
* it is formatted. Without `format` a number shows as `String(value)`,
|
|
24
|
+
* and the steps show those decimal places.
|
|
25
|
+
*/
|
|
26
|
+
format?: (value: number) => string;
|
|
27
|
+
/** A unit after the value, smaller and in the muted ink: `EUR`, `ms`, `tasks`. */
|
|
28
|
+
unit?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The change, in the muted ink. A number gets its sign: `+12`, `−3`
|
|
31
|
+
* (a true minus), `0`. A string is shown as written, sign included.
|
|
32
|
+
*/
|
|
33
|
+
delta?: string | number;
|
|
34
|
+
/** What the delta compares with, after it: `since last week`. */
|
|
35
|
+
deltaNote?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Colours the delta in a status hue. Without it the delta stays muted.
|
|
38
|
+
* The label or `deltaNote` must say the same, because colour is never
|
|
39
|
+
* the only signal.
|
|
40
|
+
*/
|
|
41
|
+
tone?: StatTone;
|
|
42
|
+
variant?: StatVariant;
|
|
43
|
+
};
|
|
44
|
+
declare const Stat: import("svelte").Component<StatProps, {}, "">;
|
|
45
|
+
type Stat = ReturnType<typeof Stat>;
|
|
46
|
+
export default Stat;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLOlAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
export type StepsItem = {
|
|
5
|
+
/** The step's caps label. A short fixed string the app defines. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** A supporting line in `--muted`, as written. */
|
|
8
|
+
detail?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Where a step stands, from `current`. Each step's `<li>` carries it in `data-state`. */
|
|
12
|
+
export type StepsState = 'done' | 'current' | 'upcoming';
|
|
13
|
+
|
|
14
|
+
/** From which width the steps sit in a row: 600px, 900px or 1200px. */
|
|
15
|
+
export type StepsBreakpoint = 'medium' | 'expanded' | 'large';
|
|
16
|
+
|
|
17
|
+
export type StepsProps = Omit<HTMLOlAttributes, 'children'> & {
|
|
18
|
+
/** In order, first to last. */
|
|
19
|
+
steps: StepsItem[];
|
|
20
|
+
/**
|
|
21
|
+
* The index of the current step, from 0. Steps before it are done,
|
|
22
|
+
* steps after it upcoming. `steps.length` marks every step done.
|
|
23
|
+
*/
|
|
24
|
+
current: number;
|
|
25
|
+
/**
|
|
26
|
+
* From which width the steps sit in a row of equal columns; narrower,
|
|
27
|
+
* they stack. Default `medium` (600px). Pick a later one when long
|
|
28
|
+
* labels or many steps would not fit their columns.
|
|
29
|
+
*/
|
|
30
|
+
rowFrom?: StepsBreakpoint;
|
|
31
|
+
/** Screen-reader text after a done step's label. Default `done`. */
|
|
32
|
+
doneLabel?: string;
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
let {
|
|
38
|
+
steps,
|
|
39
|
+
current,
|
|
40
|
+
rowFrom = 'medium',
|
|
41
|
+
doneLabel = 'done',
|
|
42
|
+
class: className,
|
|
43
|
+
...rest
|
|
44
|
+
}: StepsProps = $props();
|
|
45
|
+
|
|
46
|
+
function stateOf(index: number): StepsState {
|
|
47
|
+
return index < current ? 'done' : index === current ? 'current' : 'upcoming';
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<!--
|
|
52
|
+
Ordered steps as discrete segments (BRIEF, visual rule 7): each step has a
|
|
53
|
+
3px edge, titanium once reached and --border while upcoming, so the edges
|
|
54
|
+
read as a segmented bar. The current step adds the selection tint and
|
|
55
|
+
aria-current="step"; a done step shows a check for its number. A row from
|
|
56
|
+
`rowFrom` (600px by default), stacked below. When the current step
|
|
57
|
+
changes, the edges and the tint fade their colour over --dur-segment.
|
|
58
|
+
Extra attributes (an `aria-label` naming the process)
|
|
59
|
+
and `class` go to the <ol>.
|
|
60
|
+
-->
|
|
61
|
+
<ol role="list" {...rest} class={['hp-steps', className]} data-row-from={rowFrom}>
|
|
62
|
+
{#each steps as step, index}
|
|
63
|
+
{@const state = stateOf(index)}
|
|
64
|
+
<li class="hp-steps-step" data-state={state} aria-current={state === 'current' ? 'step' : undefined}>
|
|
65
|
+
<span class="hp-steps-mark" aria-hidden="true">
|
|
66
|
+
{#if state === 'done'}
|
|
67
|
+
<svg viewBox="0 0 16 16" width="16" height="16" focusable="false">
|
|
68
|
+
<path d="m3 8.5 3.5 3.5 6.5-7.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" />
|
|
69
|
+
</svg>
|
|
70
|
+
{:else}
|
|
71
|
+
{index + 1}
|
|
72
|
+
{/if}
|
|
73
|
+
</span>
|
|
74
|
+
<span class="hp-steps-label hp-label">{step.label}</span>
|
|
75
|
+
{#if state === 'done'}<span class="hp-visually-hidden">, {doneLabel}</span>{/if}
|
|
76
|
+
{#if step.detail}<span class="hp-steps-detail">{step.detail}</span>{/if}
|
|
77
|
+
</li>
|
|
78
|
+
{/each}
|
|
79
|
+
</ol>
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
/* Stacked below 600px. The edges and the current tint reach --bleed into
|
|
83
|
+
the gutter, so the labels keep the page edge, as table rows do. */
|
|
84
|
+
.hp-steps {
|
|
85
|
+
display: grid;
|
|
86
|
+
gap: var(--space-1);
|
|
87
|
+
margin: 0 calc(-1 * var(--bleed));
|
|
88
|
+
padding: 0;
|
|
89
|
+
list-style: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* The edge colour is a registered colour property, so it can transition
|
|
93
|
+
on its own while the edge's shape switches at once between the stacked
|
|
94
|
+
and the row form. */
|
|
95
|
+
@property --steps-edge {
|
|
96
|
+
syntax: '<color>';
|
|
97
|
+
inherits: false;
|
|
98
|
+
initial-value: transparent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* When the current step changes, the edge and the tint fade their colour
|
|
102
|
+
over --dur-segment (BRIEF, short motion). A CSS transition never runs
|
|
103
|
+
on an element's first style, so steps show their state at once when
|
|
104
|
+
they render. 0ms is a snap. Colour only, so reduced motion keeps it.
|
|
105
|
+
The text colour and the number or check change at once. */
|
|
106
|
+
.hp-steps-step {
|
|
107
|
+
--steps-edge: var(--accent);
|
|
108
|
+
position: relative;
|
|
109
|
+
display: grid;
|
|
110
|
+
grid-template-columns: var(--space-4) minmax(0, 1fr);
|
|
111
|
+
column-gap: var(--space-2);
|
|
112
|
+
row-gap: var(--space-1);
|
|
113
|
+
align-content: start;
|
|
114
|
+
padding: var(--space-3) var(--space-3) var(--space-3) var(--bleed);
|
|
115
|
+
/* the brief's 3px selection edge */
|
|
116
|
+
box-shadow: inset 3px 0 0 var(--steps-edge);
|
|
117
|
+
overflow-wrap: anywhere;
|
|
118
|
+
transition:
|
|
119
|
+
--steps-edge var(--dur-segment) var(--settle),
|
|
120
|
+
background-color var(--dur-segment) var(--settle);
|
|
121
|
+
}
|
|
122
|
+
.hp-steps-step[data-state='current'] {
|
|
123
|
+
background: var(--surface-selected);
|
|
124
|
+
}
|
|
125
|
+
.hp-steps-step[data-state='upcoming'] {
|
|
126
|
+
--steps-edge: var(--border);
|
|
127
|
+
color: var(--muted);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* The number or the check, in a 16px box beside the label's first line. */
|
|
131
|
+
.hp-steps-mark {
|
|
132
|
+
display: grid;
|
|
133
|
+
place-items: center;
|
|
134
|
+
height: var(--space-4);
|
|
135
|
+
font-size: var(--type-label);
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
line-height: 16px;
|
|
138
|
+
font-variant-numeric: tabular-nums;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.hp-steps-detail {
|
|
142
|
+
grid-column: 2;
|
|
143
|
+
color: var(--muted);
|
|
144
|
+
font-size: var(--type-body);
|
|
145
|
+
line-height: 24px;
|
|
146
|
+
}
|
|
147
|
+
@media (pointer: fine) {
|
|
148
|
+
.hp-steps-detail {
|
|
149
|
+
font-size: var(--type-dense);
|
|
150
|
+
line-height: 20px;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* A row of equal columns from the rowFrom width, the edge on top. A label
|
|
155
|
+
longer than its column still breaks rather than widen the page. */
|
|
156
|
+
@media (min-width: 600px) {
|
|
157
|
+
.hp-steps[data-row-from='medium'] {
|
|
158
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
159
|
+
grid-auto-flow: column;
|
|
160
|
+
}
|
|
161
|
+
[data-row-from='medium'] > .hp-steps-step {
|
|
162
|
+
padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
|
|
163
|
+
/* the brief's 3px selection edge */
|
|
164
|
+
box-shadow: inset 0 3px 0 var(--steps-edge);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
@media (min-width: 900px) {
|
|
168
|
+
.hp-steps[data-row-from='expanded'] {
|
|
169
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
170
|
+
grid-auto-flow: column;
|
|
171
|
+
}
|
|
172
|
+
[data-row-from='expanded'] > .hp-steps-step {
|
|
173
|
+
padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
|
|
174
|
+
/* the brief's 3px selection edge */
|
|
175
|
+
box-shadow: inset 0 3px 0 var(--steps-edge);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
@media (min-width: 1200px) {
|
|
179
|
+
.hp-steps[data-row-from='large'] {
|
|
180
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
181
|
+
grid-auto-flow: column;
|
|
182
|
+
}
|
|
183
|
+
[data-row-from='large'] > .hp-steps-step {
|
|
184
|
+
padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
|
|
185
|
+
/* the brief's 3px selection edge */
|
|
186
|
+
box-shadow: inset 0 3px 0 var(--steps-edge);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTMLOlAttributes } from 'svelte/elements';
|
|
2
|
+
export type StepsItem = {
|
|
3
|
+
/** The step's caps label. A short fixed string the app defines. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** A supporting line in `--muted`, as written. */
|
|
6
|
+
detail?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Where a step stands, from `current`. Each step's `<li>` carries it in `data-state`. */
|
|
9
|
+
export type StepsState = 'done' | 'current' | 'upcoming';
|
|
10
|
+
/** From which width the steps sit in a row: 600px, 900px or 1200px. */
|
|
11
|
+
export type StepsBreakpoint = 'medium' | 'expanded' | 'large';
|
|
12
|
+
export type StepsProps = Omit<HTMLOlAttributes, 'children'> & {
|
|
13
|
+
/** In order, first to last. */
|
|
14
|
+
steps: StepsItem[];
|
|
15
|
+
/**
|
|
16
|
+
* The index of the current step, from 0. Steps before it are done,
|
|
17
|
+
* steps after it upcoming. `steps.length` marks every step done.
|
|
18
|
+
*/
|
|
19
|
+
current: number;
|
|
20
|
+
/**
|
|
21
|
+
* From which width the steps sit in a row of equal columns; narrower,
|
|
22
|
+
* they stack. Default `medium` (600px). Pick a later one when long
|
|
23
|
+
* labels or many steps would not fit their columns.
|
|
24
|
+
*/
|
|
25
|
+
rowFrom?: StepsBreakpoint;
|
|
26
|
+
/** Screen-reader text after a done step's label. Default `done`. */
|
|
27
|
+
doneLabel?: string;
|
|
28
|
+
};
|
|
29
|
+
declare const Steps: import("svelte").Component<StepsProps, {}, "">;
|
|
30
|
+
type Steps = ReturnType<typeof Steps>;
|
|
31
|
+
export default Steps;
|