@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.
Files changed (115) hide show
  1. package/AGENTS.md +6 -6
  2. package/dist/README.md +1 -1
  3. package/dist/attachments/index.d.ts +1 -0
  4. package/dist/attachments/index.js +1 -0
  5. package/dist/attachments/long-press.d.ts +43 -0
  6. package/dist/attachments/long-press.js +107 -0
  7. package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
  8. package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
  9. package/dist/components/Breadcrumbs/README.md +174 -0
  10. package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
  11. package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
  12. package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
  13. package/dist/components/Breadcrumbs/i18n.js +40 -0
  14. package/dist/components/Breadcrumbs/index.css +84 -0
  15. package/dist/components/Breadcrumbs/index.d.ts +4 -0
  16. package/dist/components/Breadcrumbs/index.js +4 -0
  17. package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
  18. package/dist/components/Breadcrumbs/json-ld.js +56 -0
  19. package/dist/components/Collapsible/Collapsible.svelte +84 -15
  20. package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
  21. package/dist/components/Collapsible/README.md +82 -23
  22. package/dist/components/Collapsible/index.css +38 -0
  23. package/dist/components/Collapsible/index.d.ts +1 -1
  24. package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
  25. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
  26. package/dist/components/ContextMenu/README.md +159 -0
  27. package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
  28. package/dist/components/ContextMenu/i18n-sk.js +18 -0
  29. package/dist/components/ContextMenu/i18n.d.ts +30 -0
  30. package/dist/components/ContextMenu/i18n.js +38 -0
  31. package/dist/components/ContextMenu/index.css +26 -0
  32. package/dist/components/ContextMenu/index.d.ts +3 -0
  33. package/dist/components/ContextMenu/index.js +3 -0
  34. package/dist/components/CopyButton/CopyButton.svelte +278 -0
  35. package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
  36. package/dist/components/CopyButton/README.md +241 -0
  37. package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
  38. package/dist/components/CopyButton/i18n-sk.js +20 -0
  39. package/dist/components/CopyButton/i18n.d.ts +35 -0
  40. package/dist/components/CopyButton/i18n.js +43 -0
  41. package/dist/components/CopyButton/index.css +67 -0
  42. package/dist/components/CopyButton/index.d.ts +3 -0
  43. package/dist/components/CopyButton/index.js +3 -0
  44. package/dist/components/DataTable/DataTable.svelte +41 -34
  45. package/dist/components/DataTable/README.md +45 -0
  46. package/dist/components/DataTable/i18n-sk.js +1 -0
  47. package/dist/components/DataTable/i18n.d.ts +1 -0
  48. package/dist/components/DataTable/i18n.js +3 -0
  49. package/dist/components/DataTable/index.css +41 -3
  50. package/dist/components/EmptyState/EmptyState.svelte +121 -0
  51. package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
  52. package/dist/components/EmptyState/README.md +188 -0
  53. package/dist/components/EmptyState/index.css +154 -0
  54. package/dist/components/EmptyState/index.d.ts +1 -0
  55. package/dist/components/EmptyState/index.js +1 -0
  56. package/dist/components/Input/FieldOptions.svelte +92 -2
  57. package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
  58. package/dist/components/Input/README.md +66 -31
  59. package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
  60. package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
  61. package/dist/components/Input/index.css +58 -0
  62. package/dist/components/Pagination/Pagination.svelte +250 -0
  63. package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
  64. package/dist/components/Pagination/README.md +152 -0
  65. package/dist/components/Pagination/i18n-sk.d.ts +17 -0
  66. package/dist/components/Pagination/i18n-sk.js +24 -0
  67. package/dist/components/Pagination/i18n.d.ts +41 -0
  68. package/dist/components/Pagination/i18n.js +49 -0
  69. package/dist/components/Pagination/index.css +50 -0
  70. package/dist/components/Pagination/index.d.ts +4 -0
  71. package/dist/components/Pagination/index.js +4 -0
  72. package/dist/components/Pagination/pagination-range.d.ts +21 -0
  73. package/dist/components/Pagination/pagination-range.js +41 -0
  74. package/dist/components/Pill/Pill.svelte +8 -2
  75. package/dist/components/Pill/Pill.svelte.d.ts +2 -0
  76. package/dist/components/Pill/README.md +35 -22
  77. package/dist/components/Pill/index.css +57 -18
  78. package/dist/components/Stat/README.md +157 -0
  79. package/dist/components/Stat/Stat.svelte +226 -0
  80. package/dist/components/Stat/Stat.svelte.d.ts +54 -0
  81. package/dist/components/Stat/index.css +217 -0
  82. package/dist/components/Stat/index.d.ts +1 -0
  83. package/dist/components/Stat/index.js +1 -0
  84. package/dist/components/Stepper/README.md +181 -0
  85. package/dist/components/Stepper/Stepper.svelte +240 -0
  86. package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
  87. package/dist/components/Stepper/i18n-sk.d.ts +17 -0
  88. package/dist/components/Stepper/i18n-sk.js +21 -0
  89. package/dist/components/Stepper/i18n.d.ts +35 -0
  90. package/dist/components/Stepper/i18n.js +43 -0
  91. package/dist/components/Stepper/index.css +302 -0
  92. package/dist/components/Stepper/index.d.ts +3 -0
  93. package/dist/components/Stepper/index.js +3 -0
  94. package/dist/components/TrendChart/README.md +123 -0
  95. package/dist/components/TrendChart/TrendChart.svelte +114 -0
  96. package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
  97. package/dist/components/TrendChart/index.css +51 -0
  98. package/dist/components/TrendChart/index.d.ts +1 -0
  99. package/dist/components/TrendChart/index.js +1 -0
  100. package/dist/icons/index.d.ts +4 -0
  101. package/dist/icons/index.js +4 -0
  102. package/dist/index.css +7 -0
  103. package/dist/index.d.ts +7 -0
  104. package/dist/index.js +12 -0
  105. package/dist/utils/copy-to-clipboard.d.ts +43 -0
  106. package/dist/utils/copy-to-clipboard.js +102 -0
  107. package/dist/utils/index.d.ts +1 -0
  108. package/dist/utils/index.js +1 -0
  109. package/docs/architecture.md +1 -1
  110. package/docs/domains/attachments.md +41 -4
  111. package/docs/domains/components.md +280 -13
  112. package/docs/domains/utils.md +8 -6
  113. package/docs/maybe-todo.md +132 -0
  114. package/docs/upgrading.md +6 -0
  115. 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 trigger.
