@mystravil/hardpoint 0.2.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 (52) hide show
  1. package/CHANGELOG.md +42 -38
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -625
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +174 -174
  7. package/dist/components/Checkbox.svelte +235 -235
  8. package/dist/components/CheckboxGroup.svelte +207 -207
  9. package/dist/components/Combobox.svelte +901 -901
  10. package/dist/components/Dialog.svelte +359 -359
  11. package/dist/components/Disclosure.svelte +201 -201
  12. package/dist/components/EmptyState.svelte +88 -88
  13. package/dist/components/ErrorState.svelte +135 -135
  14. package/dist/components/ImageViewer.svelte +764 -764
  15. package/dist/components/KeyValueList.svelte +114 -114
  16. package/dist/components/ListDetail.svelte +218 -218
  17. package/dist/components/LoadRegion.svelte +219 -219
  18. package/dist/components/MediaFrame.svelte +110 -110
  19. package/dist/components/MediaImage.svelte +256 -256
  20. package/dist/components/Menu.svelte +258 -258
  21. package/dist/components/MenuCheckboxItem.svelte +119 -119
  22. package/dist/components/MenuGroup.svelte +50 -50
  23. package/dist/components/MenuItem.svelte +94 -94
  24. package/dist/components/MenuRadioGroup.svelte +62 -62
  25. package/dist/components/MenuRadioItem.svelte +110 -110
  26. package/dist/components/MenuSeparator.svelte +28 -28
  27. package/dist/components/Meter.svelte +283 -283
  28. package/dist/components/NativeSelect.svelte +135 -135
  29. package/dist/components/Notice.svelte +262 -262
  30. package/dist/components/Popover.svelte +222 -222
  31. package/dist/components/Progress.svelte +157 -157
  32. package/dist/components/RadioGroup.svelte +440 -440
  33. package/dist/components/Select.svelte +471 -471
  34. package/dist/components/Skeleton.svelte +149 -149
  35. package/dist/components/Spinner.svelte +118 -118
  36. package/dist/components/Stat.svelte +253 -253
  37. package/dist/components/Steps.svelte +189 -189
  38. package/dist/components/Switch.svelte +222 -222
  39. package/dist/components/Table.svelte +504 -504
  40. package/dist/components/Tabs.svelte +246 -246
  41. package/dist/components/Tag.svelte +40 -40
  42. package/dist/components/TextArea.svelte +272 -272
  43. package/dist/components/TextField.svelte +256 -256
  44. package/dist/components/Timeline.svelte +156 -156
  45. package/dist/components/Toaster.svelte +411 -411
  46. package/dist/components/Tooltip.svelte +200 -200
  47. package/dist/components/TooltipGroup.svelte +46 -46
  48. package/dist/components/internal/Scrim.svelte +52 -52
  49. package/dist/components/internal/Sheet.svelte +263 -263
  50. package/dist/fonts.css +71 -71
  51. package/dist/tokens.css +283 -283
  52. 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>