@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,46 @@
|
|
|
1
|
+
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
2
|
+
export type NativeSelectOption<V = string> = {
|
|
3
|
+
value: V;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** From which width the label sits beside the well: 600px, 900px or 1200px. */
|
|
8
|
+
export type NativeSelectBreakpoint = 'medium' | 'expanded' | 'large';
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
declare function $$render<V = string>(): {
|
|
11
|
+
props: Omit<HTMLSelectAttributes, "children" | "value" | "multiple"> & {
|
|
12
|
+
/** The caps field label. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The chosen option's value. Bindable. */
|
|
15
|
+
value?: V;
|
|
16
|
+
/** The choices. Without it, pass `<option>` elements as children. */
|
|
17
|
+
options?: NativeSelectOption<V>[];
|
|
18
|
+
/**
|
|
19
|
+
* The label sits above the well by default. `true` puts it beside the
|
|
20
|
+
* well at every width; a breakpoint puts it beside from that width up.
|
|
21
|
+
*/
|
|
22
|
+
labelBeside?: boolean | NativeSelectBreakpoint;
|
|
23
|
+
children?: Snippet;
|
|
24
|
+
};
|
|
25
|
+
exports: {};
|
|
26
|
+
bindings: "value";
|
|
27
|
+
slots: {};
|
|
28
|
+
events: {};
|
|
29
|
+
};
|
|
30
|
+
declare class __sveltets_Render<V = string> {
|
|
31
|
+
props(): ReturnType<typeof $$render<V>>['props'];
|
|
32
|
+
events(): ReturnType<typeof $$render<V>>['events'];
|
|
33
|
+
slots(): ReturnType<typeof $$render<V>>['slots'];
|
|
34
|
+
bindings(): "value";
|
|
35
|
+
exports(): {};
|
|
36
|
+
}
|
|
37
|
+
interface $$IsomorphicComponent {
|
|
38
|
+
new <V = string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
|
|
39
|
+
$$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
|
|
40
|
+
} & ReturnType<__sveltets_Render<V>['exports']>;
|
|
41
|
+
<V = string>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
|
|
42
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
43
|
+
}
|
|
44
|
+
declare const NativeSelect: $$IsomorphicComponent;
|
|
45
|
+
type NativeSelect<V = string> = InstanceType<typeof NativeSelect<V>>;
|
|
46
|
+
export default NativeSelect;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
export type PaginationProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
5
|
+
/** The current page, from 1. Bindable. */
|
|
6
|
+
page?: number;
|
|
7
|
+
/** The number of items. */
|
|
8
|
+
count: number;
|
|
9
|
+
/** Items per page. With the default 1, `count` is the number of pages. */
|
|
10
|
+
perPage?: number;
|
|
11
|
+
/** The navigation landmark's accessible name. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** The caps label before the page figure. */
|
|
14
|
+
pageLabel?: string;
|
|
15
|
+
previousLabel?: string;
|
|
16
|
+
nextLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { Pagination as BitsPagination } from 'bits-ui';
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
page = $bindable(1),
|
|
25
|
+
count,
|
|
26
|
+
perPage = 1,
|
|
27
|
+
label = 'Pages',
|
|
28
|
+
pageLabel = 'Page',
|
|
29
|
+
previousLabel = 'Previous page',
|
|
30
|
+
nextLabel = 'Next page',
|
|
31
|
+
class: className,
|
|
32
|
+
...rest
|
|
33
|
+
}: PaginationProps = $props();
|
|
34
|
+
|
|
35
|
+
const pages = $derived(Math.max(1, Math.ceil(count / perPage)));
|
|
36
|
+
|
|
37
|
+
// Fewer items (a filter) can leave the page past the end; keep it in range.
|
|
38
|
+
$effect.pre(() => {
|
|
39
|
+
if (page > pages) page = pages;
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!--
|
|
44
|
+
The list view's one group of solid blocks: square titanium steps with
|
|
45
|
+
black arrows. Bits UI Pagination supplies the behaviour: disabled ends and
|
|
46
|
+
arrow keys between the steps.
|
|
47
|
+
-->
|
|
48
|
+
<BitsPagination.Root bind:page {count} {perPage}>
|
|
49
|
+
{#snippet child({ props, currentPage })}
|
|
50
|
+
<nav {...rest} {...props} class={['hp-pagination', className]} aria-label={label}>
|
|
51
|
+
<span class="hp-label">{pageLabel}</span>
|
|
52
|
+
<span class="figure" aria-live="polite">{currentPage} / {pages}</span>
|
|
53
|
+
<span class="steps">
|
|
54
|
+
<BitsPagination.PrevButton>
|
|
55
|
+
{#snippet child({ props })}
|
|
56
|
+
<button {...props} class="step" data-direction="previous" aria-label={previousLabel}
|
|
57
|
+
></button>
|
|
58
|
+
{/snippet}
|
|
59
|
+
</BitsPagination.PrevButton>
|
|
60
|
+
<BitsPagination.NextButton>
|
|
61
|
+
{#snippet child({ props })}
|
|
62
|
+
<button {...props} class="step" data-direction="next" aria-label={nextLabel}></button>
|
|
63
|
+
{/snippet}
|
|
64
|
+
</BitsPagination.NextButton>
|
|
65
|
+
</span>
|
|
66
|
+
</nav>
|
|
67
|
+
{/snippet}
|
|
68
|
+
</BitsPagination.Root>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.hp-pagination {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: var(--space-4);
|
|
75
|
+
}
|
|
76
|
+
.figure {
|
|
77
|
+
font-family: var(--mono);
|
|
78
|
+
font-variant-numeric: tabular-nums;
|
|
79
|
+
}
|
|
80
|
+
/* 8px apart, so a focus contour (4px out) never overlaps the neighbour. */
|
|
81
|
+
.steps {
|
|
82
|
+
display: flex;
|
|
83
|
+
gap: var(--space-2);
|
|
84
|
+
margin-left: var(--space-2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* 24px on fine pointers, 44px on coarse ones (--hit-target). A
|
|
88
|
+
disabled step drops to the band grey. */
|
|
89
|
+
.step {
|
|
90
|
+
display: grid;
|
|
91
|
+
place-items: center;
|
|
92
|
+
width: var(--hit-target);
|
|
93
|
+
height: var(--hit-target);
|
|
94
|
+
padding: 0;
|
|
95
|
+
border: 0;
|
|
96
|
+
background: var(--accent);
|
|
97
|
+
color: var(--canvas);
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
-webkit-tap-highlight-color: transparent;
|
|
100
|
+
}
|
|
101
|
+
.step::before {
|
|
102
|
+
content: '';
|
|
103
|
+
width: 6px;
|
|
104
|
+
height: 8px;
|
|
105
|
+
background: currentColor;
|
|
106
|
+
}
|
|
107
|
+
@media (pointer: coarse) {
|
|
108
|
+
.step::before {
|
|
109
|
+
width: 8px;
|
|
110
|
+
height: 12px;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
.step[data-direction='previous']::before {
|
|
114
|
+
clip-path: polygon(100% 0, 0 50%, 100% 100%);
|
|
115
|
+
}
|
|
116
|
+
.step[data-direction='next']::before {
|
|
117
|
+
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
|
118
|
+
}
|
|
119
|
+
.step:disabled {
|
|
120
|
+
background: var(--surface-band);
|
|
121
|
+
color: var(--muted);
|
|
122
|
+
cursor: default;
|
|
123
|
+
}
|
|
124
|
+
@media (hover: hover) and (pointer: fine) {
|
|
125
|
+
.step:not(:disabled):hover {
|
|
126
|
+
background: var(--muted);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
.step:not(:disabled):active {
|
|
130
|
+
background: var(--muted);
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
export type PaginationProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
3
|
+
/** The current page, from 1. Bindable. */
|
|
4
|
+
page?: number;
|
|
5
|
+
/** The number of items. */
|
|
6
|
+
count: number;
|
|
7
|
+
/** Items per page. With the default 1, `count` is the number of pages. */
|
|
8
|
+
perPage?: number;
|
|
9
|
+
/** The navigation landmark's accessible name. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** The caps label before the page figure. */
|
|
12
|
+
pageLabel?: string;
|
|
13
|
+
previousLabel?: string;
|
|
14
|
+
nextLabel?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const Pagination: import("svelte").Component<PaginationProps, {}, "page">;
|
|
17
|
+
type Pagination = ReturnType<typeof Pagination>;
|
|
18
|
+
export default Pagination;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { Popover as BitsPopover } from 'bits-ui';
|
|
4
|
+
|
|
5
|
+
export type PopoverTriggerSnippetProps = {
|
|
6
|
+
/** Spread onto the button that opens the popover. */
|
|
7
|
+
props: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PopoverActionsSnippetProps = {
|
|
11
|
+
/** Closes the popover; focus returns to the trigger. */
|
|
12
|
+
close: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Attributes and handlers not listed here go to the panel (Bits UI
|
|
17
|
+
* `Popover.Content`), among them `side` (default `bottom`), `align`
|
|
18
|
+
* (default `start`), `sideOffset`, `onOpenAutoFocus`, `onCloseAutoFocus`,
|
|
19
|
+
* `onEscapeKeydown` and `onInteractOutside`. The placement props apply
|
|
20
|
+
* from 600px only; below it the panel is a bottom sheet.
|
|
21
|
+
*/
|
|
22
|
+
export type PopoverProps = Omit<
|
|
23
|
+
BitsPopover.ContentProps,
|
|
24
|
+
'child' | 'children' | 'forceMount' | 'ref' | 'title'
|
|
25
|
+
> & {
|
|
26
|
+
open?: boolean;
|
|
27
|
+
/** The caps title, and the panel's accessible name. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** The button the panel is anchored to: spread `props` onto it. */
|
|
30
|
+
trigger: Snippet<[PopoverTriggerSnippetProps]>;
|
|
31
|
+
/** The body, under the title. */
|
|
32
|
+
children: Snippet;
|
|
33
|
+
/** The action row at the bottom, primary action last. */
|
|
34
|
+
actions?: Snippet<[PopoverActionsSnippetProps]>;
|
|
35
|
+
/** The sheet's close button's accessible name (below 600px). */
|
|
36
|
+
closeLabel?: string;
|
|
37
|
+
/** The panel element. */
|
|
38
|
+
ref?: HTMLElement | null;
|
|
39
|
+
};
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
44
|
+
import { pop } from '../motion/index.js';
|
|
45
|
+
import Sheet from './internal/Sheet.svelte';
|
|
46
|
+
|
|
47
|
+
const uid = $props.id();
|
|
48
|
+
|
|
49
|
+
let {
|
|
50
|
+
open = $bindable(false),
|
|
51
|
+
title,
|
|
52
|
+
trigger,
|
|
53
|
+
children,
|
|
54
|
+
actions,
|
|
55
|
+
closeLabel = 'Close',
|
|
56
|
+
ref = $bindable(null),
|
|
57
|
+
class: className,
|
|
58
|
+
...rest
|
|
59
|
+
}: PopoverProps = $props();
|
|
60
|
+
|
|
61
|
+
const titleId = `${uid}-title`;
|
|
62
|
+
|
|
63
|
+
// The form follows the width (BRIEF, Apps and phones: Overlays): a panel
|
|
64
|
+
// anchored to the trigger from 600px, the shared bottom sheet below it.
|
|
65
|
+
const wide = new MediaQuery('min-width: 600px', true);
|
|
66
|
+
const form = $derived(wide.current ? 'popover' : 'sheet');
|
|
67
|
+
|
|
68
|
+
function close() {
|
|
69
|
+
open = false;
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<!--
|
|
74
|
+
Bits UI gives the trigger aria-haspopup="dialog", aria-expanded and
|
|
75
|
+
aria-controls; the panel takes role="dialog" and its title as its name.
|
|
76
|
+
Focus moves into the panel on open (to the sheet's close button on
|
|
77
|
+
compact) and returns to the trigger on close. Escape, a press outside,
|
|
78
|
+
the trigger and, on compact, the close button and a swipe down close it.
|
|
79
|
+
-->
|
|
80
|
+
<BitsPopover.Root bind:open>
|
|
81
|
+
<BitsPopover.Trigger>
|
|
82
|
+
{#snippet child({ props })}
|
|
83
|
+
{@render trigger({ props })}
|
|
84
|
+
{/snippet}
|
|
85
|
+
</BitsPopover.Trigger>
|
|
86
|
+
|
|
87
|
+
<BitsPopover.Portal>
|
|
88
|
+
{#if wide.current}
|
|
89
|
+
<BitsPopover.Content
|
|
90
|
+
forceMount
|
|
91
|
+
bind:ref
|
|
92
|
+
role="dialog"
|
|
93
|
+
aria-labelledby={titleId}
|
|
94
|
+
side="bottom"
|
|
95
|
+
align="start"
|
|
96
|
+
sideOffset={8}
|
|
97
|
+
collisionPadding={8}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
{#snippet child({ wrapperProps, props, open: shown })}
|
|
101
|
+
{#if shown}
|
|
102
|
+
<div {...wrapperProps}>
|
|
103
|
+
<div {...props} class={['hp-popover', className]} data-form="popover" transition:pop>
|
|
104
|
+
<p id={titleId} class="hp-popover-title hp-label">{title}</p>
|
|
105
|
+
{@render body()}
|
|
106
|
+
{#if actions}
|
|
107
|
+
{@render actionRow()}
|
|
108
|
+
{/if}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{/if}
|
|
112
|
+
{/snippet}
|
|
113
|
+
</BitsPopover.Content>
|
|
114
|
+
{:else}
|
|
115
|
+
<BitsPopover.ContentStatic forceMount preventScroll bind:ref role="dialog" aria-labelledby={titleId} {...rest}>
|
|
116
|
+
{#snippet child({ props, open: shown })}
|
|
117
|
+
<Sheet
|
|
118
|
+
{...props}
|
|
119
|
+
class={['hp-popover', className]}
|
|
120
|
+
data-form="sheet"
|
|
121
|
+
open={shown}
|
|
122
|
+
layer="popover"
|
|
123
|
+
{title}
|
|
124
|
+
{titleId}
|
|
125
|
+
{closeLabel}
|
|
126
|
+
onclose={close}
|
|
127
|
+
footer={actions ? actionRow : undefined}
|
|
128
|
+
>
|
|
129
|
+
{@render body()}
|
|
130
|
+
</Sheet>
|
|
131
|
+
{/snippet}
|
|
132
|
+
</BitsPopover.ContentStatic>
|
|
133
|
+
{/if}
|
|
134
|
+
</BitsPopover.Portal>
|
|
135
|
+
</BitsPopover.Root>
|
|
136
|
+
|
|
137
|
+
{#snippet body()}
|
|
138
|
+
<div class="hp-popover-body" data-form={form} data-end={actions ? undefined : ''}>
|
|
139
|
+
{@render children()}
|
|
140
|
+
</div>
|
|
141
|
+
{/snippet}
|
|
142
|
+
|
|
143
|
+
{#snippet actionRow()}
|
|
144
|
+
<div class="hp-popover-actions" data-form={form}>
|
|
145
|
+
{@render actions?.({ close })}
|
|
146
|
+
</div>
|
|
147
|
+
{/snippet}
|
|
148
|
+
|
|
149
|
+
<style>
|
|
150
|
+
/* ---- Popover (600px and up): a black panel under its trigger --------- */
|
|
151
|
+
|
|
152
|
+
/* On the popover layer (tokens.css), over the shell and over an open
|
|
153
|
+
dialog. Bits UI copies the z-index to the positioned wrapper. As wide
|
|
154
|
+
as its content up to --popover-width (an app passes
|
|
155
|
+
style="--popover-width: …" for another cap; the sheet ignores it), and
|
|
156
|
+
as tall as the space beside the trigger; the body scrolls inside. */
|
|
157
|
+
.hp-popover {
|
|
158
|
+
--popover-width: 360px;
|
|
159
|
+
|
|
160
|
+
position: relative;
|
|
161
|
+
z-index: var(--layer-popover);
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
box-sizing: border-box;
|
|
165
|
+
width: max-content;
|
|
166
|
+
max-width: min(var(--popover-width), var(--bits-popover-content-available-width));
|
|
167
|
+
max-height: var(--bits-popover-content-available-height);
|
|
168
|
+
border: 1px solid var(--border-control);
|
|
169
|
+
background: var(--canvas);
|
|
170
|
+
color: var(--text);
|
|
171
|
+
overscroll-behavior: contain;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* A caps label names the panel, in the main ink, so the muted caps
|
|
175
|
+
labels of fields inside it read as one level down. */
|
|
176
|
+
.hp-popover-title {
|
|
177
|
+
flex: none;
|
|
178
|
+
margin: 0;
|
|
179
|
+
padding: var(--space-3) var(--space-4) 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ---- Body and actions, in both forms -------------------------------- */
|
|
183
|
+
|
|
184
|
+
/* The top padding keeps a first control's focus contour (4px out, 2px
|
|
185
|
+
wide) inside the scrolling box. */
|
|
186
|
+
.hp-popover-body {
|
|
187
|
+
display: grid;
|
|
188
|
+
align-content: start;
|
|
189
|
+
gap: var(--space-4);
|
|
190
|
+
min-width: 0;
|
|
191
|
+
font-size: var(--type-body);
|
|
192
|
+
line-height: 24px;
|
|
193
|
+
}
|
|
194
|
+
.hp-popover-body[data-form='popover'] {
|
|
195
|
+
flex: 1 1 auto;
|
|
196
|
+
min-height: 0;
|
|
197
|
+
padding: var(--space-3) var(--space-4);
|
|
198
|
+
overflow-y: auto;
|
|
199
|
+
overscroll-behavior: contain;
|
|
200
|
+
}
|
|
201
|
+
/* The sheet's body scrolls itself. */
|
|
202
|
+
.hp-popover-body[data-form='sheet'] {
|
|
203
|
+
padding: var(--space-2) var(--gutter);
|
|
204
|
+
}
|
|
205
|
+
/* Without an action row the body ends the panel. */
|
|
206
|
+
.hp-popover-body[data-end] {
|
|
207
|
+
padding-bottom: var(--space-4);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Actions sit at the end, primary last, 8px apart. */
|
|
211
|
+
.hp-popover-actions {
|
|
212
|
+
flex: none;
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
justify-content: flex-end;
|
|
216
|
+
gap: var(--space-2);
|
|
217
|
+
padding: var(--space-4) var(--gutter);
|
|
218
|
+
}
|
|
219
|
+
.hp-popover-actions[data-form='popover'] {
|
|
220
|
+
padding: var(--space-1) var(--space-4) var(--space-4);
|
|
221
|
+
}
|
|
222
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { Popover as BitsPopover } from 'bits-ui';
|
|
3
|
+
export type PopoverTriggerSnippetProps = {
|
|
4
|
+
/** Spread onto the button that opens the popover. */
|
|
5
|
+
props: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export type PopoverActionsSnippetProps = {
|
|
8
|
+
/** Closes the popover; focus returns to the trigger. */
|
|
9
|
+
close: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Attributes and handlers not listed here go to the panel (Bits UI
|
|
13
|
+
* `Popover.Content`), among them `side` (default `bottom`), `align`
|
|
14
|
+
* (default `start`), `sideOffset`, `onOpenAutoFocus`, `onCloseAutoFocus`,
|
|
15
|
+
* `onEscapeKeydown` and `onInteractOutside`. The placement props apply
|
|
16
|
+
* from 600px only; below it the panel is a bottom sheet.
|
|
17
|
+
*/
|
|
18
|
+
export type PopoverProps = Omit<BitsPopover.ContentProps, 'child' | 'children' | 'forceMount' | 'ref' | 'title'> & {
|
|
19
|
+
open?: boolean;
|
|
20
|
+
/** The caps title, and the panel's accessible name. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The button the panel is anchored to: spread `props` onto it. */
|
|
23
|
+
trigger: Snippet<[PopoverTriggerSnippetProps]>;
|
|
24
|
+
/** The body, under the title. */
|
|
25
|
+
children: Snippet;
|
|
26
|
+
/** The action row at the bottom, primary action last. */
|
|
27
|
+
actions?: Snippet<[PopoverActionsSnippetProps]>;
|
|
28
|
+
/** The sheet's close button's accessible name (below 600px). */
|
|
29
|
+
closeLabel?: string;
|
|
30
|
+
/** The panel element. */
|
|
31
|
+
ref?: HTMLElement | null;
|
|
32
|
+
};
|
|
33
|
+
declare const Popover: import("svelte").Component<PopoverProps, {}, "ref" | "open">;
|
|
34
|
+
type Popover = ReturnType<typeof Popover>;
|
|
35
|
+
export default Popover;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { Progress as BitsProgress } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
export type ProgressProps = Omit<BitsProgress.RootProps, 'value' | 'child' | 'children'> & {
|
|
5
|
+
/**
|
|
6
|
+
* How far the work has got, from `min` to `max`. Pass the real figure
|
|
7
|
+
* each time it changes: the bar never moves on its own. Work whose
|
|
8
|
+
* progress is unknown shows a `Spinner` instead.
|
|
9
|
+
*/
|
|
10
|
+
value: number;
|
|
11
|
+
/** The value at which the work is done. Default 100. */
|
|
12
|
+
max?: number;
|
|
13
|
+
/** The value at which the work starts. Default 0. */
|
|
14
|
+
min?: number;
|
|
15
|
+
/**
|
|
16
|
+
* How many segments the bar has (default 20, at least 2). A segment
|
|
17
|
+
* lights once the value has passed its whole share, so the bar never
|
|
18
|
+
* shows more than has been done.
|
|
19
|
+
*/
|
|
20
|
+
segments?: number;
|
|
21
|
+
/**
|
|
22
|
+
* A line above the bar saying what is in progress, as written (never
|
|
23
|
+
* caps: it may hold a file or project name). It names the progress
|
|
24
|
+
* bar; without it, pass `aria-label` or `aria-labelledby`.
|
|
25
|
+
*/
|
|
26
|
+
label?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The value in words, such as `3 of 12 files`. Shown at the end of the
|
|
29
|
+
* label line and read as `aria-valuetext`. Without it both give the
|
|
30
|
+
* percentage done, rounded down.
|
|
31
|
+
*/
|
|
32
|
+
valueText?: string;
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
let {
|
|
38
|
+
value,
|
|
39
|
+
max = 100,
|
|
40
|
+
min = 0,
|
|
41
|
+
segments = 20,
|
|
42
|
+
label,
|
|
43
|
+
valueText,
|
|
44
|
+
id,
|
|
45
|
+
class: className,
|
|
46
|
+
ref = $bindable(null),
|
|
47
|
+
...rest
|
|
48
|
+
}: ProgressProps = $props();
|
|
49
|
+
|
|
50
|
+
const uid = $props.id();
|
|
51
|
+
const labelId = `${uid}-label`;
|
|
52
|
+
|
|
53
|
+
// The value inside its range, so the bar and aria-valuenow agree. An
|
|
54
|
+
// empty range counts as done.
|
|
55
|
+
const current = $derived(Math.min(max, Math.max(min, value)));
|
|
56
|
+
const span = $derived(max - min);
|
|
57
|
+
const count = $derived(Math.max(2, Math.floor(segments)));
|
|
58
|
+
// Round down: a segment lights only when its whole share is done.
|
|
59
|
+
// Multiply before dividing and clear float noise before rounding down,
|
|
60
|
+
// so 58 of 100 and 0.58 of 1 are both 58% and not 57.99…
|
|
61
|
+
const share = (n: number) => Math.floor(Number((((current - min) * n) / span).toPrecision(12)));
|
|
62
|
+
const lit = $derived(current >= max || span <= 0 ? count : share(count));
|
|
63
|
+
const percent = $derived(span > 0 ? share(100) : 100);
|
|
64
|
+
const shown = $derived(valueText ?? `${percent}%`);
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<!--
|
|
68
|
+
Bits UI Progress, rendered through its `child` snippet so the element is
|
|
69
|
+
this component's markup and its styles stay scoped. The root is the
|
|
70
|
+
progressbar (`role`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`,
|
|
71
|
+
`data-state` loading | loaded); `id`, extra attributes and `class` go to
|
|
72
|
+
it. Its children are presentational to assistive technology, so the label
|
|
73
|
+
line names it through `aria-labelledby`, and the value as shown (the
|
|
74
|
+
`valueText` or the floored percentage) reaches it as `aria-valuetext`.
|
|
75
|
+
A segment that lights or goes out fades its colour over --dur-segment.
|
|
76
|
+
-->
|
|
77
|
+
<BitsProgress.Root
|
|
78
|
+
bind:ref
|
|
79
|
+
value={current}
|
|
80
|
+
{max}
|
|
81
|
+
{min}
|
|
82
|
+
aria-labelledby={label ? labelId : undefined}
|
|
83
|
+
aria-valuetext={shown}
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
86
|
+
{#snippet child({ props })}
|
|
87
|
+
<div {...props} {id} class={['hp-progress', className]} data-lit={lit} data-segments={count}>
|
|
88
|
+
{#if label}
|
|
89
|
+
<div class="hp-progress-line">
|
|
90
|
+
<span id={labelId} class="hp-progress-label">{label}</span>
|
|
91
|
+
<span class="hp-progress-value">{shown}</span>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
<div class="hp-progress-bar" style:--segments={count}>
|
|
95
|
+
{#each { length: count }, i}
|
|
96
|
+
<span class="hp-progress-segment" data-lit={i < lit || undefined}></span>
|
|
97
|
+
{/each}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{/snippet}
|
|
101
|
+
</BitsProgress.Root>
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
/* The bar fills the width it is given; the app sets a narrower one. */
|
|
105
|
+
.hp-progress {
|
|
106
|
+
display: grid;
|
|
107
|
+
gap: var(--space-2);
|
|
108
|
+
min-width: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* 16px on touch, the dense 14px on fine pointers. The value keeps the
|
|
112
|
+
end of the line; a long label wraps rather than widen the page. */
|
|
113
|
+
.hp-progress-line {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: baseline;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
gap: var(--space-4);
|
|
118
|
+
font-size: var(--type-body);
|
|
119
|
+
line-height: 24px;
|
|
120
|
+
}
|
|
121
|
+
@media (pointer: fine) {
|
|
122
|
+
.hp-progress-line {
|
|
123
|
+
font-size: var(--type-dense);
|
|
124
|
+
line-height: 20px;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
.hp-progress-label {
|
|
128
|
+
min-width: 0;
|
|
129
|
+
overflow-wrap: anywhere;
|
|
130
|
+
}
|
|
131
|
+
.hp-progress-value {
|
|
132
|
+
flex: none;
|
|
133
|
+
color: var(--muted);
|
|
134
|
+
font-variant-numeric: tabular-nums;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Separate segments (BRIEF, visual rule 7; da3-38): titanium once done,
|
|
138
|
+
the 3:1 control boundary while still to do, so the unlit share reads as
|
|
139
|
+
part of the bar. Square, 4px apart, one row. */
|
|
140
|
+
.hp-progress-bar {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: repeat(var(--segments), minmax(0, 1fr));
|
|
143
|
+
gap: var(--space-1);
|
|
144
|
+
height: 12px;
|
|
145
|
+
}
|
|
146
|
+
/* A segment that lights or goes out fades its colour over --dur-segment
|
|
147
|
+
(BRIEF, short motion). A CSS transition never runs on an element's
|
|
148
|
+
first style, so a bar shows its value at once when it renders. 0ms is
|
|
149
|
+
a snap. Colour only, so reduced motion keeps it. */
|
|
150
|
+
.hp-progress-segment {
|
|
151
|
+
background: var(--border-control);
|
|
152
|
+
transition: background-color var(--dur-segment) var(--settle);
|
|
153
|
+
}
|
|
154
|
+
.hp-progress-segment[data-lit] {
|
|
155
|
+
background: var(--accent);
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Progress as BitsProgress } from 'bits-ui';
|
|
2
|
+
export type ProgressProps = Omit<BitsProgress.RootProps, 'value' | 'child' | 'children'> & {
|
|
3
|
+
/**
|
|
4
|
+
* How far the work has got, from `min` to `max`. Pass the real figure
|
|
5
|
+
* each time it changes: the bar never moves on its own. Work whose
|
|
6
|
+
* progress is unknown shows a `Spinner` instead.
|
|
7
|
+
*/
|
|
8
|
+
value: number;
|
|
9
|
+
/** The value at which the work is done. Default 100. */
|
|
10
|
+
max?: number;
|
|
11
|
+
/** The value at which the work starts. Default 0. */
|
|
12
|
+
min?: number;
|
|
13
|
+
/**
|
|
14
|
+
* How many segments the bar has (default 20, at least 2). A segment
|
|
15
|
+
* lights once the value has passed its whole share, so the bar never
|
|
16
|
+
* shows more than has been done.
|
|
17
|
+
*/
|
|
18
|
+
segments?: number;
|
|
19
|
+
/**
|
|
20
|
+
* A line above the bar saying what is in progress, as written (never
|
|
21
|
+
* caps: it may hold a file or project name). It names the progress
|
|
22
|
+
* bar; without it, pass `aria-label` or `aria-labelledby`.
|
|
23
|
+
*/
|
|
24
|
+
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The value in words, such as `3 of 12 files`. Shown at the end of the
|
|
27
|
+
* label line and read as `aria-valuetext`. Without it both give the
|
|
28
|
+
* percentage done, rounded down.
|
|
29
|
+
*/
|
|
30
|
+
valueText?: string;
|
|
31
|
+
};
|
|
32
|
+
declare const Progress: import("svelte").Component<ProgressProps, {}, "ref">;
|
|
33
|
+
type Progress = ReturnType<typeof Progress>;
|
|
34
|
+
export default Progress;
|