@marianmeres/stuic 3.167.0 → 3.169.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/AGENTS.md +6 -6
- package/dist/README.md +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Input/FieldOptions.svelte +92 -2
- package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
- package/dist/components/Input/README.md +66 -31
- package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
- package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
- package/dist/components/Input/index.css +58 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Pill/Pill.svelte +8 -2
- package/dist/components/Pill/Pill.svelte.d.ts +2 -0
- package/dist/components/Pill/README.md +35 -22
- package/dist/components/Pill/index.css +57 -18
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +132 -0
- package/docs/upgrading.md +6 -0
- package/package.json +11 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
Icon + title + description + CTA placeholder for empty lists, tables, and search
|
|
4
|
+
results. Centered, theme-aware, and non-interactive by itself — the CTA area is a
|
|
5
|
+
snippet you fill with your own `Button`s or links. Pairs naturally with `DataTable`
|
|
6
|
+
("no rows"), search UIs ("no results"), and first-run dashboard tiles.
|
|
7
|
+
|
|
8
|
+
## Props
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| ------------------ | ---------------------------------- | --------- | ------------------------------------------------- |
|
|
12
|
+
| `icon` | `THC` | - | Icon area content (e.g. `{ html: iconSearch() }`) |
|
|
13
|
+
| `title` | `THC` | - | Title headline |
|
|
14
|
+
| `description` | `THC` | - | Short supporting text below the title |
|
|
15
|
+
| `variant` | `"plain" \| "outline" \| "dashed"` | `"plain"` | Container treatment (bordered box or none) |
|
|
16
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Padding, icon and typography scale |
|
|
17
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
18
|
+
| `class` | `string` | - | Additional CSS classes (merged via twMerge) |
|
|
19
|
+
| `classIcon` | `string` | - | Class for the icon area |
|
|
20
|
+
| `classTitle` | `string` | - | Class for the title |
|
|
21
|
+
| `classDescription` | `string` | - | Class for the description |
|
|
22
|
+
| `classActions` | `string` | - | Class for the actions area |
|
|
23
|
+
| `el` | `HTMLElement` | - | Element reference (bindable) |
|
|
24
|
+
|
|
25
|
+
## Snippet Props
|
|
26
|
+
|
|
27
|
+
| Snippet | Description |
|
|
28
|
+
| ------------ | -------------------------------------------------- |
|
|
29
|
+
| `actions` | CTA area below the description (buttons, links) |
|
|
30
|
+
| `renderIcon` | Override the icon area (instead of the `icon` THC) |
|
|
31
|
+
| `children` | Override the entire default layout |
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic
|
|
36
|
+
|
|
37
|
+
```svelte
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import { EmptyState, Button, iconSearch } from "@marianmeres/stuic";
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<EmptyState
|
|
43
|
+
icon={{ html: iconSearch() }}
|
|
44
|
+
title="No results found"
|
|
45
|
+
description="Try adjusting your search or filters."
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### With CTA
|
|
50
|
+
|
|
51
|
+
```svelte
|
|
52
|
+
<EmptyState
|
|
53
|
+
icon={{ html: iconPlus() }}
|
|
54
|
+
title="No projects yet"
|
|
55
|
+
description="Create your first project to get started."
|
|
56
|
+
>
|
|
57
|
+
{#snippet actions()}
|
|
58
|
+
<Button intent="primary">New project</Button>
|
|
59
|
+
<Button variant="ghost">Learn more</Button>
|
|
60
|
+
{/snippet}
|
|
61
|
+
</EmptyState>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Variants
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<!-- No box (default) — for use inside an already-framed area (table body, panel) -->
|
|
68
|
+
<EmptyState variant="plain" title="Nothing here" />
|
|
69
|
+
|
|
70
|
+
<!-- Bordered container -->
|
|
71
|
+
<EmptyState variant="outline" title="Nothing here" />
|
|
72
|
+
|
|
73
|
+
<!-- Dashed container — the "drop zone / placeholder" look -->
|
|
74
|
+
<EmptyState variant="dashed" title="Nothing here" />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Sizes
|
|
78
|
+
|
|
79
|
+
```svelte
|
|
80
|
+
<EmptyState size="sm" title="No comments" />
|
|
81
|
+
<EmptyState size="md" title="No comments" />
|
|
82
|
+
<EmptyState size="lg" title="No comments" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### With DataTable
|
|
86
|
+
|
|
87
|
+
`DataTable` already renders an `EmptyState` (titled with its `no_data` message) when it
|
|
88
|
+
has no rows. Its `empty` snippet replaces that one, which is where an icon, a
|
|
89
|
+
description and a CTA go:
|
|
90
|
+
|
|
91
|
+
```svelte
|
|
92
|
+
<DataTable {columns} {data}>
|
|
93
|
+
{#snippet empty()}
|
|
94
|
+
<EmptyState
|
|
95
|
+
icon={{ html: iconFile() }}
|
|
96
|
+
title="No documents"
|
|
97
|
+
description="Documents you upload will show up here."
|
|
98
|
+
>
|
|
99
|
+
{#snippet actions()}
|
|
100
|
+
<Button intent="primary">Upload</Button>
|
|
101
|
+
{/snippet}
|
|
102
|
+
</EmptyState>
|
|
103
|
+
{/snippet}
|
|
104
|
+
</DataTable>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Swapping the whole table out is the other option — when the empty case should not look
|
|
108
|
+
like a table at all (no header row, no pager):
|
|
109
|
+
|
|
110
|
+
```svelte
|
|
111
|
+
{#if !rows.length}
|
|
112
|
+
<EmptyState
|
|
113
|
+
icon={{ html: iconFile() }}
|
|
114
|
+
title="No documents"
|
|
115
|
+
description="Documents you upload will show up here."
|
|
116
|
+
>
|
|
117
|
+
{#snippet actions()}
|
|
118
|
+
<Button intent="primary">Upload</Button>
|
|
119
|
+
{/snippet}
|
|
120
|
+
</EmptyState>
|
|
121
|
+
{:else}
|
|
122
|
+
<DataTable {rows} ... />
|
|
123
|
+
{/if}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Styling
|
|
127
|
+
|
|
128
|
+
```svelte
|
|
129
|
+
<!-- Override the entire layout -->
|
|
130
|
+
<EmptyState variant="dashed">
|
|
131
|
+
<img src="/illustrations/empty.svg" alt="" class="w-40" />
|
|
132
|
+
<p>Absolutely nothing.</p>
|
|
133
|
+
</EmptyState>
|
|
134
|
+
|
|
135
|
+
<!-- Override component tokens inline -->
|
|
136
|
+
<EmptyState
|
|
137
|
+
title="Big icon"
|
|
138
|
+
icon={{ html: iconSearch() }}
|
|
139
|
+
style="--stuic-empty-state-icon-size-md: 5rem;"
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Accessibility
|
|
144
|
+
|
|
145
|
+
The component renders a plain `<div>` with no implicit role. When an empty state
|
|
146
|
+
replaces content dynamically (e.g. search results updating), pass `role="status"`
|
|
147
|
+
so screen readers announce it:
|
|
148
|
+
|
|
149
|
+
```svelte
|
|
150
|
+
<EmptyState role="status" title="No results found" />
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## CSS Variables
|
|
154
|
+
|
|
155
|
+
### Component Tokens
|
|
156
|
+
|
|
157
|
+
| Variable | Default | Description |
|
|
158
|
+
| ------------------------------------------- | -------------------------------- | -------------------------------------- |
|
|
159
|
+
| `--stuic-empty-state-gap` | `0.5rem` | Gap between icon/title/description |
|
|
160
|
+
| `--stuic-empty-state-icon-text` | `--stuic-color-muted-foreground` | Icon color |
|
|
161
|
+
| `--stuic-empty-state-icon-opacity` | `0.8` | Icon opacity |
|
|
162
|
+
| `--stuic-empty-state-icon-margin-bottom` | `0.25rem` | Extra space below the icon |
|
|
163
|
+
| `--stuic-empty-state-title-font-weight` | `--font-weight-semibold` | Title font weight |
|
|
164
|
+
| `--stuic-empty-state-title-text` | `--stuic-color-foreground` | Title color |
|
|
165
|
+
| `--stuic-empty-state-description-text` | `--stuic-color-muted-foreground` | Description color |
|
|
166
|
+
| `--stuic-empty-state-description-max-width` | `45ch` | Description line-length cap |
|
|
167
|
+
| `--stuic-empty-state-actions-gap` | `0.5rem` | Gap between CTA buttons |
|
|
168
|
+
| `--stuic-empty-state-actions-margin-top` | `1rem` | Space above the CTA row |
|
|
169
|
+
| `--stuic-empty-state-bg` | `transparent` | Background |
|
|
170
|
+
| `--stuic-empty-state-border` | `--stuic-color-border` | Border color (outline/dashed variants) |
|
|
171
|
+
| `--stuic-empty-state-border-width` | `--stuic-border-width` | Border width (outline/dashed variants) |
|
|
172
|
+
| `--stuic-empty-state-radius` | `--stuic-radius-container` | Border radius (outline/dashed) |
|
|
173
|
+
|
|
174
|
+
### Size Tokens
|
|
175
|
+
|
|
176
|
+
Each size (sm, md, lg) has corresponding tokens:
|
|
177
|
+
|
|
178
|
+
- `--stuic-empty-state-padding-{size}`
|
|
179
|
+
- `--stuic-empty-state-icon-size-{size}` (applied to any `svg` inside the icon area)
|
|
180
|
+
- `--stuic-empty-state-title-font-size-{size}`
|
|
181
|
+
- `--stuic-empty-state-description-font-size-{size}`
|
|
182
|
+
|
|
183
|
+
## Data Attributes
|
|
184
|
+
|
|
185
|
+
The component uses data attributes for styling:
|
|
186
|
+
|
|
187
|
+
- `data-variant` - `"plain" | "outline" | "dashed"`
|
|
188
|
+
- `data-size` - `"sm" | "md" | "lg"`
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
EMPTY STATE COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-empty-state-icon-size-md: 4rem; }
|
|
4
|
+
Override locally: <EmptyState style="--stuic-empty-state-gap: 1rem;">
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
/* prettier-ignore */
|
|
8
|
+
:root {
|
|
9
|
+
--stuic-empty-state-gap: 0.5rem;
|
|
10
|
+
|
|
11
|
+
/* Size: sm */
|
|
12
|
+
--stuic-empty-state-padding-sm: 1.5rem;
|
|
13
|
+
--stuic-empty-state-icon-size-sm: 2rem;
|
|
14
|
+
--stuic-empty-state-title-font-size-sm: var(--text-base);
|
|
15
|
+
--stuic-empty-state-description-font-size-sm: var(--text-xs);
|
|
16
|
+
|
|
17
|
+
/* Size: md */
|
|
18
|
+
--stuic-empty-state-padding-md: 3rem;
|
|
19
|
+
--stuic-empty-state-icon-size-md: 2.5rem;
|
|
20
|
+
--stuic-empty-state-title-font-size-md: var(--text-lg);
|
|
21
|
+
--stuic-empty-state-description-font-size-md: var(--text-sm);
|
|
22
|
+
|
|
23
|
+
/* Size: lg */
|
|
24
|
+
--stuic-empty-state-padding-lg: 4rem;
|
|
25
|
+
--stuic-empty-state-icon-size-lg: 3rem;
|
|
26
|
+
--stuic-empty-state-title-font-size-lg: var(--text-xl);
|
|
27
|
+
--stuic-empty-state-description-font-size-lg: var(--text-base);
|
|
28
|
+
|
|
29
|
+
/* Icon */
|
|
30
|
+
--stuic-empty-state-icon-text: var(--stuic-color-muted-foreground);
|
|
31
|
+
--stuic-empty-state-icon-opacity: 0.8;
|
|
32
|
+
--stuic-empty-state-icon-margin-bottom: 0.25rem;
|
|
33
|
+
|
|
34
|
+
/* Typography */
|
|
35
|
+
--stuic-empty-state-title-font-weight: var(--font-weight-semibold);
|
|
36
|
+
--stuic-empty-state-title-text: var(--stuic-color-foreground);
|
|
37
|
+
--stuic-empty-state-description-text: var(--stuic-color-muted-foreground);
|
|
38
|
+
--stuic-empty-state-description-max-width: 45ch;
|
|
39
|
+
|
|
40
|
+
/* Actions */
|
|
41
|
+
--stuic-empty-state-actions-gap: 0.5rem;
|
|
42
|
+
--stuic-empty-state-actions-margin-top: 1rem;
|
|
43
|
+
|
|
44
|
+
/* Outline/dashed variants */
|
|
45
|
+
--stuic-empty-state-bg: transparent;
|
|
46
|
+
--stuic-empty-state-border: var(--stuic-color-border);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@layer components {
|
|
50
|
+
/* ============================================================================
|
|
51
|
+
BASE STYLES
|
|
52
|
+
============================================================================ */
|
|
53
|
+
|
|
54
|
+
.stuic-empty-state {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
text-align: center;
|
|
60
|
+
width: 100%;
|
|
61
|
+
gap: var(--stuic-empty-state-gap);
|
|
62
|
+
background: var(--stuic-empty-state-bg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ============================================================================
|
|
66
|
+
SIZE VARIANTS
|
|
67
|
+
Each size sets the scale via internal CSS vars consumed by the child areas.
|
|
68
|
+
============================================================================ */
|
|
69
|
+
|
|
70
|
+
.stuic-empty-state[data-size="sm"] {
|
|
71
|
+
padding: var(--stuic-empty-state-padding-sm);
|
|
72
|
+
--_icon-size: var(--stuic-empty-state-icon-size-sm);
|
|
73
|
+
--_title-font-size: var(--stuic-empty-state-title-font-size-sm);
|
|
74
|
+
--_description-font-size: var(--stuic-empty-state-description-font-size-sm);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.stuic-empty-state[data-size="md"] {
|
|
78
|
+
padding: var(--stuic-empty-state-padding-md);
|
|
79
|
+
--_icon-size: var(--stuic-empty-state-icon-size-md);
|
|
80
|
+
--_title-font-size: var(--stuic-empty-state-title-font-size-md);
|
|
81
|
+
--_description-font-size: var(--stuic-empty-state-description-font-size-md);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.stuic-empty-state[data-size="lg"] {
|
|
85
|
+
padding: var(--stuic-empty-state-padding-lg);
|
|
86
|
+
--_icon-size: var(--stuic-empty-state-icon-size-lg);
|
|
87
|
+
--_title-font-size: var(--stuic-empty-state-title-font-size-lg);
|
|
88
|
+
--_description-font-size: var(--stuic-empty-state-description-font-size-lg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ============================================================================
|
|
92
|
+
CONTAINER VARIANTS
|
|
93
|
+
plain (default): no box. outline/dashed: bordered container.
|
|
94
|
+
============================================================================ */
|
|
95
|
+
|
|
96
|
+
.stuic-empty-state[data-variant="outline"],
|
|
97
|
+
.stuic-empty-state[data-variant="dashed"] {
|
|
98
|
+
border: var(--stuic-empty-state-border-width, var(--stuic-border-width)) solid
|
|
99
|
+
var(--stuic-empty-state-border);
|
|
100
|
+
border-radius: var(--stuic-empty-state-radius, var(--stuic-radius-container));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stuic-empty-state[data-variant="dashed"] {
|
|
104
|
+
border-style: dashed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ============================================================================
|
|
108
|
+
ICON AREA
|
|
109
|
+
============================================================================ */
|
|
110
|
+
|
|
111
|
+
.stuic-empty-state-icon {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
color: var(--stuic-empty-state-icon-text);
|
|
116
|
+
opacity: var(--stuic-empty-state-icon-opacity);
|
|
117
|
+
margin-bottom: var(--stuic-empty-state-icon-margin-bottom);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Normalize any passed svg icon to the size preset (like DismissibleMessage) */
|
|
121
|
+
.stuic-empty-state-icon svg {
|
|
122
|
+
width: var(--_icon-size);
|
|
123
|
+
height: var(--_icon-size);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ============================================================================
|
|
127
|
+
TITLE + DESCRIPTION
|
|
128
|
+
============================================================================ */
|
|
129
|
+
|
|
130
|
+
.stuic-empty-state-title {
|
|
131
|
+
font-size: var(--_title-font-size);
|
|
132
|
+
font-weight: var(--stuic-empty-state-title-font-weight);
|
|
133
|
+
color: var(--stuic-empty-state-title-text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.stuic-empty-state-description {
|
|
137
|
+
font-size: var(--_description-font-size);
|
|
138
|
+
color: var(--stuic-empty-state-description-text);
|
|
139
|
+
max-width: var(--stuic-empty-state-description-max-width);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ============================================================================
|
|
143
|
+
ACTIONS (CTA)
|
|
144
|
+
============================================================================ */
|
|
145
|
+
|
|
146
|
+
.stuic-empty-state-actions {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
align-items: center;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
gap: var(--stuic-empty-state-actions-gap);
|
|
152
|
+
margin-top: var(--stuic-empty-state-actions-margin-top);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as EmptyState, type Props as EmptyStateProps, type EmptyStateVariant, type EmptyStateSize, } from "./EmptyState.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as EmptyState, } from "./EmptyState.svelte";
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
type ValidationResult,
|
|
21
21
|
} from "../../actions/validate.svelte.js";
|
|
22
22
|
import type { TranslateFn } from "../../types.js";
|
|
23
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
23
24
|
import { getId } from "../../utils/get-id.js";
|
|
24
25
|
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
25
26
|
import { maybeJsonParse } from "../../utils/maybe-json-parse.js";
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
import X from "../X/X.svelte";
|
|
37
38
|
import InputWrap from "./_internal/InputWrap.svelte";
|
|
38
39
|
import FieldLikeButton from "./FieldLikeButton.svelte";
|
|
40
|
+
import FieldLikeChips, { type FieldLikeChip } from "./_internal/FieldLikeChips.svelte";
|
|
39
41
|
import ListItemButton from "../ListItemButton/ListItemButton.svelte";
|
|
40
42
|
import type { InputWrapClassProps } from "./types.js";
|
|
41
43
|
|
|
@@ -90,6 +92,18 @@
|
|
|
90
92
|
* serialized to `value` on submit. No-op for single-select. Default `false`.
|
|
91
93
|
*/
|
|
92
94
|
ordered?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Opt-in: render the current selection as inline, individually removable chips
|
|
97
|
+
* (`Pill`s) instead of the comma-joined text button. Picking still happens in the
|
|
98
|
+
* modal (opened by the trailing button, or by clicking the empty part of the row);
|
|
99
|
+
* removing a chip writes `value` immediately, without the modal. `renderValue` is
|
|
100
|
+
* ignored in this mode (labels come from `renderOptionLabel`). Default `false`.
|
|
101
|
+
*/
|
|
102
|
+
chips?: boolean;
|
|
103
|
+
/** Classes for each chip (`chips` mode only) */
|
|
104
|
+
classChip?: string;
|
|
105
|
+
/** Pill intent of the chips (`chips` mode only) */
|
|
106
|
+
chipIntent?: IntentColorKey;
|
|
93
107
|
showIconsCheckbox?: boolean;
|
|
94
108
|
showIconsRadio?: boolean;
|
|
95
109
|
searchPlaceholder?: string;
|
|
@@ -124,6 +138,10 @@
|
|
|
124
138
|
no_results: "No results found.",
|
|
125
139
|
add_new: 'Add "{{value}}"...',
|
|
126
140
|
click_add_new: "You must add the value to continue",
|
|
141
|
+
// chips display mode
|
|
142
|
+
chips_placeholder: "Nothing selected",
|
|
143
|
+
chips_open: "Choose...",
|
|
144
|
+
chips_remove: "Remove {{value}}",
|
|
127
145
|
//
|
|
128
146
|
pick_tab: "Pick",
|
|
129
147
|
arrange_tab: "Arrange ({{value}})",
|
|
@@ -214,6 +232,9 @@
|
|
|
214
232
|
renderOptionGroup = (s: string) => `${s}`.replaceAll("_", " "),
|
|
215
233
|
allowUnknown = false,
|
|
216
234
|
ordered = false,
|
|
235
|
+
chips = false,
|
|
236
|
+
classChip,
|
|
237
|
+
chipIntent,
|
|
217
238
|
showIconsCheckbox = true,
|
|
218
239
|
showIconsRadio = false,
|
|
219
240
|
searchPlaceholder,
|
|
@@ -229,8 +250,9 @@
|
|
|
229
250
|
modal = modalDialog;
|
|
230
251
|
});
|
|
231
252
|
|
|
232
|
-
// Imperative API delegates to the inner FieldLikeButton
|
|
233
|
-
|
|
253
|
+
// Imperative API delegates to the inner trigger (FieldLikeButton, or FieldLikeChips in
|
|
254
|
+
// `chips` mode) — both expose the same methods.
|
|
255
|
+
let triggerRef: FieldLikeButton | FieldLikeChips | undefined = $state();
|
|
234
256
|
|
|
235
257
|
/** Trigger validation now. Renders the inline message if invalid. */
|
|
236
258
|
export function validate(): ValidationResult | undefined {
|
|
@@ -604,6 +626,38 @@
|
|
|
604
626
|
});
|
|
605
627
|
}
|
|
606
628
|
|
|
629
|
+
// --- chips (inline display of the selection) ---
|
|
630
|
+
|
|
631
|
+
// straight from `value`, in value order — so with `ordered` the arranged order is what
|
|
632
|
+
// the user sees; labels go through the same renderer as the modal's options
|
|
633
|
+
let chipItems: FieldLikeChip[] = $derived.by(() => {
|
|
634
|
+
if (!chips) return [];
|
|
635
|
+
const parsed = maybeJsonParse(value || "[]");
|
|
636
|
+
if (!Array.isArray(parsed)) return [];
|
|
637
|
+
return parsed
|
|
638
|
+
.filter((item) => item != null)
|
|
639
|
+
.map((item: Item, i: number) => {
|
|
640
|
+
const label = _renderOptionLabel(item);
|
|
641
|
+
return {
|
|
642
|
+
// index-suffixed so a (malformed) duplicate id can't break the keyed each
|
|
643
|
+
key: `${item[itemIdPropName] ?? ""}__${i}`,
|
|
644
|
+
label,
|
|
645
|
+
removeLabel: t("chips_remove", { value: label }),
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// removing a chip is an immediate, modal-less edit of `value` + the same change
|
|
651
|
+
// notification the modal submit does (so validation re-runs)
|
|
652
|
+
function removeChip(_chip: FieldLikeChip, index: number) {
|
|
653
|
+
const parsed = maybeJsonParse(value || "[]");
|
|
654
|
+
if (!Array.isArray(parsed)) return;
|
|
655
|
+
const items = parsed.filter((item) => item != null);
|
|
656
|
+
value = JSON.stringify(items.filter((_, i) => i !== index));
|
|
657
|
+
_dispatch_change_to_owner();
|
|
658
|
+
onChange?.(value);
|
|
659
|
+
}
|
|
660
|
+
|
|
607
661
|
// "outer" submit - will set the outer bound value (always string) and close modal...
|
|
608
662
|
// further process is left on the consumer
|
|
609
663
|
function submit() {
|
|
@@ -712,6 +766,42 @@
|
|
|
712
766
|
<div>
|
|
713
767
|
{#if trigger}
|
|
714
768
|
{@render trigger({ value, modal: modalDialog })}
|
|
769
|
+
{:else if chips}
|
|
770
|
+
<FieldLikeChips
|
|
771
|
+
bind:this={triggerRef}
|
|
772
|
+
bind:value
|
|
773
|
+
bind:input={parentHiddenInputEl}
|
|
774
|
+
chips={chipItems}
|
|
775
|
+
{name}
|
|
776
|
+
class={classProp}
|
|
777
|
+
{label}
|
|
778
|
+
{description}
|
|
779
|
+
{labelLeft}
|
|
780
|
+
{labelAfter}
|
|
781
|
+
{below}
|
|
782
|
+
{labelLeftWidth}
|
|
783
|
+
{labelLeftBreakpoint}
|
|
784
|
+
{classLabel}
|
|
785
|
+
{classLabelBox}
|
|
786
|
+
{classInputBox}
|
|
787
|
+
{classInputBoxWrap}
|
|
788
|
+
{classInputBoxWrapInvalid}
|
|
789
|
+
{classDescBox}
|
|
790
|
+
{classDescBoxToggle}
|
|
791
|
+
{classBelowBox}
|
|
792
|
+
{classValidationBox}
|
|
793
|
+
{style}
|
|
794
|
+
validate={wrappedValidate}
|
|
795
|
+
{required}
|
|
796
|
+
{disabled}
|
|
797
|
+
{tabindex}
|
|
798
|
+
{classChip}
|
|
799
|
+
{chipIntent}
|
|
800
|
+
placeholder={t("chips_placeholder")}
|
|
801
|
+
openLabel={t("chips_open")}
|
|
802
|
+
onOpen={() => modalDialog?.open()}
|
|
803
|
+
onRemove={removeChip}
|
|
804
|
+
/>
|
|
715
805
|
{:else}
|
|
716
806
|
<FieldLikeButton
|
|
717
807
|
bind:this={triggerRef}
|
|
@@ -2,6 +2,7 @@ import { type Item } from "@marianmeres/item-collection";
|
|
|
2
2
|
import { type Snippet } from "svelte";
|
|
3
3
|
import { type ValidateOptions, type ValidationResult } from "../../actions/validate.svelte.js";
|
|
4
4
|
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type { IntentColorKey } from "../../utils/design-tokens.js";
|
|
5
6
|
import { ModalDialog } from "../ModalDialog/index.js";
|
|
6
7
|
import { NotificationsStack } from "../Notifications/index.js";
|
|
7
8
|
import type { THC } from "../Thc/Thc.svelte";
|
|
@@ -62,6 +63,18 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
62
63
|
* serialized to `value` on submit. No-op for single-select. Default `false`.
|
|
63
64
|
*/
|
|
64
65
|
ordered?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Opt-in: render the current selection as inline, individually removable chips
|
|
68
|
+
* (`Pill`s) instead of the comma-joined text button. Picking still happens in the
|
|
69
|
+
* modal (opened by the trailing button, or by clicking the empty part of the row);
|
|
70
|
+
* removing a chip writes `value` immediately, without the modal. `renderValue` is
|
|
71
|
+
* ignored in this mode (labels come from `renderOptionLabel`). Default `false`.
|
|
72
|
+
*/
|
|
73
|
+
chips?: boolean;
|
|
74
|
+
/** Classes for each chip (`chips` mode only) */
|
|
75
|
+
classChip?: string;
|
|
76
|
+
/** Pill intent of the chips (`chips` mode only) */
|
|
77
|
+
chipIntent?: IntentColorKey;
|
|
65
78
|
showIconsCheckbox?: boolean;
|
|
66
79
|
showIconsRadio?: boolean;
|
|
67
80
|
searchPlaceholder?: string;
|
|
@@ -408,48 +408,54 @@ Components use data attributes for CSS styling:
|
|
|
408
408
|
|
|
409
409
|
## FieldOptions
|
|
410
410
|
|
|
411
|
-
A modal-based multi-select/single-select component with search functionality, typeahead support, and option grouping.
|
|
411
|
+
A modal-based multi-select/single-select component with search functionality, typeahead support, and option grouping. The closed field can show the selection as inline removable chips (`chips`).
|
|
412
412
|
|
|
413
413
|
### Props
|
|
414
414
|
|
|
415
|
-
| Prop | Type | Default | Description
|
|
416
|
-
| ------------------- | ---------------------------------------------------------- | ---------- |
|
|
417
|
-
| `value` | `string` | `"[]"` | JSON array of selected items (bindable)
|
|
418
|
-
| `name` | `string` | - | Form field name
|
|
419
|
-
| `getOptions` | `(q: string, current: Item[]) => Promise<{found: Item[]}>` | - | Async function to fetch options
|
|
420
|
-
| `cardinality` | `number` | `Infinity` | Max selections (-1 for unlimited)
|
|
421
|
-
| `allowUnknown` | `boolean` | `false` | Allow typing custom values
|
|
422
|
-
| `ordered` | `boolean` | `false` | Opt-in: add an "Arrange" screen to manually order the selection (multi-select only)
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
| `
|
|
426
|
-
| `
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
415
|
+
| Prop | Type | Default | Description |
|
|
416
|
+
| ------------------- | ---------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- |
|
|
417
|
+
| `value` | `string` | `"[]"` | JSON array of selected items (bindable) |
|
|
418
|
+
| `name` | `string` | - | Form field name |
|
|
419
|
+
| `getOptions` | `(q: string, current: Item[]) => Promise<{found: Item[]}>` | - | Async function to fetch options |
|
|
420
|
+
| `cardinality` | `number` | `Infinity` | Max selections (-1 for unlimited) |
|
|
421
|
+
| `allowUnknown` | `boolean` | `false` | Allow typing custom values |
|
|
422
|
+
| `ordered` | `boolean` | `false` | Opt-in: add an "Arrange" screen to manually order the selection (multi-select only) |
|
|
423
|
+
| `chips` | `boolean` | `false` | Opt-in: show the selection as inline removable chips; picking still happens in the modal |
|
|
424
|
+
| `classChip` | `string` | - | Classes for each chip (`chips` mode) |
|
|
425
|
+
| `chipIntent` | `IntentColorKey` | - | Pill intent of the chips (`chips` mode) |
|
|
426
|
+
| `renderOptionLabel` | `(item: Item) => string` | - | Custom option label renderer |
|
|
427
|
+
| `renderOptionGroup` | `(s: string) => string` | - | Custom optgroup label renderer |
|
|
428
|
+
| `renderValue` | `(stringifiedItems: string) => string` | - | Custom value display renderer |
|
|
429
|
+
| `showIconsCheckbox` | `boolean` | `true` | Show checkbox icons in multi-select |
|
|
430
|
+
| `showIconsRadio` | `boolean` | `false` | Show radio icons in single-select |
|
|
431
|
+
| `searchPlaceholder` | `string` | - | Custom search placeholder |
|
|
432
|
+
| `itemIdPropName` | `string` | `"id"` | Property name for item ID |
|
|
433
|
+
| `notifications` | `NotificationsStack` | - | Notification handler for errors |
|
|
431
434
|
|
|
432
435
|
### Class Props
|
|
433
436
|
|
|
434
|
-
| Prop | Target
|
|
435
|
-
| ------------------- |
|
|
436
|
-
| `classOption` | Option item (ListItemButton)
|
|
437
|
-
| `classOptionActive` | Active/selected option
|
|
438
|
-
| `classOptgroup` | Option group label
|
|
439
|
-
| `classModalField` | Modal field wrapper
|
|
437
|
+
| Prop | Target |
|
|
438
|
+
| ------------------- | -------------------------------- |
|
|
439
|
+
| `classOption` | Option item (ListItemButton) |
|
|
440
|
+
| `classOptionActive` | Active/selected option |
|
|
441
|
+
| `classOptgroup` | Option group label |
|
|
442
|
+
| `classModalField` | Modal field wrapper |
|
|
443
|
+
| `classChip` | Each chip (Pill) in `chips` mode |
|
|
440
444
|
|
|
441
445
|
### CSS Variables
|
|
442
446
|
|
|
443
447
|
#### Component Tokens
|
|
444
448
|
|
|
445
|
-
| Variable
|
|
446
|
-
|
|
|
447
|
-
| `--stuic-field-options-divider`
|
|
448
|
-
| `--stuic-field-options-control-text`
|
|
449
|
-
| `--stuic-field-options-control-text-hover`
|
|
450
|
-
| `--stuic-field-options-control-ring`
|
|
451
|
-
| `--stuic-field-options-muted-text`
|
|
452
|
-
| `--stuic-field-options-optgroup-text`
|
|
449
|
+
| Variable | Default | Description |
|
|
450
|
+
| ---------------------------------------------- | -------------------------------- | ----------------------------------------- |
|
|
451
|
+
| `--stuic-field-options-divider` | `--stuic-color-border` | Divider/separator color |
|
|
452
|
+
| `--stuic-field-options-control-text` | `--stuic-color-muted-foreground` | Control button text color |
|
|
453
|
+
| `--stuic-field-options-control-text-hover` | `--stuic-color-foreground` | Control button hover text color |
|
|
454
|
+
| `--stuic-field-options-control-ring` | `--stuic-color-ring` | Control button focus ring |
|
|
455
|
+
| `--stuic-field-options-muted-text` | `--stuic-color-muted-foreground` | Muted/secondary text color |
|
|
456
|
+
| `--stuic-field-options-optgroup-text` | `--stuic-color-muted-foreground` | Option group label color |
|
|
457
|
+
| `--stuic-field-options-chips-gap` | `0.25rem` | Gap between chips (`chips` mode) |
|
|
458
|
+
| `--stuic-field-options-chips-placeholder-text` | `--stuic-input-placeholder` | Placeholder color of an empty `chips` row |
|
|
453
459
|
|
|
454
460
|
### Usage
|
|
455
461
|
|
|
@@ -503,6 +509,35 @@ on submit (and round-trips on reopen). Single-select fields ignore the prop.
|
|
|
503
509
|
> default contract, so the Arrange list can render selected items even when they aren't in
|
|
504
510
|
> the current search results.
|
|
505
511
|
|
|
512
|
+
### Chips display (`chips`)
|
|
513
|
+
|
|
514
|
+
By default the closed field is a button showing the selection as comma-joined text. Opt in
|
|
515
|
+
with `chips` to show it as inline, individually removable `Pill` chips instead — the tags form
|
|
516
|
+
factor. Picking still happens in the same modal: open it with the trailing button, or by
|
|
517
|
+
clicking the empty part of the row. Each chip's × removes that item immediately (writes
|
|
518
|
+
`value`, fires `onChange`, re-runs validation) without opening the modal, and keyboard focus
|
|
519
|
+
stays in the field. Chips render in `value` order, so with `ordered` they mirror the arranged
|
|
520
|
+
order. `renderValue` is ignored in this mode; labels come from `renderOptionLabel`.
|
|
521
|
+
|
|
522
|
+
```svelte
|
|
523
|
+
<FieldOptions
|
|
524
|
+
label="Tags"
|
|
525
|
+
name="tags"
|
|
526
|
+
bind:value
|
|
527
|
+
{getOptions}
|
|
528
|
+
cardinality={-1}
|
|
529
|
+
allowUnknown
|
|
530
|
+
chips
|
|
531
|
+
chipIntent="primary"
|
|
532
|
+
/>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Each chip's × is a full-height 24px square (the Pill's own touch-target floor,
|
|
536
|
+
`--stuic-pill-dismiss-min-size`), so no extra hit-area padding is needed on touch devices.
|
|
537
|
+
Typing directly into the field (an inline combobox) is deliberately not part of this mode — the
|
|
538
|
+
modal remains the single place where options are searched and picked, which is what keeps the
|
|
539
|
+
field usable under a soft keyboard.
|
|
540
|
+
|
|
506
541
|
### Customization Examples
|
|
507
542
|
|
|
508
543
|
```css
|