@dryui/ui 1.4.0 → 1.5.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.
@@ -34,21 +34,15 @@
34
34
 
35
35
  <style>
36
36
  [data-app-frame] {
37
- --dry-app-frame-bg: var(--dry-color-bg-base);
38
- --dry-app-frame-chrome-bg: var(--dry-color-bg-raised);
39
- --dry-app-frame-border: var(--dry-color-stroke-weak);
40
- --dry-app-frame-radius: var(--dry-radius-xl);
41
- --dry-app-frame-dot-size: 0.75rem;
42
- --dry-app-frame-dot-close: #ff5f56;
43
- --dry-app-frame-dot-min: #ffbd2e;
44
- --dry-app-frame-dot-max: #27c93f;
45
-
46
37
  display: grid;
47
38
  grid-template-rows: auto minmax(0, 1fr);
48
- border: 1px solid var(--dry-app-frame-border);
49
- border-radius: var(--dry-app-frame-radius);
50
- background: var(--dry-app-frame-bg);
39
+ border: 1px solid var(--dry-app-frame-border, var(--dry-color-stroke-weak));
40
+ border-radius: var(--dry-app-frame-radius, var(--dry-radius-xl));
41
+ background: var(--dry-app-frame-bg, var(--dry-color-bg-base));
51
42
  overflow: clip;
43
+ transition:
44
+ background-color var(--dry-app-frame-transition, 0s) ease,
45
+ border-color var(--dry-app-frame-transition, 0s) ease;
52
46
  }
53
47
 
54
48
  [data-part='chrome'] {
@@ -57,8 +51,11 @@
57
51
  grid-template-rows: auto;
58
52
  align-items: center;
59
53
  padding: var(--dry-app-frame-chrome-padding, var(--dry-space-3) var(--dry-space-4));
60
- border-block-end: 1px solid var(--dry-app-frame-border);
61
- background: var(--dry-app-frame-chrome-bg);
54
+ border-block-end: 1px solid var(--dry-app-frame-border, var(--dry-color-stroke-weak));
55
+ background: var(--dry-app-frame-chrome-bg, var(--dry-color-bg-raised));
56
+ transition:
57
+ background-color var(--dry-app-frame-transition, 0s) ease,
58
+ border-color var(--dry-app-frame-transition, 0s) ease;
62
59
  }
63
60
 