233
- let triggerRef: FieldLikeButton | undefined = $state();
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
- | `renderOptionLabel` | `(item: Item) => string` | - | Custom option label renderer |
424
- | `renderOptionGroup` | `(s: string) => string` | - | Custom optgroup label renderer |
425
- | `renderValue` | `(stringifiedItems: string) => string` | - | Custom value display renderer |
426
- | `showIconsCheckbox` | `boolean` | `true` | Show checkbox icons in multi-select |
427
- | `showIconsRadio` | `boolean` | `false` | Show radio icons in single-select |
428
- | `searchPlaceholder` | `string` | - | Custom search placeholder |
429
- | `itemIdPropName` | `string` | `"id"` | Property name for item ID |
430
- | `notifications` | `NotificationsStack` | - | Notification handler for errors |
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 | Default | Description |
446
- | ------------------------------------------ | -------------------------------- | ------------------------------- |
447
- | `--stuic-field-options-divider` | `--stuic-color-border` | Divider/separator color |
448
- | `--stuic-field-options-control-text` | `--stuic-color-muted-foreground` | Control button text color |
449
- | `--stuic-field-options-control-text-hover` | `--stuic-color-foreground` | Control button hover text color |
450
- | `--stuic-field-options-control-ring` | `--stuic-color-ring` | Control button focus ring |
451
- | `--stuic-field-options-muted-text` | `--stuic-color-muted-foreground` | Muted/secondary text color |
452
- | `--stuic-field-options-optgroup-text` | `--stuic-color-muted-foreground` | Option group label color |
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