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