64
61
  [data-part='chrome'] > * {
@@ -77,22 +74,23 @@
77
74
 
78
75
  [data-part='dot'] {
79
76
  display: block;
80
- block-size: var(--dry-app-frame-dot-size);
77
+ block-size: var(--dry-app-frame-dot-size, 0.75rem);
81
78
  aspect-ratio: 1;
82
79
  border-radius: var(--dry-radius-full);
83
80
  background: var(--dry-color-stroke-weak);
81
+ transition: background-color var(--dry-app-frame-transition, 0s) ease;
84
82
  }
85
83
 
86
84
  [data-part='dot'][data-tone='close'] {
87
- background: var(--dry-app-frame-dot-close);
85
+ background: var(--dry-app-frame-dot-close, #ff5f56);
88
86
  }
89
87
 
90
88
  [data-part='dot'][data-tone='min'] {
91
- background: var(--dry-app-frame-dot-min);
89
+ background: var(--dry-app-frame-dot-min, #ffbd2e);
92
90
  }
93
91
 
94
92
  [data-part='dot'][data-tone='max'] {
95
- background: var(--dry-app-frame-dot-max);
93
+ background: var(--dry-app-frame-dot-max, #27c93f);
96
94
  }
97
95
 
98
96
  [data-part='title'] {
@@ -122,4 +120,12 @@
122
120
  padding: var(--dry-app-frame-content-padding, 0);
123
121
  min-block-size: 0;
124
122
  }
123
+
124
+ @media (prefers-reduced-motion: reduce) {
125
+ [data-app-frame],
126
+ [data-part='chrome'],
127
+ [data-part='dot'] {
128
+ transition: none;
129
+ }
130
+ }
125
131
  </style>
@@ -25,6 +25,7 @@
25
25
  }: Props = $props();
26
26
 
27
27
  const ctx = getComboboxCtx();
28
+ const getTriggerEl = () => ctx.triggerEl ?? ctx.inputEl;
28
29
 
29
30
  let el = $state<HTMLDivElement | null>(null);
30
31
 
@@ -39,7 +40,7 @@
39
40
  }
40
41
 
41
42
  const popover = createAnchoredPopover({
42
- triggerEl: () => ctx.inputEl,
43
+ triggerEl: getTriggerEl,
43
44
  contentEl: () => el ?? null,
44
45
  open: () => ctx.open,
45
46
  placement: () => placement,
@@ -51,7 +52,7 @@
51
52
  escapeKey: false,
52
53
  onDismiss: () => ctx.close(),
53
54
  contentEl: () => el ?? null,
54
- triggerEl: () => ctx.inputEl
55
+ triggerEl: getTriggerEl
55
56
  });
56
57
  </script>
57
58
 
@@ -87,7 +88,10 @@
87
88
  margin: 0;
88
89
 
89
90
  display: grid;
90
- grid-template-columns: minmax(max(12rem, anchor-size(inline)), max-content);
91
+ grid-template-columns: minmax(
92
+ max(var(--dry-combobox-content-min-inline-size, 12rem), anchor-size(inline)),
93
+ max-content
94
+ );
91
95
  background: var(--dry-color-bg-overlay);
92
96
  border: 1px solid var(--dry-color-stroke-weak);
93
97
  border-radius: var(--dry-radius-md);
@@ -26,6 +26,7 @@
26
26
  let inputValue = $state('');
27
27
  let activeIndex = $state(-1);
28
28
  let inputEl = $state<HTMLInputElement | null>(null);
29
+ let triggerEl = $state<HTMLElement | null>(null);
29
30
 
30
31
  setComboboxCtx({
31
32
  get open() {
@@ -54,6 +55,12 @@
54
55
  set inputEl(element: HTMLInputElement | null) {
55
56
  inputEl = element;
56
57
  },
58
+ get triggerEl() {
59
+ return triggerEl;
60
+ },
61
+ set triggerEl(element: HTMLElement | null) {
62
+ triggerEl = element;
63
+ },
57
64
  show() {
58
65
  if (!disabled) open = true;
59
66
  },
@@ -93,6 +100,7 @@
93
100
  [data-combobox-wrapper] {
94
101
  container-type: inline-size;
95
102
  display: grid;
103
+ grid-template-columns: minmax(0, 1fr);
96
104
  position: relative;
97
105
  }
98
106
  </style>
@@ -1,23 +1,42 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLInputAttributes } from 'svelte/elements';
3
4
  import { getComboboxCtx } from './context.svelte.js';
4
5
 
5
6
  interface Props extends Omit<HTMLInputAttributes, 'size'> {
6
7
  placeholder?: string;
7
8
  size?: 'sm' | 'md' | 'lg';
9
+ leading?: Snippet;
10
+ trailing?: Snippet;
8
11
  }
9
12
 
10
13
  let {
11
14
  class: className,
15
+ style,
12
16
  size = 'md',
13
17
  placeholder = '',
18
+ leading,
19
+ trailing,
20
+ disabled = false,
14
21
  oninput,
15
22
  onkeydown,
16
23
  onfocus,
24
+ 'aria-invalid': ariaInvalid,
25
+ 'data-invalid': dataInvalid,
17
26
  ...rest
18
27
  }: Props = $props();
19
28
 
20
29
  const ctx = getComboboxCtx();
30
+ const hasLeading = $derived(Boolean(leading));
31
+ const hasTrailing = $derived(Boolean(trailing));
32
+ const isDisabled = $derived(Boolean(ctx.disabled || disabled));
33
+ const isInvalid = $derived(
34
+ isTruthyInvalidValue(ariaInvalid) || isTruthyInvalidValue(dataInvalid)
35
+ );
36
+
37
+ function isTruthyInvalidValue(value: unknown): boolean {
38
+ return value != null && value !== false && value !== 'false';
39
+ }
21
40
 
22
41
  function attachInput(node: HTMLInputElement) {
23
42
  ctx.inputEl = node;
@@ -28,6 +47,15 @@
28
47
  };
29
48
  }
30
49
 
50
+ function attachTrigger(node: HTMLElement) {
51
+ ctx.triggerEl = node;
52
+ return () => {
53
+ if (ctx.triggerEl === node) {
54
+ ctx.triggerEl = null;
55
+ }
56
+ };
57
+ }
58
+
31
59
  function getOptionItems(contentEl: HTMLElement | null): HTMLElement[] {
32
60
  if (!contentEl) return [];
33
61
  return Array.from(
@@ -42,7 +70,7 @@
42
70
  function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
43
71
  const val = e.currentTarget.value;
44
72
  ctx.setInputValue(val);
45
- ctx.setActiveIndex(-1);
73
+ if (ctx.activeIndex !== -1) ctx.setActiveIndex(-1);
46
74
  if (!ctx.open) ctx.show();
47
75
  if (oninput) (oninput as (e: Event & { currentTarget: HTMLInputElement }) => void)(e);
48
76
  }
@@ -53,8 +81,8 @@
53
81
  }
54
82
 
55
83
  function handleKeydown(e: KeyboardEvent & { currentTarget: HTMLInputElement }) {
56
- const contentEl = getContentEl();
57
- const items = getOptionItems(contentEl);
84
+ let cachedItems: HTMLElement[] | undefined;
85
+ const items = () => (cachedItems ??= getOptionItems(getContentEl()));
58
86
 
59
87
  switch (e.key) {
60
88
  case 'ArrowDown': {
@@ -63,7 +91,7 @@
63
91
  ctx.show();
64
92
  ctx.setActiveIndex(0);
65
93
  } else {
66
- const next = ctx.activeIndex < items.length - 1 ? ctx.activeIndex + 1 : 0;
94
+ const next = ctx.activeIndex < items().length - 1 ? ctx.activeIndex + 1 : 0;
67
95
  ctx.setActiveIndex(next);
68
96
  }
69
97
  break;
@@ -72,17 +100,17 @@
72
100
  e.preventDefault();
73
101
  if (!ctx.open) {
74
102
  ctx.show();
75
- ctx.setActiveIndex(items.length - 1);
103
+ ctx.setActiveIndex(items().length - 1);
76
104
  } else {
77
- const prev = ctx.activeIndex > 0 ? ctx.activeIndex - 1 : items.length - 1;
105
+ const prev = ctx.activeIndex > 0 ? ctx.activeIndex - 1 : items().length - 1;
78
106
  ctx.setActiveIndex(prev);
79
107
  }
80
108
  break;
81
109
  }
82
110
  case 'Enter': {
83
111
  e.preventDefault();
84
- if (ctx.open && ctx.activeIndex >= 0 && items[ctx.activeIndex]) {
85
- items[ctx.activeIndex]!.click();
112
+ if (ctx.open && ctx.activeIndex >= 0) {
113
+ items()[ctx.activeIndex]?.click();
86
114
  }
87
115
  break;
88
116
  }
@@ -93,21 +121,25 @@
93
121
  break;
94
122
  }
95
123
  case 'Home': {
96
- if (!ctx.open || items.length === 0) break;
124
+ if (!ctx.open || items().length === 0) break;
97
125
  e.preventDefault();
98
126
  ctx.setActiveIndex(0);
99
127
  break;
100
128
  }
101
129
  case 'End': {
102
- if (!ctx.open || items.length === 0) break;
130
+ if (!ctx.open) break;
131
+ const list = items();
132
+ if (list.length === 0) break;
103
133
  e.preventDefault();
104
- ctx.setActiveIndex(items.length - 1);
134
+ ctx.setActiveIndex(list.length - 1);
105
135
  break;
106
136
  }
107
137
  case 'Tab': {
108
138
  if (ctx.open) {
109
- if (ctx.activeIndex >= 0 && items[ctx.activeIndex]) {
110
- items[ctx.activeIndex]!.click();
139
+ if (ctx.activeIndex >= 0) {
140
+ const item = items()[ctx.activeIndex];
141
+ if (item) item.click();
142
+ else ctx.close();
111
143
  } else {
112
144
  ctx.close();
113
145
  }
@@ -121,63 +153,106 @@
121
153
  }
122
154
  </script>
123
155
 
124
- <input
125
- {@attach attachInput}
126
- id={ctx.inputId}
127
- type="text"
128
- role="combobox"
129
- aria-expanded={ctx.open}
130
- aria-autocomplete="list"
131
- aria-controls={ctx.contentId}
132
- aria-activedescendant={ctx.activeIndex >= 0
133
- ? `${ctx.contentId}-item-${ctx.activeIndex}`
134
- : undefined}
156
+ <label
157
+ {@attach attachTrigger}
135
158
  data-combobox-input
136
159
  data-size={size}
137
160
  data-state={ctx.open ? 'open' : 'closed'}
138
- disabled={ctx.disabled || undefined}
139
- data-disabled={ctx.disabled || undefined}
140
- value={ctx.inputValue}
141
- {placeholder}
142
- autocomplete="off"
161
+ data-disabled={isDisabled || undefined}
162
+ data-invalid={isInvalid || undefined}
163
+ data-has-leading={hasLeading || undefined}
164
+ data-has-trailing={hasTrailing || undefined}
143
165
  class={className}
144
- oninput={handleInput}
145
- onkeydown={handleKeydown}
146
- onfocus={handleFocus}
147
- {...rest}
148
- />
166
+ {style}
167
+ >
168
+ {#if leading}
169
+ <span data-part="leading" aria-hidden="true">{@render leading()}</span>
170
+ {/if}
171
+
172
+ <input
173
+ {@attach attachInput}
174
+ id={ctx.inputId}
175
+ type="text"
176
+ role="combobox"
177
+ aria-expanded={ctx.open}
178
+ aria-autocomplete="list"
179
+ aria-controls={ctx.contentId}
180
+ aria-activedescendant={ctx.activeIndex >= 0
181
+ ? `${ctx.contentId}-item-${ctx.activeIndex}`
182
+ : undefined}
183
+ aria-invalid={ariaInvalid}
184
+ data-invalid={dataInvalid}
185
+ disabled={isDisabled || undefined}
186
+ data-disabled={isDisabled || undefined}
187
+ value={ctx.inputValue}
188
+ {placeholder}
189
+ autocomplete="off"
190
+ oninput={handleInput}
191
+ onkeydown={handleKeydown}
192
+ onfocus={handleFocus}
193
+ {...rest}
194
+ />
195
+
196
+ {#if trailing}
197
+ <span data-part="trailing" aria-hidden="true">{@render trailing()}</span>
198
+ {/if}
199
+ </label>
149
200
 
150
201
  <style>
151
202
  [data-combobox-input] {
152
- --dry-combobox-bg: var(--dry-form-control-bg);
153
- --dry-combobox-border: var(--dry-form-control-border);
154
- --dry-combobox-color: var(--dry-form-control-color);
155
- --dry-combobox-padding-x: var(--dry-form-control-padding-inline);
156
- --dry-combobox-padding-y: var(--dry-form-control-padding-block);
157
- --dry-combobox-font-size: var(--dry-form-control-font-size);
203
+ --_dry-combobox-bg: var(--dry-combobox-bg, var(--dry-form-control-bg));
204
+ --_dry-combobox-border: var(--dry-combobox-border, var(--dry-form-control-border));
205
+ --_dry-combobox-color: var(--dry-combobox-color, var(--dry-form-control-color));
206
+ --_dry-combobox-padding-x: var(
207
+ --dry-combobox-padding-x,
208
+ var(--dry-form-control-padding-inline)
209
+ );
210
+ --_dry-combobox-padding-y: var(--dry-combobox-padding-y, var(--dry-form-control-padding-block));
211
+ --_dry-combobox-font-size: var(--dry-combobox-font-size, var(--dry-form-control-font-size));
212
+ --_dry-combobox-affix-gap: var(--dry-combobox-affix-gap, var(--dry-space-2));
213
+ --_dry-combobox-affix-min-inline-size: var(--dry-combobox-affix-min-inline-size, 1.5rem);
158
214
 
159
215
  display: grid;
160
- padding: var(--dry-combobox-padding-y) var(--dry-combobox-padding-x);
161
- font-size: var(--dry-combobox-font-size);
216
+ align-items: center;
217
+ grid-template-columns: minmax(0, 1fr);
218
+ padding-block: var(--_dry-combobox-padding-y);
219
+ padding-inline: var(--_dry-combobox-padding-x);
220
+ font-size: var(--_dry-combobox-font-size);
162
221
  line-height: var(--dry-type-small-leading);
163
222
  font-family: var(--dry-font-sans);
164
- color: var(--dry-combobox-color);
165
- background: var(--dry-combobox-bg);
166
- border: 1px solid var(--dry-combobox-border);
223
+ color: var(--_dry-combobox-color);
224
+ background: var(--_dry-combobox-bg);
225
+ border: 1px solid var(--_dry-combobox-border);
167
226
  border-radius: var(--dry-combobox-radius, var(--dry-form-control-radius));
168
227
  box-sizing: border-box;
169
- appearance: none;
228
+ column-gap: var(--_dry-combobox-affix-gap);
229
+ cursor: text;
170
230
  transition:
171
231
  border-color var(--dry-duration-fast) var(--dry-ease-default),
172
232
  box-shadow var(--dry-duration-fast) var(--dry-ease-default);
173
233
  }
174
234
 
235
+ [data-combobox-input][data-has-leading] {
236
+ grid-template-columns:
237
+ minmax(var(--_dry-combobox-affix-min-inline-size), max-content)
238
+ minmax(0, 1fr);
239
+ }
240
+
241
+ [data-combobox-input][data-has-trailing] {
242
+ grid-template-columns: minmax(0, 1fr) max-content;
243
+ }
244
+
245
+ [data-combobox-input][data-has-leading][data-has-trailing] {
246
+ grid-template-columns:
247
+ minmax(var(--_dry-combobox-affix-min-inline-size), max-content)
248
+ minmax(0, 1fr) max-content;
249
+ }
250
+
175
251
  [data-combobox-input]:hover:not([data-disabled]) {
176
252
  border-color: var(--dry-form-control-border-hover);
177
253
  }
178
254
 
179
- [data-combobox-input]:focus-visible,
180
- [data-combobox-input]:focus {
255
+ [data-combobox-input]:focus-within {
181
256
  outline: var(--dry-focus-ring);
182
257
  outline-offset: -1px;
183
258
  border-color: var(--dry-color-stroke-focus);
@@ -190,7 +265,6 @@
190
265
  cursor: not-allowed;
191
266
  }
192
267
 
193
- [data-combobox-input][aria-invalid='true'],
194
268
  [data-combobox-input][data-invalid] {
195
269
  --dry-combobox-bg: color-mix(
196
270
  in srgb,
@@ -200,17 +274,47 @@
200
274
  --dry-combobox-border: var(--dry-color-stroke-error);
201
275
  }
202
276
 
203
- [data-combobox-input][aria-invalid='true']:hover:not([data-disabled]),
204
277
  [data-combobox-input][data-invalid]:hover:not([data-disabled]) {
205
278
  border-color: var(--dry-color-stroke-error-strong);
206
279
  }
207
280
 
208
- [data-combobox-input][aria-invalid='true']:focus-visible,
209
- [data-combobox-input][data-invalid]:focus-visible {
281
+ [data-combobox-input][data-invalid]:focus-within {
210
282
  outline-color: var(--dry-color-fill-error);
211
283
  border-color: var(--dry-color-stroke-error);
212
284
  }
213
285
 
286
+ [data-combobox-input] input {
287
+ appearance: none;
288
+ background: transparent;
289
+ border: 0;
290
+ box-sizing: border-box;
291
+ color: inherit;
292
+ font: inherit;
293
+ line-height: inherit;
294
+ margin: 0;
295
+ outline: none;
296
+ overflow: hidden;
297
+ padding: 0;
298
+ }
299
+
300
+ [data-combobox-input] input::placeholder {
301
+ color: var(--dry-form-control-color-placeholder);
302
+ }
303
+
304
+ [data-combobox-input] input:focus,
305
+ [data-combobox-input] input:focus-visible {
306
+ outline: none;
307
+ }
308
+
309
+ [data-combobox-input] [data-part='leading'],
310
+ [data-combobox-input] [data-part='trailing'] {
311
+ color: var(--dry-color-text-weak);
312
+ display: grid;
313
+ align-items: center;
314
+ justify-items: center;
315
+ line-height: 1;
316
+ }
317
+
214
318
  /* Size variants */
215
319
  [data-combobox-input][data-size='sm'] {
216
320
  --dry-combobox-padding-x: var(--dry-space-2);
@@ -1,7 +1,10 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLInputAttributes } from 'svelte/elements';
2
3
  interface Props extends Omit<HTMLInputAttributes, 'size'> {
3
4
  placeholder?: string;
4
5
  size?: 'sm' | 'md' | 'lg';
6
+ leading?: Snippet;
7
+ trailing?: Snippet;
5
8
  }
6
9
  declare const ComboboxInput: import("svelte").Component<Props, {}, "">;
7
10
  type ComboboxInput = ReturnType<typeof ComboboxInput>;
@@ -27,6 +27,7 @@
27
27
 
28
28
  const isSelected = $derived(ctx.value === value);
29
29
  const isHighlighted = $derived(ctx.activeIndex === index);
30
+ const hasIcon = $derived(Boolean(icon));
30
31
 
31
32
  function handleClick(e: MouseEvent & { currentTarget: HTMLDivElement }) {
32
33
  if (disabled) return;
@@ -64,6 +65,7 @@
64
65
  data-state={isSelected ? 'selected' : 'unselected'}
65
66
  data-highlighted={isHighlighted || undefined}
66
67
  data-disabled={disabled || undefined}
68
+ data-has-icon={hasIcon || undefined}
67
69
  data-value={value}
68
70
  class={className}
69
71
  onclick={handleClick}
@@ -82,10 +84,10 @@
82
84
  var(--dry-control-radius, var(--dry-radius-sm)),
83
85
  var(--dry-space-4)
84
86
  );
87
+ --_dry-combobox-item-icon-size: var(--dry-combobox-item-icon-size, 1.5rem);
85
88
 
86
89
  display: grid;
87
- grid-auto-flow: column;
88
- grid-auto-columns: max-content;
90
+ grid-template-columns: minmax(0, 1fr);
89
91
  align-items: center;
90
92
  padding: var(--dry-space-2) var(--dry-space-3);
91
93
  border-radius: var(--dry-combobox-item-radius);
@@ -97,6 +99,11 @@
97
99
  min-height: var(--dry-space-10);
98
100
  }
99
101
 
102
+ [data-combobox-item][data-has-icon] {
103
+ column-gap: var(--dry-space-2);
104
+ grid-template-columns: minmax(var(--_dry-combobox-item-icon-size), max-content) minmax(0, 1fr);
105
+ }
106
+
100
107
  [data-combobox-item]:hover:not([data-disabled]),
101
108
  [data-combobox-item][data-highlighted] {
102
109
  background: var(--dry-color-fill);
@@ -121,8 +128,9 @@
121
128
  }
122
129
 
123
130
  [data-combobox-item] [data-part='icon'] {
124
- display: inline-grid;
131
+ display: grid;
125
132
  align-items: center;
126
- margin-right: var(--dry-space-2);
133
+ justify-content: center;
134
+ line-height: 1;
127
135
  }
128
136
  </style>
@@ -8,6 +8,7 @@ export interface ComboboxContext {
8
8
  readonly inputId: string;
9
9
  readonly contentId: string;
10
10
  inputEl: HTMLInputElement | null;
11
+ triggerEl: HTMLElement | null;
11
12
  show: () => void;
12
13
  close: () => void;
13
14
  toggle: () => void;
@@ -4,6 +4,8 @@ import type { ComboboxInputProps as PrimitiveComboboxInputProps } from '@dryui/p
4
4
  export type { ComboboxRootProps, ComboboxContentProps, ComboboxItemProps, ComboboxEmptyProps } from '@dryui/primitives';
5
5
  export interface ComboboxInputProps extends Omit<PrimitiveComboboxInputProps, 'size'> {
6
6
  size?: 'sm' | 'md' | 'lg';
7
+ leading?: Snippet;
8
+ trailing?: Snippet;
7
9
  }
8
10
  export interface ComboboxGroupProps extends HTMLAttributes<HTMLDivElement> {
9
11
  label: string;
@@ -22,7 +22,7 @@
22
22
 
23
23
  <div data-diagram-container data-fit={fit}>
24
24
  <svg
25
- width={fit === 'native' ? vbW : undefined}
25
+ width={fit === 'native' ? vbW : '100%'}
26
26
  height={fit === 'native' ? vbH : undefined}
27
27
  viewBox="0 0 {vbW} {vbH}"
28
28
  preserveAspectRatio="xMidYMin meet"
package/dist/index.d.ts CHANGED
@@ -248,8 +248,6 @@ export { VideoEmbed } from './video-embed/index.js';
248
248
  export type { VideoEmbedProps } from './video-embed/index.js';
249
249
  export { PhoneInput } from './phone-input/index.js';
250
250
  export type { PhoneInputProps } from './phone-input/index.js';
251
- export { CountrySelect } from './country-select/index.js';
252
- export type { CountrySelectProps } from './country-select/index.js';
253
251
  export { DateTimeInput } from './date-time-input/index.js';
254
252
  export type { DateTimeInputProps } from './date-time-input/index.js';
255
253
  export { NotificationCenter } from './notification-center/index.js';
package/dist/index.js CHANGED
@@ -127,7 +127,6 @@ export { Sparkline } from './sparkline/index.js';
127
127
  export { FlipCard } from './flip-card/index.js';
128
128
  export { VideoEmbed } from './video-embed/index.js';
129
129
  export { PhoneInput } from './phone-input/index.js';
130
- export { CountrySelect } from './country-select/index.js';
131
130
  export { DateTimeInput } from './date-time-input/index.js';
132
131
  export { NotificationCenter } from './notification-center/index.js';
133
132
  export { MegaMenu } from './mega-menu/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dryui/ui",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Zero-dependency styled Svelte 5 components with scoped styles and --dry-* CSS variable theming.",
5
5
  "author": "Rob Balfre",
6
6
  "license": "MIT",
@@ -195,11 +195,6 @@
195
195
  "svelte": "./dist/context-menu/index.js",
196
196
  "default": "./dist/context-menu/index.js"
197
197
  },
198
- "./country-select": {
199
- "types": "./dist/country-select/index.d.ts",
200
- "svelte": "./dist/country-select/index.js",
201
- "default": "./dist/country-select/index.js"
202
- },
203
198
  "./data-grid": {
204
199
  "types": "./dist/data-grid/index.d.ts",
205
200
  "svelte": "./dist/data-grid/index.js",
@@ -784,7 +779,7 @@
784
779
  "postpack": "bun ../../scripts/postpack-exports.ts"
785
780
  },
786
781
  "dependencies": {
787
- "@dryui/primitives": "^1.4.0"
782
+ "@dryui/primitives": "^1.5.0"
788
783
  },
789
784
  "peerDependencies": {
790
785
  "svelte": "^5.55.1"
@@ -116,6 +116,18 @@ The test: every `<Input>`, `<Select.Root>`, `<Textarea>` is inside a `Field.Root
116
116
 
117
117
  The test: search your markup for raw `<input`, `<select>`, `<dialog>`, `<button>`, `<hr>`, `<table>` — each should be a DryUI component instead.
118
118
 
119
+ ## 7. Ask the Svelte MCP for Svelte Questions
120
+
121
+ **DryUI owns components. `@sveltejs/mcp` owns the framework.**
122
+
123
+ For Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`), snippets, SvelteKit load fns, `+page.server.ts` shape, form actions, and anything Svelte-syntax adjacent: call the official `svelte-autofixer` and `get-documentation` tools from `@sveltejs/mcp` before guessing from memory.
124
+
125
+ - `dryui setup --install` registers `@sveltejs/mcp` by default; pass `--no-svelte-mcp` to skip.
126
+ - If it's not registered, the fallback is the remote endpoint `https://mcp.svelte.dev/mcp` or a one-liner like `claude mcp add -t stdio -s user svelte -- npx -y @sveltejs/mcp`.
127
+ - Scope split: DryUI `ask`/`check` cover component APIs, theming, composition, and validation. Svelte MCP covers the runtime, compiler, and framework idioms.
128
+
129
+ The test: before writing non-trivial Svelte 5 or SvelteKit code, did you either (a) call `svelte-autofixer` / `get-documentation`, or (b) confirm the pattern is already covered by a DryUI recipe via `ask --scope recipe`?
130
+
119
131
  ## Quick Start
120
132
 
121
133
  **1. Install the CLI** so every subsequent command is short and fast:
@@ -149,6 +161,8 @@ This works for greenfield (empty directory), brownfield (existing non-SvelteKit
149
161
  - OpenCode: `npx degit rob-balfre/dryui/packages/ui/skills/dryui .opencode/skills/dryui` + add the `dryui` and `dryui-feedback` local MCP servers in `opencode.json` (OpenCode also loads `.agents/skills/dryui` and reads `AGENTS.md`)
150
162
  - Copilot/Cursor/Windsurf: `npx degit rob-balfre/dryui/packages/ui/skills/dryui .agents/skills/dryui` + add MCP config (see https://dryui.dev/tools)
151
163
 
164
+ **5. Register the Svelte MCP companion.** `dryui setup --install` does this automatically for Copilot, Cursor, OpenCode, Windsurf, and Zed. For Claude Code run `claude mcp add -t stdio -s user svelte -- npx -y @sveltejs/mcp`; for Codex add `[mcp_servers.svelte] command = "npx", args = ["-y", "@sveltejs/mcp"]` to `~/.codex/config.toml`. See rule 7 above.
165
+
152
166
  ### Manual setup
153
167
 
154
168
  1. `bun add @dryui/ui`
@@ -1,173 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
- import { CountrySelect as PrimitiveCountrySelect } from '@dryui/primitives';
5
-
6
- interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
7
- value?: string;
8
- regions?: string[];
9
- showDialCode?: boolean;
10
- disabled?: boolean;
11
- placeholder?: string;
12
- name?: string;
13
- onchange?: (code: string) => void;
14
- children?: Snippet;
15
- }
16
-
17
- let {
18
- value = $bindable(''),
19
- regions,
20
- showDialCode = false,
21
- disabled = false,
22
- placeholder = 'Select country',
23
- name: formName,
24
- onchange,
25
- class: className,
26
- ...rest
27
- }: Props = $props();
28
- </script>
29
-
30
- <span data-country-select-wrapper class={className}>
31
- <PrimitiveCountrySelect
32
- bind:value
33
- {regions}
34
- {showDialCode}
35
- {disabled}
36
- {placeholder}
37
- name={formName}
38
- {onchange}
39
- {...rest}
40
- />
41
- </span>
42
-
43
- <style>
44
- [data-country-select-wrapper] {
45
- display: contents;
46
- }
47
-
48
- [data-country-select-wrapper] [data-part='country-select'] {
49
- display: inline-grid;
50
- grid-template-columns: minmax(14rem, max-content);
51
- }
52
-
53
- [data-country-select-wrapper] [data-part='control'] {
54
- display: grid;
55
- grid-template-columns: auto minmax(0, 1fr) auto;
56
- align-items: center;
57
- gap: var(--dry-space-2, 0.5rem);
58
- min-height: 2.75rem;
59
- padding-inline: var(--dry-space-3, 0.75rem);
60
- border: 1px solid var(--dry-color-stroke-weak);
61
- border-radius: var(--dry-radius-md);
62
- background: var(--dry-color-bg-raised);
63
- box-shadow: var(--dry-shadow-xs);
64
- transition:
65
- border-color 140ms ease,
66
- box-shadow 140ms ease,
67
- background-color 140ms ease;
68
- }
69
-
70
- [data-country-select-wrapper] [data-part='control'][data-state='open'] {
71
- border-color: color-mix(
72
- in srgb,
73
- var(--dry-color-brand-primary) 52%,
74
- var(--dry-color-stroke-weak)
75
- );
76
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dry-color-brand-primary) 18%, transparent);
77
- }
78
-
79
- [data-country-select-wrapper] [data-part='control'][data-disabled] {
80
- opacity: 0.6;
81
- }
82
-
83
- [data-country-select-wrapper] [data-part='flag'] {
84
- font-size: 1rem;
85
- line-height: 1;
86
- }
87
-
88
- [data-country-select-wrapper] [data-part='input'] {
89
- border: none;
90
- outline: none;
91
- padding: 0;
92
- background: transparent;
93
- color: var(--dry-color-text-strong);
94
- font-family: var(--dry-font-sans);
95
- font-size: var(--dry-type-body-size, var(--dry-text-md-size));
96
- line-height: var(--dry-type-body-leading, var(--dry-text-md-leading));
97
- }
98
-
99
- [data-country-select-wrapper] [data-part='input']::placeholder {
100
- color: var(--dry-color-text-weak);
101
- }
102
-
103
- [data-country-select-wrapper] [data-part='dial-code'] {
104
- color: var(--dry-color-text-weak);
105
- font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
106
- }
107
-
108
- [data-country-select-wrapper] [data-part='dropdown'] {
109
- display: grid;
110
- grid-template-columns: minmax(max(14rem, anchor-size(inline)), max-content);
111
- gap: var(--dry-space-1, 0.25rem);
112
- max-height: 18rem;
113
- padding: var(--dry-space-1, 0.25rem);
114
- border: 1px solid var(--dry-color-stroke-weak);
115
- border-radius: calc(var(--dry-radius-md) + 0.125rem);
116
- background: var(--dry-color-bg-raised);
117
- box-shadow: var(--dry-shadow-lg);
118
- inset: unset;
119
- margin: 0;
120
- }
121
-
122
- [data-country-select-wrapper] [data-part='option'] {
123
- display: grid;
124
- grid-template-columns: auto minmax(0, 1fr) auto;
125
- align-items: center;
126
- gap: var(--dry-space-2, 0.5rem);
127
- justify-self: stretch;
128
- padding: var(--dry-space-2, 0.5rem) var(--dry-space-3, 0.75rem);
129
- border: none;
130
- border-radius: calc(var(--dry-radius-sm) + 0.0625rem);
131
- background: transparent;
132
- color: var(--dry-color-text-strong);
133
- font-family: var(--dry-font-sans);
134
- font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
135
- line-height: var(--dry-type-small-leading, var(--dry-text-sm-leading));
136
- text-align: left;
137
- cursor: pointer;
138
- }
139
-
140
- [data-country-select-wrapper] [data-part='option']::before {
141
- content: attr(data-flag);
142
- font-size: 1rem;
143
- line-height: 1;
144
- }
145
-
146
- [data-country-select-wrapper] [data-part='option'][data-dial-code]::after {
147
- content: attr(data-dial-code);
148
- color: var(--dry-color-text-weak);
149
- font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
150
- }
151
-
152
- [data-country-select-wrapper] [data-part='option'][data-highlighted],
153
- [data-country-select-wrapper] [data-part='option']:hover {
154
- background: color-mix(in srgb, var(--dry-color-brand-primary) 12%, var(--dry-color-bg-raised));
155
- }
156
-
157
- [data-country-select-wrapper] [data-part='option'][data-state='selected'] {
158
- background: color-mix(in srgb, var(--dry-color-brand-primary) 18%, var(--dry-color-bg-raised));
159
- color: var(--dry-color-text-strong);
160
- }
161
-
162
- [data-country-select-wrapper] [data-part='country-name'] {
163
- overflow: hidden;
164
- text-overflow: ellipsis;
165
- white-space: nowrap;
166
- }
167
-
168
- [data-country-select-wrapper] [data-part='empty'] {
169
- padding: var(--dry-space-2, 0.5rem) var(--dry-space-3, 0.75rem);
170
- color: var(--dry-color-text-weak);
171
- font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
172
- }
173
- </style>
@@ -1,15 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { HTMLAttributes } from 'svelte/elements';
3
- interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
4
- value?: string;
5
- regions?: string[];
6
- showDialCode?: boolean;
7
- disabled?: boolean;
8
- placeholder?: string;
9
- name?: string;
10
- onchange?: (code: string) => void;
11
- children?: Snippet;
12
- }
13
- declare const CountrySelectButtonInput: import("svelte").Component<Props, {}, "value">;
14
- type CountrySelectButtonInput = ReturnType<typeof CountrySelectButtonInput>;
15
- export default CountrySelectButtonInput;
@@ -1,2 +0,0 @@
1
- export type { CountrySelectProps } from '@dryui/primitives';
2
- export { default as CountrySelect } from './country-select-button-input.svelte';
@@ -1 +0,0 @@
1
- export { default as CountrySelect } from './country-select-button-input.svelte';