@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +42 -18
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -346
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +174 -174
  7. package/dist/components/Checkbox.svelte +235 -198
  8. package/dist/components/Checkbox.svelte.d.ts +9 -2
  9. package/dist/components/CheckboxGroup.svelte +207 -99
  10. package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
  11. package/dist/components/Combobox.svelte +901 -824
  12. package/dist/components/Combobox.svelte.d.ts +10 -1
  13. package/dist/components/Dialog.svelte +359 -356
  14. package/dist/components/Disclosure.svelte +201 -201
  15. package/dist/components/EmptyState.svelte +88 -88
  16. package/dist/components/ErrorState.svelte +135 -135
  17. package/dist/components/ImageViewer.svelte +764 -0
  18. package/dist/components/ImageViewer.svelte.d.ts +55 -0
  19. package/dist/components/KeyValueList.svelte +114 -114
  20. package/dist/components/ListDetail.svelte +218 -0
  21. package/dist/components/ListDetail.svelte.d.ts +37 -0
  22. package/dist/components/LoadRegion.svelte +219 -219
  23. package/dist/components/MediaFrame.svelte +110 -110
  24. package/dist/components/MediaImage.svelte +256 -256
  25. package/dist/components/Menu.svelte +258 -258
  26. package/dist/components/MenuCheckboxItem.svelte +119 -119
  27. package/dist/components/MenuGroup.svelte +50 -50
  28. package/dist/components/MenuItem.svelte +94 -94
  29. package/dist/components/MenuRadioGroup.svelte +62 -62
  30. package/dist/components/MenuRadioItem.svelte +110 -110
  31. package/dist/components/MenuSeparator.svelte +28 -28
  32. package/dist/components/Meter.svelte +283 -0
  33. package/dist/components/Meter.svelte.d.ts +66 -0
  34. package/dist/components/NativeSelect.svelte +18 -2
  35. package/dist/components/NativeSelect.svelte.d.ts +6 -0
  36. package/dist/components/Notice.svelte +262 -0
  37. package/dist/components/Notice.svelte.d.ts +30 -0
  38. package/dist/components/Popover.svelte +222 -222
  39. package/dist/components/Progress.svelte +157 -157
  40. package/dist/components/RadioGroup.svelte +440 -0
  41. package/dist/components/RadioGroup.svelte.d.ts +50 -0
  42. package/dist/components/Select.svelte +471 -405
  43. package/dist/components/Select.svelte.d.ts +6 -0
  44. package/dist/components/Skeleton.svelte +149 -149
  45. package/dist/components/Spinner.svelte +118 -117
  46. package/dist/components/Stat.svelte +253 -253
  47. package/dist/components/Steps.svelte +189 -189
  48. package/dist/components/Switch.svelte +222 -222
  49. package/dist/components/Table.svelte +504 -504
  50. package/dist/components/Tabs.svelte +246 -246
  51. package/dist/components/Tag.svelte +40 -40
  52. package/dist/components/TextArea.svelte +272 -0
  53. package/dist/components/TextArea.svelte.d.ts +28 -0
  54. package/dist/components/TextField.svelte +256 -256
  55. package/dist/components/Timeline.svelte +156 -156
  56. package/dist/components/Toaster.svelte +411 -411
  57. package/dist/components/Tooltip.svelte +200 -200
  58. package/dist/components/TooltipGroup.svelte +46 -46
  59. package/dist/components/internal/Scrim.svelte +52 -52
  60. package/dist/components/internal/Sheet.svelte +263 -263
  61. package/dist/components/internal/groups.d.ts +14 -0
  62. package/dist/components/internal/groups.js +23 -0
  63. package/dist/fonts.css +71 -71
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.js +6 -0
  66. package/dist/tokens.css +283 -279
  67. package/package.json +81 -76
@@ -1,201 +1,201 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import type { Collapsible } from 'bits-ui';
4
-
5
- export type DisclosureTitleElement = 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
-
7
- /**
8
- * Attributes and handlers not listed here (`id`, `aria-*`, `data-*`,
9
- * `onOpenChange`) go to the root element.
10
- */
11
- export type DisclosureProps = Omit<Collapsible.RootProps, 'child' | 'children' | 'open' | 'ref' | 'title'> & {
12
- /** The trigger's text, as written. It never takes caps. */
13
- title: string;
14
- /** Whether the content shows. Bindable. */
15
- open?: boolean;
16
- /**
17
- * Wraps the trigger in a heading of this level, so the section shows
18
- * in a screen reader's heading list. Pick the level that fits the
19
- * page's outline. Without it the trigger stands alone.
20
- */
21
- titleAs?: DisclosureTitleElement;
22
- disabled?: boolean;
23
- /** The root element. */
24
- ref?: HTMLDivElement | null;
25
- /** The content. */
26
- children?: Snippet;
27
- };
28
- </script>
29
-
30
- <script lang="ts">
31
- import { untrack } from 'svelte';
32
- import { Collapsible as BitsCollapsible } from 'bits-ui';
33
- import { duration } from '../motion/transitions.js';
34
-
35
- let {
36
- title,
37
- open = $bindable(false),
38
- titleAs,
39
- disabled = false,
40
- ref = $bindable(null),
41
- class: className,
42
- children,
43
- ...rest
44
- }: DisclosureProps = $props();
45
-
46
- let body = $state<HTMLDivElement>();
47
- /** The last `open` this component saw, so only a change fades. */
48
- let was = untrack(() => open);
49
-
50
- // Height snaps: the content is there at its full size on the first
51
- // frame, and its text fades in. Opacity only, so it also runs under
52
- // reduced motion. Never on first render: `was` starts at the first
53
- // value of `open`. The duration and the curve are read now, so a token
54
- // retuned on <html> applies. A fade that is still running when the
55
- // section closes is cancelled, so closing is a cut.
56
- $effect(() => {
57
- const now = open;
58
- if (now === was) return;
59
- was = now;
60
- untrack(() => {
61
- if (!body) return;
62
- for (const a of body.getAnimations()) a.cancel();
63
- if (!now) return;
64
- const ms = duration(body, '--dur-expand');
65
- if (ms <= 0) return;
66
- const settle = getComputedStyle(body).getPropertyValue('--settle').trim();
67
- body.animate(
68
- { opacity: [0, 1] },
69
- { duration: ms, easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out' }
70
- );
71
- });
72
- });
73
- </script>
74
-
75
- {#snippet trigger()}
76
- <BitsCollapsible.Trigger class="hp-disclosure-trigger hp-dense">{title}</BitsCollapsible.Trigger>
77
- {/snippet}
78
-
79
- <!--
80
- One expandable section on Bits UI Collapsible. The trigger is a button
81
- with aria-expanded and aria-controls (the WAI disclosure pattern); with
82
- `titleAs` it sits inside a heading. Opening snaps the height and fades
83
- the content in over --dur-expand; closing cuts. Closed content stays in
84
- the document as hidden="until-found", so a browser that supports it finds
85
- text in it and opens the section (Bits UI's beforematch handler). A browser
86
- without it treats the attribute as plain hidden: the closed content is
87
- not searchable, and the trigger is the only way in. The attribute is set
88
- here, not by Bits UI, so closing hides the content in the same frame.
89
- -->
90
- <BitsCollapsible.Root bind:open bind:ref {disabled} class={['hp-disclosure', className]} {...rest}>
91
- {#if titleAs}
92
- <svelte:element this={titleAs} class="heading">{@render trigger()}</svelte:element>
93
- {:else}
94
- {@render trigger()}
95
- {/if}
96
- <BitsCollapsible.Content hiddenUntilFound>
97
- {#snippet child({ props })}
98
- <div {...props} hidden={open ? undefined : 'until-found'} class="hp-disclosure-content">
99
- <div class="body" bind:this={body}>
100
- {@render children?.()}
101
- </div>
102
- </div>
103
- {/snippet}
104
- </BitsCollapsible.Content>
105
- </BitsCollapsible.Root>
106
-
107
- <style>
108
- /* Stacked disclosures sit 8px apart. No dividers: structure from
109
- alignment. */
110
- :global(.hp-disclosure + .hp-disclosure) {
111
- margin-top: var(--space-2);
112
- }
113
-
114
- .heading {
115
- margin: 0;
116
- font: inherit;
117
- }
118
-
119
- /* A full-width row, title at the start, chevron at the end. The hover
120
- tint and the focus contour reach --bleed past the text edge, as table
121
- bands do, and the padding keeps the title on the page edge. It wraps
122
- a long title; it never truncates. */
123
- :global(.hp-disclosure-trigger) {
124
- display: flex;
125
- align-items: center;
126
- justify-content: space-between;
127
- gap: var(--space-4);
128
- box-sizing: border-box;
129
- width: calc(100% + 2 * var(--bleed));
130
- min-height: var(--control-height);
131
- margin: 0 calc(-1 * var(--bleed));
132
- padding: var(--space-2) var(--bleed);
133
- border: 0;
134
- background: transparent;
135
- color: var(--text);
136
- font-family: var(--sans);
137
- font-weight: 500;
138
- text-align: start;
139
- overflow-wrap: anywhere;
140
- cursor: pointer;
141
- touch-action: manipulation;
142
- -webkit-tap-highlight-color: transparent;
143
- }
144
-
145
- /* The select well's chevron: the same mask, 16px, --muted; it points up
146
- while open, with the list's timing (--dur-pop opening, --dur-pop-exit
147
- closing) and no turn under reduced motion. */
148
- :global(.hp-disclosure-trigger::after) {
149
- content: '';
150
- flex: none;
151
- width: 16px;
152
- height: 16px;
153
- background-color: var(--muted);
154
- mask: var(--select-chevron) center / 16px 16px no-repeat;
155
- transition: transform var(--dur-pop-exit) var(--settle);
156
- }
157
- :global(.hp-disclosure-trigger[aria-expanded='true']::after) {
158
- transform: rotate(180deg);
159
- transition-duration: var(--dur-pop);
160
- }
161
- @media (prefers-reduced-motion: reduce) {
162
- :global(.hp-disclosure-trigger::after) {
163
- transition: none;
164
- }
165
- }
166
-
167
- /* Hover: fine pointers only. The tint alone barely shows (1.03:1), so the
168
- title also underlines, as a tertiary Button's does. */
169
- @media (hover: hover) and (pointer: fine) {
170
- :global(.hp-disclosure-trigger) {
171
- transition: background-color var(--dur-hover) var(--settle);
172
- }
173
- :global(.hp-disclosure-trigger:active:not(:disabled)) {
174
- transition-duration: var(--dur-press);
175
- }
176
- :global(.hp-disclosure-trigger:hover:not(:disabled)) {
177
- background-color: var(--surface-hover);
178
- text-decoration: underline;
179
- text-decoration-thickness: 1px;
180
- text-underline-offset: 0.3em;
181
- }
182
- }
183
- /* One step past the hover tint, so a press reads on a phone too. */
184
- :global(.hp-disclosure-trigger:active:not(:disabled)) {
185
- background-color: var(--surface-raised);
186
- }
187
-
188
- :global(.hp-disclosure-trigger:disabled) {
189
- color: var(--muted);
190
- cursor: not-allowed;
191
- }
192
- :global(.hp-disclosure-trigger:disabled::after) {
193
- background-color: var(--border);
194
- }
195
-
196
- /* The padding sits on the inner box: closed content is hidden, and a
197
- hidden box still shows its own padding. */
198
- .body {
199
- padding: var(--space-1) 0 var(--space-3);
200
- }
201
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { Collapsible } from 'bits-ui';
4
+
5
+ export type DisclosureTitleElement = 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
6
+
7
+ /**
8
+ * Attributes and handlers not listed here (`id`, `aria-*`, `data-*`,
9
+ * `onOpenChange`) go to the root element.
10
+ */
11
+ export type DisclosureProps = Omit<Collapsible.RootProps, 'child' | 'children' | 'open' | 'ref' | 'title'> & {
12
+ /** The trigger's text, as written. It never takes caps. */
13
+ title: string;
14
+ /** Whether the content shows. Bindable. */
15
+ open?: boolean;
16
+ /**
17
+ * Wraps the trigger in a heading of this level, so the section shows
18
+ * in a screen reader's heading list. Pick the level that fits the
19
+ * page's outline. Without it the trigger stands alone.
20
+ */
21
+ titleAs?: DisclosureTitleElement;
22
+ disabled?: boolean;
23
+ /** The root element. */
24
+ ref?: HTMLDivElement | null;
25
+ /** The content. */
26
+ children?: Snippet;
27
+ };
28
+ </script>
29
+
30
+ <script lang="ts">
31
+ import { untrack } from 'svelte';
32
+ import { Collapsible as BitsCollapsible } from 'bits-ui';
33
+ import { duration } from '../motion/transitions.js';
34
+
35
+ let {
36
+ title,
37
+ open = $bindable(false),
38
+ titleAs,
39
+ disabled = false,
40
+ ref = $bindable(null),
41
+ class: className,
42
+ children,
43
+ ...rest
44
+ }: DisclosureProps = $props();
45
+
46
+ let body = $state<HTMLDivElement>();
47
+ /** The last `open` this component saw, so only a change fades. */
48
+ let was = untrack(() => open);
49
+
50
+ // Height snaps: the content is there at its full size on the first
51
+ // frame, and its text fades in. Opacity only, so it also runs under
52
+ // reduced motion. Never on first render: `was` starts at the first
53
+ // value of `open`. The duration and the curve are read now, so a token
54
+ // retuned on <html> applies. A fade that is still running when the
55
+ // section closes is cancelled, so closing is a cut.
56
+ $effect(() => {
57
+ const now = open;
58
+ if (now === was) return;
59
+ was = now;
60
+ untrack(() => {
61
+ if (!body) return;
62
+ for (const a of body.getAnimations()) a.cancel();
63
+ if (!now) return;
64
+ const ms = duration(body, '--dur-expand');
65
+ if (ms <= 0) return;
66
+ const settle = getComputedStyle(body).getPropertyValue('--settle').trim();
67
+ body.animate(
68
+ { opacity: [0, 1] },
69
+ { duration: ms, easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out' }
70
+ );
71
+ });
72
+ });
73
+ </script>
74
+
75
+ {#snippet trigger()}
76
+ <BitsCollapsible.Trigger class="hp-disclosure-trigger hp-dense">{title}</BitsCollapsible.Trigger>
77
+ {/snippet}
78
+
79
+ <!--
80
+ One expandable section on Bits UI Collapsible. The trigger is a button
81
+ with aria-expanded and aria-controls (the WAI disclosure pattern); with
82
+ `titleAs` it sits inside a heading. Opening snaps the height and fades
83
+ the content in over --dur-expand; closing cuts. Closed content stays in
84
+ the document as hidden="until-found", so a browser that supports it finds
85
+ text in it and opens the section (Bits UI's beforematch handler). A browser
86
+ without it treats the attribute as plain hidden: the closed content is
87
+ not searchable, and the trigger is the only way in. The attribute is set
88
+ here, not by Bits UI, so closing hides the content in the same frame.
89
+ -->
90
+ <BitsCollapsible.Root bind:open bind:ref {disabled} class={['hp-disclosure', className]} {...rest}>
91
+ {#if titleAs}
92
+ <svelte:element this={titleAs} class="heading">{@render trigger()}</svelte:element>
93
+ {:else}
94
+ {@render trigger()}
95
+ {/if}
96
+ <BitsCollapsible.Content hiddenUntilFound>
97
+ {#snippet child({ props })}
98
+ <div {...props} hidden={open ? undefined : 'until-found'} class="hp-disclosure-content">
99
+ <div class="body" bind:this={body}>
100
+ {@render children?.()}
101
+ </div>
102
+ </div>
103
+ {/snippet}
104
+ </BitsCollapsible.Content>
105
+ </BitsCollapsible.Root>
106
+
107
+ <style>
108
+ /* Stacked disclosures sit 8px apart. No dividers: structure from
109
+ alignment. */
110
+ :global(.hp-disclosure + .hp-disclosure) {
111
+ margin-top: var(--space-2);
112
+ }
113
+
114
+ .heading {
115
+ margin: 0;
116
+ font: inherit;
117
+ }
118
+
119
+ /* A full-width row, title at the start, chevron at the end. The hover
120
+ tint and the focus contour reach --bleed past the text edge, as table
121
+ bands do, and the padding keeps the title on the page edge. It wraps
122
+ a long title; it never truncates. */
123
+ :global(.hp-disclosure-trigger) {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: space-between;
127
+ gap: var(--space-4);
128
+ box-sizing: border-box;
129
+ width: calc(100% + 2 * var(--bleed));
130
+ min-height: var(--control-height);
131
+ margin: 0 calc(-1 * var(--bleed));
132
+ padding: var(--space-2) var(--bleed);
133
+ border: 0;
134
+ background: transparent;
135
+ color: var(--text);
136
+ font-family: var(--sans);
137
+ font-weight: 500;
138
+ text-align: start;
139
+ overflow-wrap: anywhere;
140
+ cursor: pointer;
141
+ touch-action: manipulation;
142
+ -webkit-tap-highlight-color: transparent;
143
+ }
144
+
145
+ /* The select well's chevron: the same mask, 16px, --muted; it points up
146
+ while open, with the list's timing (--dur-pop opening, --dur-pop-exit
147
+ closing) and no turn under reduced motion. */
148
+ :global(.hp-disclosure-trigger::after) {
149
+ content: '';
150
+ flex: none;
151
+ width: 16px;
152
+ height: 16px;
153
+ background-color: var(--muted);
154
+ mask: var(--select-chevron) center / 16px 16px no-repeat;
155
+ transition: transform var(--dur-pop-exit) var(--settle);
156
+ }
157
+ :global(.hp-disclosure-trigger[aria-expanded='true']::after) {
158
+ transform: rotate(180deg);
159
+ transition-duration: var(--dur-pop);
160
+ }
161
+ @media (prefers-reduced-motion: reduce) {
162
+ :global(.hp-disclosure-trigger::after) {
163
+ transition: none;
164
+ }
165
+ }
166
+
167
+ /* Hover: fine pointers only. The tint alone barely shows (1.03:1), so the
168
+ title also underlines, as a tertiary Button's does. */
169
+ @media (hover: hover) and (pointer: fine) {
170
+ :global(.hp-disclosure-trigger) {
171
+ transition: background-color var(--dur-hover) var(--settle);
172
+ }
173
+ :global(.hp-disclosure-trigger:active:not(:disabled)) {
174
+ transition-duration: var(--dur-press);
175
+ }
176
+ :global(.hp-disclosure-trigger:hover:not(:disabled)) {
177
+ background-color: var(--surface-hover);
178
+ text-decoration: underline;
179
+ text-decoration-thickness: 1px;
180
+ text-underline-offset: 0.3em;
181
+ }
182
+ }
183
+ /* One step past the hover tint, so a press reads on a phone too. */
184
+ :global(.hp-disclosure-trigger:active:not(:disabled)) {
185
+ background-color: var(--surface-raised);
186
+ }
187
+
188
+ :global(.hp-disclosure-trigger:disabled) {
189
+ color: var(--muted);
190
+ cursor: not-allowed;
191
+ }
192
+ :global(.hp-disclosure-trigger:disabled::after) {
193
+ background-color: var(--border);
194
+ }
195
+
196
+ /* The padding sits on the inner box: closed content is hidden, and a
197
+ hidden box still shows its own padding. */
198
+ .body {
199
+ padding: var(--space-1) 0 var(--space-3);
200
+ }
201
+ </style>
@@ -1,88 +1,88 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
-
5
- /**
6
- * `standard` (default): the title at the section-title size. `display`:
7
- * the title in Geist Pixel Square, as the screen's one display line.
8
- */
9
- export type EmptyStateVariant = 'standard' | 'display';
10
-
11
- /** The title's element: `p` by default, a heading where the state names a region of the page. */
12
- export type EmptyStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
13
-
14
- export type EmptyStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
15
- /** What is empty, as a short fixed line: `No projects yet`. Never transformed. */
16
- title: string;
17
- /** Why it is empty or what fills it, in the muted ink. */
18
- description?: string;
19
- /** The one clear next action: render a `Button` here. */
20
- action?: Snippet;
21
- titleAs?: EmptyStateTitleElement;
22
- variant?: EmptyStateVariant;
23
- };
24
- </script>
25
-
26
- <script lang="ts">
27
- let {
28
- title,
29
- description,
30
- action,
31
- titleAs = 'p',
32
- variant = 'standard',
33
- class: className,
34
- ...rest
35
- }: EmptyStateProps = $props();
36
- </script>
37
-
38
- <!--
39
- A data view with nothing to show (BRIEF, Apps and phones: States): a title,
40
- an optional description and one clear next action, aligned to the text
41
- edge with no box around them. It sets no outer margin; the page or the
42
- Table's `empty` snippet places it. Extra attributes and `class` go to the
43
- root `div.hp-empty-state`.
44
- -->
45
- <div {...rest} class={['hp-empty-state', className]} data-variant={variant}>
46
- <svelte:element
47
- this={titleAs}
48
- class={['hp-empty-state-title', variant === 'display' ? 'hp-pixel' : 'hp-section-title']}
49
- >{title}</svelte:element
50
- >
51
- {#if description}
52
- <p class="hp-empty-state-description">{description}</p>
53
- {/if}
54
- {#if action}
55
- <div class="hp-empty-state-action">{@render action()}</div>
56
- {/if}
57
- </div>
58
-
59
- <style>
60
- .hp-empty-state {
61
- display: grid;
62
- justify-items: start;
63
- min-width: 0;
64
- }
65
-
66
- .hp-empty-state-title {
67
- max-width: 100%;
68
- overflow-wrap: anywhere;
69
- }
70
-
71
- /* Reading text: 16px at every width, in the reading measure. */
72
- .hp-empty-state-description {
73
- max-width: var(--measure);
74
- margin-top: var(--space-2);
75
- color: var(--muted);
76
- font-size: var(--type-body);
77
- line-height: 24px;
78
- overflow-wrap: anywhere;
79
- }
80
-
81
- /* The action is its own group: three times the gap inside the text. */
82
- .hp-empty-state-action {
83
- display: flex;
84
- flex-wrap: wrap;
85
- gap: var(--space-2);
86
- margin-top: var(--space-6);
87
- }
88
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ /**
6
+ * `standard` (default): the title at the section-title size. `display`:
7
+ * the title in Geist Pixel Square, as the screen's one display line.
8
+ */
9
+ export type EmptyStateVariant = 'standard' | 'display';
10
+
11
+ /** The title's element: `p` by default, a heading where the state names a region of the page. */
12
+ export type EmptyStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
13
+
14
+ export type EmptyStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
15
+ /** What is empty, as a short fixed line: `No projects yet`. Never transformed. */
16
+ title: string;
17
+ /** Why it is empty or what fills it, in the muted ink. */
18
+ description?: string;
19
+ /** The one clear next action: render a `Button` here. */
20
+ action?: Snippet;
21
+ titleAs?: EmptyStateTitleElement;
22
+ variant?: EmptyStateVariant;
23
+ };
24
+ </script>
25
+
26
+ <script lang="ts">
27
+ let {
28
+ title,
29
+ description,
30
+ action,
31
+ titleAs = 'p',
32
+ variant = 'standard',
33
+ class: className,
34
+ ...rest
35
+ }: EmptyStateProps = $props();
36
+ </script>
37
+
38
+ <!--
39
+ A data view with nothing to show (BRIEF, Apps and phones: States): a title,
40
+ an optional description and one clear next action, aligned to the text
41
+ edge with no box around them. It sets no outer margin; the page or the
42
+ Table's `empty` snippet places it. Extra attributes and `class` go to the
43
+ root `div.hp-empty-state`.
44
+ -->
45
+ <div {...rest} class={['hp-empty-state', className]} data-variant={variant}>
46
+ <svelte:element
47
+ this={titleAs}
48
+ class={['hp-empty-state-title', variant === 'display' ? 'hp-pixel' : 'hp-section-title']}
49
+ >{title}</svelte:element
50
+ >
51
+ {#if description}
52
+ <p class="hp-empty-state-description">{description}</p>
53
+ {/if}
54
+ {#if action}
55
+ <div class="hp-empty-state-action">{@render action()}</div>
56
+ {/if}
57
+ </div>
58
+
59
+ <style>
60
+ .hp-empty-state {
61
+ display: grid;
62
+ justify-items: start;
63
+ min-width: 0;
64
+ }
65
+
66
+ .hp-empty-state-title {
67
+ max-width: 100%;
68
+ overflow-wrap: anywhere;
69
+ }
70
+
71
+ /* Reading text: 16px at every width, in the reading measure. */
72
+ .hp-empty-state-description {
73
+ max-width: var(--measure);
74
+ margin-top: var(--space-2);
75
+ color: var(--muted);
76
+ font-size: var(--type-body);
77
+ line-height: 24px;
78
+ overflow-wrap: anywhere;
79
+ }
80
+
81
+ /* The action is its own group: three times the gap inside the text. */
82
+ .hp-empty-state-action {
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ gap: var(--space-2);
86
+ margin-top: var(--space-6);
87
+ }
88
+ </style>