@dryui/ui 0.1.13 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-palette/command-palette-list.svelte +1 -0
- package/dist/command-palette/command-palette-root.svelte +1 -0
- package/dist/index.js +1 -0
- package/dist/logo-mark/index.d.ts +10 -0
- package/dist/logo-mark/index.js +1 -0
- package/dist/logo-mark/logo-mark.svelte +154 -0
- package/dist/logo-mark/logo-mark.svelte.d.ts +12 -0
- package/dist/pin-input/pin-input-root.svelte +25 -32
- package/dist/select/select-root.svelte +27 -2
- package/dist/select/select-root.svelte.d.ts +3 -1
- package/dist/time-input/time-input.svelte +55 -38
- package/package.json +12 -2
- package/skills/dryui/SKILL.md +9 -0
- package/skills/dryui/rules/composition.md +52 -3
package/dist/index.js
CHANGED
|
@@ -74,6 +74,7 @@ export { DatePicker } from './date-picker/index.js';
|
|
|
74
74
|
export { Calendar } from './calendar/index.js';
|
|
75
75
|
export { DateRangePicker } from './date-range-picker/index.js';
|
|
76
76
|
export { Listbox } from './listbox/index.js';
|
|
77
|
+
export { LogoMark } from './logo-mark/index.js';
|
|
77
78
|
export { PinInput } from './pin-input/index.js';
|
|
78
79
|
export { RangeCalendar } from './range-calendar/index.js';
|
|
79
80
|
export { Rating } from './rating/index.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
export interface LogoMarkProps extends HTMLAttributes<HTMLSpanElement> {
|
|
3
|
+
src?: string;
|
|
4
|
+
alt?: string;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
shape?: 'square' | 'rounded' | 'circle';
|
|
8
|
+
color?: 'brand' | 'neutral' | 'error' | 'warning' | 'success' | 'info';
|
|
9
|
+
}
|
|
10
|
+
export { default as LogoMark } from './logo-mark.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LogoMark } from './logo-mark.svelte';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
src?: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
fallback?: string;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
shape?: 'square' | 'rounded' | 'circle';
|
|
10
|
+
color?: 'brand' | 'neutral' | 'error' | 'warning' | 'success' | 'info';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
src,
|
|
15
|
+
alt = '',
|
|
16
|
+
fallback,
|
|
17
|
+
size = 'md',
|
|
18
|
+
shape = 'rounded',
|
|
19
|
+
color = 'neutral',
|
|
20
|
+
class: className,
|
|
21
|
+
...rest
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let failedSrc = $state<string | null>(null);
|
|
25
|
+
const showImage = $derived(Boolean(src) && src !== failedSrc);
|
|
26
|
+
|
|
27
|
+
function handleError() {
|
|
28
|
+
failedSrc = src ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getFallbackText(): string {
|
|
32
|
+
const source = (fallback ?? alt).trim();
|
|
33
|
+
if (!source) return '';
|
|
34
|
+
|
|
35
|
+
const compact = source.replace(/[^\p{L}\p{N}]+/gu, '');
|
|
36
|
+
const characters = Array.from(compact || source);
|
|
37
|
+
return characters.slice(0, 2).join('').toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<span
|
|
42
|
+
role="img"
|
|
43
|
+
aria-label={alt}
|
|
44
|
+
data-logo-mark
|
|
45
|
+
data-size={size}
|
|
46
|
+
data-shape={shape}
|
|
47
|
+
data-color={color}
|
|
48
|
+
class={className}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{#if showImage}
|
|
52
|
+
<img {src} {alt} onerror={handleError} />
|
|
53
|
+
{:else}
|
|
54
|
+
<span aria-hidden="true">{getFallbackText()}</span>
|
|
55
|
+
{/if}
|
|
56
|
+
</span>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
[data-logo-mark] {
|
|
60
|
+
--dry-logo-mark-size: 32px;
|
|
61
|
+
--dry-logo-mark-radius: var(--dry-radius-md);
|
|
62
|
+
--dry-logo-mark-bg: var(--dry-color-fill);
|
|
63
|
+
--dry-logo-mark-color: var(--dry-color-text-weak);
|
|
64
|
+
|
|
65
|
+
display: inline-grid;
|
|
66
|
+
place-items: center;
|
|
67
|
+
aspect-ratio: 1;
|
|
68
|
+
height: var(--dry-logo-mark-size);
|
|
69
|
+
border-radius: var(--dry-logo-mark-radius);
|
|
70
|
+
background: var(--dry-logo-mark-bg);
|
|
71
|
+
color: var(--dry-logo-mark-color);
|
|
72
|
+
font-family: var(--dry-font-sans);
|
|
73
|
+
font-size: var(--dry-logo-mark-font-size, var(--dry-type-small-size, var(--dry-text-sm-size)));
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
line-height: 1;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
user-select: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-logo-mark] img {
|
|
81
|
+
width: 100%;
|
|
82
|
+
height: 100%;
|
|
83
|
+
object-fit: cover;
|
|
84
|
+
border-radius: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[data-logo-mark] > span {
|
|
88
|
+
display: grid;
|
|
89
|
+
place-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* -- Shapes ------------------------------------------------------------- */
|
|
93
|
+
|
|
94
|
+
[data-shape='square'] {
|
|
95
|
+
--dry-logo-mark-radius: var(--dry-radius-sm);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[data-shape='rounded'] {
|
|
99
|
+
--dry-logo-mark-radius: var(--dry-radius-md);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-shape='circle'] {
|
|
103
|
+
--dry-logo-mark-radius: var(--dry-radius-full);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* -- Sizes -------------------------------------------------------------- */
|
|
107
|
+
|
|
108
|
+
[data-size='sm'] {
|
|
109
|
+
--dry-logo-mark-size: 24px;
|
|
110
|
+
--dry-logo-mark-font-size: var(--dry-type-tiny-size, var(--dry-text-xs-size));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[data-size='md'] {
|
|
114
|
+
--dry-logo-mark-size: 32px;
|
|
115
|
+
--dry-logo-mark-font-size: var(--dry-type-small-size, var(--dry-text-sm-size));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-size='lg'] {
|
|
119
|
+
--dry-logo-mark-size: 40px;
|
|
120
|
+
--dry-logo-mark-font-size: var(--dry-type-small-size, var(--dry-text-base-size));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* -- Colors ------------------------------------------------------------- */
|
|
124
|
+
|
|
125
|
+
[data-color='neutral'] {
|
|
126
|
+
--dry-logo-mark-bg: var(--dry-color-fill);
|
|
127
|
+
--dry-logo-mark-color: var(--dry-color-text-weak);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[data-color='brand'] {
|
|
131
|
+
--dry-logo-mark-bg: var(--dry-color-fill-brand-weak);
|
|
132
|
+
--dry-logo-mark-color: var(--dry-color-text-brand);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[data-color='error'] {
|
|
136
|
+
--dry-logo-mark-bg: var(--dry-color-fill-error-weak);
|
|
137
|
+
--dry-logo-mark-color: var(--dry-color-text-error);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[data-color='warning'] {
|
|
141
|
+
--dry-logo-mark-bg: var(--dry-color-fill-warning-weak);
|
|
142
|
+
--dry-logo-mark-color: var(--dry-color-text-warning);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[data-color='success'] {
|
|
146
|
+
--dry-logo-mark-bg: var(--dry-color-fill-success-weak);
|
|
147
|
+
--dry-logo-mark-color: var(--dry-color-text-success);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
[data-color='info'] {
|
|
151
|
+
--dry-logo-mark-bg: var(--dry-color-fill-info-weak);
|
|
152
|
+
--dry-logo-mark-color: var(--dry-color-text-info);
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
3
|
+
src?: string;
|
|
4
|
+
alt?: string;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
shape?: 'square' | 'rounded' | 'circle';
|
|
8
|
+
color?: 'brand' | 'neutral' | 'error' | 'warning' | 'success' | 'info';
|
|
9
|
+
}
|
|
10
|
+
declare const LogoMark: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type LogoMark = ReturnType<typeof LogoMark>;
|
|
12
|
+
export default LogoMark;
|
|
@@ -47,11 +47,10 @@
|
|
|
47
47
|
const isDisabled = $derived(disabled || formCtx?.disabled || false);
|
|
48
48
|
const hasError = $derived(formCtx?.hasError || false);
|
|
49
49
|
|
|
50
|
-
let inputEl: HTMLInputElement | undefined
|
|
50
|
+
let inputEl: HTMLInputElement | undefined;
|
|
51
51
|
let isFocused = $state(false);
|
|
52
52
|
let mirrorSelectionStart = $state<number | null>(null);
|
|
53
53
|
let mirrorSelectionEnd = $state<number | null>(null);
|
|
54
|
-
let completeFired = $state(false);
|
|
55
54
|
|
|
56
55
|
const validationRegex = $derived(pattern ?? (type === 'numeric' ? /^\d+$/ : /^[a-zA-Z0-9]+$/));
|
|
57
56
|
|
|
@@ -98,24 +97,28 @@
|
|
|
98
97
|
mirrorSelectionEnd = inputEl.selectionEnd;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
function captureInput(node: HTMLInputElement) {
|
|
101
|
+
inputEl = node;
|
|
102
|
+
return () => {
|
|
103
|
+
if (inputEl === node) {
|
|
104
|
+
inputEl = undefined;
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
|
-
|
|
108
|
-
return () => document.removeEventListener('selectionchange', handler);
|
|
109
|
-
});
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
109
|
+
function maybeFireComplete(previousValue: string, nextValue: string) {
|
|
110
|
+
if (nextValue.length !== length) return;
|
|
111
|
+
if (previousValue.length === length && previousValue === nextValue) return;
|
|
112
|
+
|
|
113
|
+
oncomplete?.(nextValue);
|
|
114
|
+
if (blurOnComplete) {
|
|
115
|
+
inputEl?.blur();
|
|
114
116
|
}
|
|
115
|
-
}
|
|
117
|
+
}
|
|
116
118
|
|
|
117
119
|
function handleInput(e: Event) {
|
|
118
120
|
const input = e.target as HTMLInputElement;
|
|
121
|
+
const previousValue = value;
|
|
119
122
|
let newValue = input.value;
|
|
120
123
|
|
|
121
124
|
newValue = newValue
|
|
@@ -126,18 +129,12 @@
|
|
|
126
129
|
newValue = newValue.slice(0, length);
|
|
127
130
|
value = newValue;
|
|
128
131
|
syncSelection();
|
|
129
|
-
|
|
130
|
-
if (newValue.length === length && !completeFired) {
|
|
131
|
-
completeFired = true;
|
|
132
|
-
oncomplete?.(newValue);
|
|
133
|
-
if (blurOnComplete) {
|
|
134
|
-
inputEl?.blur();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
132
|
+
maybeFireComplete(previousValue, newValue);
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
function handlePaste(e: ClipboardEvent) {
|
|
140
136
|
e.preventDefault();
|
|
137
|
+
const previousValue = value;
|
|
141
138
|
let pasted = e.clipboardData?.getData('text') ?? '';
|
|
142
139
|
if (pasteTransformer) {
|
|
143
140
|
pasted = pasteTransformer(pasted);
|
|
@@ -166,13 +163,7 @@
|
|
|
166
163
|
syncSelection();
|
|
167
164
|
});
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
completeFired = true;
|
|
171
|
-
oncomplete?.(newValue);
|
|
172
|
-
if (blurOnComplete) {
|
|
173
|
-
inputEl?.blur();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
166
|
+
maybeFireComplete(previousValue, newValue);
|
|
176
167
|
}
|
|
177
168
|
|
|
178
169
|
function handleFocus() {
|
|
@@ -212,13 +203,10 @@
|
|
|
212
203
|
const sizeAttr = $derived(`data-pin-input-${size}`);
|
|
213
204
|
</script>
|
|
214
205
|
|
|
215
|
-
<!-- svelte-ignore a11y_role_supports_aria_props -->
|
|
216
206
|
<div
|
|
217
207
|
role="group"
|
|
218
208
|
aria-label="PIN input"
|
|
219
209
|
aria-describedby={formCtx?.describedBy}
|
|
220
|
-
aria-invalid={hasError || undefined}
|
|
221
|
-
aria-errormessage={formCtx?.errorMessageId}
|
|
222
210
|
data-pin-input-root
|
|
223
211
|
data-disabled={isDisabled || undefined}
|
|
224
212
|
data-error={hasError || undefined}
|
|
@@ -230,7 +218,7 @@
|
|
|
230
218
|
{...rest}
|
|
231
219
|
>
|
|
232
220
|
<input
|
|
233
|
-
|
|
221
|
+
{@attach captureInput}
|
|
234
222
|
type="text"
|
|
235
223
|
inputmode={type === 'numeric' ? 'numeric' : 'text'}
|
|
236
224
|
autocomplete="one-time-code"
|
|
@@ -238,6 +226,9 @@
|
|
|
238
226
|
{value}
|
|
239
227
|
id={formCtx?.id}
|
|
240
228
|
aria-label="PIN input"
|
|
229
|
+
aria-describedby={formCtx?.describedBy}
|
|
230
|
+
aria-invalid={hasError || undefined}
|
|
231
|
+
aria-errormessage={formCtx?.errorMessageId}
|
|
241
232
|
aria-required={formCtx?.required || undefined}
|
|
242
233
|
disabled={isDisabled}
|
|
243
234
|
spellcheck={false}
|
|
@@ -247,6 +238,8 @@
|
|
|
247
238
|
onfocus={handleFocus}
|
|
248
239
|
onblur={handleBlur}
|
|
249
240
|
onkeydown={handleKeydown}
|
|
241
|
+
onkeyup={syncSelection}
|
|
242
|
+
onselect={syncSelection}
|
|
250
243
|
/>
|
|
251
244
|
|
|
252
245
|
{#if userChildren}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { generateFormId } from '@dryui/primitives';
|
|
4
4
|
import { setSelectCtx } from './context.svelte.js';
|
|
5
|
+
import SelectTrigger from './select-trigger.svelte';
|
|
6
|
+
import SelectValue from './select-value.svelte';
|
|
7
|
+
import SelectContent from './select-content.svelte';
|
|
8
|
+
import SelectItem from './select-item.svelte';
|
|
9
|
+
|
|
10
|
+
type SelectOption = { value: string; label: string };
|
|
5
11
|
|
|
6
12
|
interface Props {
|
|
7
13
|
open?: boolean;
|
|
@@ -9,7 +15,9 @@
|
|
|
9
15
|
disabled?: boolean;
|
|
10
16
|
name?: string;
|
|
11
17
|
class?: string;
|
|
12
|
-
|
|
18
|
+
options?: Array<string | SelectOption>;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
children?: Snippet;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
let {
|
|
@@ -18,9 +26,15 @@
|
|
|
18
26
|
disabled = false,
|
|
19
27
|
name,
|
|
20
28
|
class: className,
|
|
29
|
+
options,
|
|
30
|
+
placeholder,
|
|
21
31
|
children
|
|
22
32
|
}: Props = $props();
|
|
23
33
|
|
|
34
|
+
const normalizedOptions = $derived(
|
|
35
|
+
options?.map((opt) => (typeof opt === 'string' ? { value: opt, label: opt } : opt))
|
|
36
|
+
);
|
|
37
|
+
|
|
24
38
|
const triggerId = generateFormId('select-trigger');
|
|
25
39
|
const contentId = generateFormId('select-content');
|
|
26
40
|
|
|
@@ -59,7 +73,18 @@
|
|
|
59
73
|
</script>
|
|
60
74
|
|
|
61
75
|
<div data-select-wrapper class={className}>
|
|
62
|
-
{
|
|
76
|
+
{#if normalizedOptions && !children}
|
|
77
|
+
<SelectTrigger>
|
|
78
|
+
<SelectValue {placeholder} />
|
|
79
|
+
</SelectTrigger>
|
|
80
|
+
<SelectContent>
|
|
81
|
+
{#each normalizedOptions as opt (opt.value)}
|
|
82
|
+
<SelectItem value={opt.value}>{opt.label}</SelectItem>
|
|
83
|
+
{/each}
|
|
84
|
+
</SelectContent>
|
|
85
|
+
{:else if children}
|
|
86
|
+
{@render children()}
|
|
87
|
+
{/if}
|
|
63
88
|
|
|
64
89
|
{#if name}
|
|
65
90
|
<input type="hidden" {name} {value} disabled={disabled || undefined} />
|
|
@@ -5,7 +5,9 @@ interface Props {
|
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
name?: string;
|
|
7
7
|
class?: string;
|
|
8
|
-
|
|
8
|
+
options?: Array<string | { value: string; label: string }>;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
children?: Snippet;
|
|
9
11
|
}
|
|
10
12
|
declare const SelectRoot: import('svelte').Component<Props, {}, 'value' | 'open'>;
|
|
11
13
|
type SelectRoot = ReturnType<typeof SelectRoot>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
3
2
|
import { getFormControlCtx } from '@dryui/primitives';
|
|
4
3
|
import { Select } from '../select/index.js';
|
|
5
4
|
|
|
5
|
+
type TimeParts = {
|
|
6
|
+
hour: string;
|
|
7
|
+
minute: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
6
10
|
interface Props {
|
|
7
11
|
value?: string;
|
|
8
12
|
disabled?: boolean;
|
|
@@ -23,43 +27,46 @@
|
|
|
23
27
|
|
|
24
28
|
const ctx = getFormControlCtx();
|
|
25
29
|
const isDisabled = $derived(disabled || ctx?.disabled || false);
|
|
30
|
+
const hasError = $derived(ctx?.hasError || false);
|
|
31
|
+
const describedBy = $derived(ctx?.describedBy);
|
|
32
|
+
const errorMessageId = $derived(ctx?.hasError ? ctx?.errorMessageId : undefined);
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
$
|
|
33
|
-
|
|
34
|
-
const parts = value.split(':');
|
|
35
|
-
const h = parts[0] ?? '';
|
|
36
|
-
const m = parts[1] ?? '';
|
|
37
|
-
untrack(() => {
|
|
38
|
-
if (h !== hourStr) hourStr = h;
|
|
39
|
-
if (m !== minuteStr) minuteStr = m;
|
|
40
|
-
});
|
|
41
|
-
});
|
|
34
|
+
function parseTime(nextValue: string): TimeParts {
|
|
35
|
+
const [hour = '', minute = ''] = nextValue.split(':');
|
|
36
|
+
return { hour, minute };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let draftHour = $state('');
|
|
40
|
+
let draftMinute = $state('');
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
$
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
const parsedValue = $derived.by(() => parseTime(value));
|
|
43
|
+
const hourStr = $derived(value ? parsedValue.hour : draftHour);
|
|
44
|
+
const minuteStr = $derived(value ? parsedValue.minute : draftMinute);
|
|
45
|
+
|
|
46
|
+
function setTime(nextHour: string, nextMinute: string) {
|
|
47
|
+
draftHour = nextHour;
|
|
48
|
+
draftMinute = nextMinute;
|
|
49
|
+
|
|
50
|
+
if (nextHour && nextMinute) {
|
|
51
|
+
value = `${nextHour}:${nextMinute}`;
|
|
52
|
+
draftHour = '';
|
|
53
|
+
draftMinute = '';
|
|
54
|
+
return;
|
|
56
55
|
}
|
|
57
|
-
});
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
value = '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setHour(nextHour: string) {
|
|
61
|
+
setTime(nextHour, minuteStr);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setMinute(nextMinute: string) {
|
|
65
|
+
setTime(hourStr, nextMinute);
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
|
61
69
|
|
|
62
|
-
// Generate minute options based on step
|
|
63
70
|
const minutes = $derived.by(() => {
|
|
64
71
|
const stepMinutes = step && step >= 60 ? Math.floor(step / 60) : 1;
|
|
65
72
|
const count = Math.floor(60 / stepMinutes);
|
|
@@ -75,12 +82,16 @@
|
|
|
75
82
|
data-time-input-wrapper
|
|
76
83
|
data-disabled={isDisabled || undefined}
|
|
77
84
|
id={ctx?.id}
|
|
78
|
-
aria-describedby={[ctx?.describedBy, ctx?.hasError ? ctx?.errorMessageId : undefined].filter(Boolean).join(' ') || undefined}
|
|
79
|
-
aria-invalid={ctx?.hasError || undefined}
|
|
80
85
|
class={className}
|
|
81
86
|
>
|
|
82
|
-
<Select.Root bind:value={hourStr} disabled={isDisabled}>
|
|
83
|
-
<Select.Trigger
|
|
87
|
+
<Select.Root bind:value={() => hourStr, setHour} disabled={isDisabled}>
|
|
88
|
+
<Select.Trigger
|
|
89
|
+
{size}
|
|
90
|
+
aria-label="Hour"
|
|
91
|
+
aria-describedby={describedBy}
|
|
92
|
+
aria-invalid={hasError || undefined}
|
|
93
|
+
aria-errormessage={errorMessageId}
|
|
94
|
+
>
|
|
84
95
|
<span data-time-display data-placeholder={!hourStr ? '' : undefined}>
|
|
85
96
|
{hourStr || 'HH'}
|
|
86
97
|
</span>
|
|
@@ -94,8 +105,14 @@
|
|
|
94
105
|
|
|
95
106
|
<span data-time-separator>:</span>
|
|
96
107
|
|
|
97
|
-
<Select.Root bind:value={minuteStr} disabled={isDisabled}>
|
|
98
|
-
<Select.Trigger
|
|
108
|
+
<Select.Root bind:value={() => minuteStr, setMinute} disabled={isDisabled}>
|
|
109
|
+
<Select.Trigger
|
|
110
|
+
{size}
|
|
111
|
+
aria-label="Minute"
|
|
112
|
+
aria-describedby={describedBy}
|
|
113
|
+
aria-invalid={hasError || undefined}
|
|
114
|
+
aria-errormessage={errorMessageId}
|
|
115
|
+
>
|
|
99
116
|
<span data-time-display data-placeholder={!minuteStr ? '' : undefined}>
|
|
100
117
|
{minuteStr || 'MM'}
|
|
101
118
|
</span>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dryui/ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"author": "Rob Balfre",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -401,6 +401,11 @@
|
|
|
401
401
|
"svelte": "./dist/listbox/index.js",
|
|
402
402
|
"default": "./dist/listbox/index.js"
|
|
403
403
|
},
|
|
404
|
+
"./logo-mark": {
|
|
405
|
+
"types": "./dist/logo-mark/index.d.ts",
|
|
406
|
+
"svelte": "./dist/logo-mark/index.js",
|
|
407
|
+
"default": "./dist/logo-mark/index.js"
|
|
408
|
+
},
|
|
404
409
|
"./map": {
|
|
405
410
|
"types": "./dist/map/index.d.ts",
|
|
406
411
|
"svelte": "./dist/map/index.js",
|
|
@@ -1141,6 +1146,11 @@
|
|
|
1141
1146
|
"svelte": "./dist/listbox/index.js",
|
|
1142
1147
|
"default": "./dist/listbox/index.js"
|
|
1143
1148
|
},
|
|
1149
|
+
"./logo-mark": {
|
|
1150
|
+
"types": "./dist/logo-mark/index.d.ts",
|
|
1151
|
+
"svelte": "./dist/logo-mark/index.js",
|
|
1152
|
+
"default": "./dist/logo-mark/index.js"
|
|
1153
|
+
},
|
|
1144
1154
|
"./map": {
|
|
1145
1155
|
"types": "./dist/map/index.d.ts",
|
|
1146
1156
|
"svelte": "./dist/map/index.js",
|
|
@@ -1504,7 +1514,7 @@
|
|
|
1504
1514
|
"thumbnail:create": "bun scripts/create-thumbnail.ts"
|
|
1505
1515
|
},
|
|
1506
1516
|
"dependencies": {
|
|
1507
|
-
"@dryui/primitives": "^0.
|
|
1517
|
+
"@dryui/primitives": "^0.2.0"
|
|
1508
1518
|
},
|
|
1509
1519
|
"peerDependencies": {
|
|
1510
1520
|
"svelte": "^5.55.1"
|
package/skills/dryui/SKILL.md
CHANGED
|
@@ -31,6 +31,8 @@ Most DryUI components are compound — they require `<Card.Root>`, not `<Card>`.
|
|
|
31
31
|
<!-- Right --> <Card.Root>content</Card.Root>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Compound components include Accordion, Alert, AlertDialog, Breadcrumb, Calendar, Card, Carousel, Chart, ChipGroup, Collapsible, ColorPicker, Combobox, CommandPalette, ContextMenu, DataGrid, DateField, DatePicker, DateRangePicker, DescriptionList, Dialog, DragAndDrop, Drawer, DropdownMenu, Field, Fieldset, FileSelect, FileUpload, FlipCard, FloatButton, HoverCard, InputGroup, LinkPreview, List, Listbox, Map, MegaMenu, Menubar, MultiSelectCombobox, NavigationMenu, NotificationCenter, OptionSwatchGroup, Pagination, PinInput, Popover, RadioGroup, RangeCalendar, RichTextEditor, SegmentedControl, Select, Sidebar, Splitter, StarRating, Stepper, Table, TableOfContents, Tabs, TagsInput, Timeline, Toast, ToggleGroup, Toolbar, Tooltip, Tour, Transfer, Tree and Typography.
|
|
35
|
+
|
|
34
36
|
The test: every compound component in your markup uses `.Root`, and its parts are wrapped inside it. See `rules/compound-components.md` for the full list and parts reference.
|
|
35
37
|
|
|
36
38
|
## 3. Let the Theme Do Its Job
|
|
@@ -153,6 +155,13 @@ Always verify with `info`, but these are the most common mistakes:
|
|
|
153
155
|
|
|
154
156
|
Use these to look up APIs, discover components, plan setup, and validate code.
|
|
155
157
|
|
|
158
|
+
### Recommended workflow
|
|
159
|
+
|
|
160
|
+
1. `compose` or `info` before writing components so you confirm kind, required parts, bindables, and canonical usage.
|
|
161
|
+
2. Build the route or component with raw CSS grid, `Container` for constrained width, and `@container` for responsive layout.
|
|
162
|
+
3. `review` or `doctor` after implementation to catch composition drift, layout violations, and accessibility regressions.
|
|
163
|
+
4. Never guess component shape from memory. DryUI is intentionally strict, and the lookup cost is lower than rework.
|
|
164
|
+
|
|
156
165
|
### MCP tools (preferred)
|
|
157
166
|
|
|
158
167
|
| Workflow | Tools |
|
|
@@ -402,13 +402,62 @@ Call `compose` with any recipe name to get a full working snippet.
|
|
|
402
402
|
| `data-table-with-actions` | Table with header actions | Table, Badge, Avatar, Button |
|
|
403
403
|
| `checkout-flow` | Multi-step checkout | Stepper, Card, Field, RadioGroup |
|
|
404
404
|
| `hotel-listing-card` | Product/listing card | Card, Image, Badge, Button, Text |
|
|
405
|
-
| `stat-card-grid` | KPI dashboard cards |
|
|
405
|
+
| `stat-card-grid` | KPI dashboard cards | StatCard, Chart, Sparkline, Container |
|
|
406
406
|
| `settings-page` | Settings with tabs | Tabs, Card, Field, Input, Select |
|
|
407
407
|
| `form-with-validation` | Form with error handling | Card, Field, Label, Input, Field.Error |
|
|
408
|
-
| `sidebar-layout` | Page with sidebar nav |
|
|
409
|
-
| `dashboard-page` | Full dashboard layout |
|
|
408
|
+
| `sidebar-layout` | Page with sidebar nav | Sidebar, PageHeader, Container |
|
|
409
|
+
| `dashboard-page` | Full dashboard layout | Sidebar, StatCard, Chart, Table |
|
|
410
410
|
| `user-profile-card` | User info card | Card, Avatar, Text, Badge, Button |
|
|
411
411
|
| `notification-list` | Notification feed | Card, Avatar, Text, Badge |
|
|
412
412
|
| `command-bar` | Command palette trigger | CommandPalette, Hotkey |
|
|
413
413
|
| `file-upload-form` | File upload with progress | Card, FileUpload, Progress, Button |
|
|
414
414
|
| `pricing-table` | Pricing comparison | Card, Text, Button, Badge |
|
|
415
|
+
|
|
416
|
+
## State-heavy form flows
|
|
417
|
+
|
|
418
|
+
DryUI is a presentation and accessibility system, not a workflow engine. For dependent-field planners, approvals, and booking-style state machines:
|
|
419
|
+
|
|
420
|
+
- Normalize route/session state in script before rendering DryUI inputs.
|
|
421
|
+
- Reset dependent `Select.Root` values when their parent choice changes; do not rely on stale child state surviving domain changes.
|
|
422
|
+
- Use raw CSS grid to lay out planner sections, and keep orchestration logic in route-level stores or derived state.
|
|
423
|
+
- Run `compose` or `info` before introducing a new field shape, then run `review` or `doctor` after the flow is wired.
|
|
424
|
+
|
|
425
|
+
```svelte
|
|
426
|
+
<script lang="ts">
|
|
427
|
+
let country = $state('');
|
|
428
|
+
let airport = $state('');
|
|
429
|
+
|
|
430
|
+
const airportOptions = $derived(getAirports(country));
|
|
431
|
+
|
|
432
|
+
$effect(() => {
|
|
433
|
+
if (!airportOptions.some((option) => option.value === airport)) {
|
|
434
|
+
airport = '';
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
</script>
|
|
438
|
+
|
|
439
|
+
<div class="planner">
|
|
440
|
+
<Field.Root>
|
|
441
|
+
<Label>Country</Label>
|
|
442
|
+
<Select.Root bind:value={country}>
|
|
443
|
+
<Select.Trigger><Select.Value placeholder="Choose country" /></Select.Trigger>
|
|
444
|
+
<Select.Content>{/* items */}</Select.Content>
|
|
445
|
+
</Select.Root>
|
|
446
|
+
</Field.Root>
|
|
447
|
+
|
|
448
|
+
<Field.Root>
|
|
449
|
+
<Label>Airport</Label>
|
|
450
|
+
<Select.Root bind:value={airport} disabled={!country}>
|
|
451
|
+
<Select.Trigger><Select.Value placeholder="Choose airport" /></Select.Trigger>
|
|
452
|
+
<Select.Content>{/* filtered items */}</Select.Content>
|
|
453
|
+
</Select.Root>
|
|
454
|
+
</Field.Root>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<style>
|
|
458
|
+
.planner {
|
|
459
|
+
display: grid;
|
|
460
|
+
gap: var(--dry-space-4);
|
|
461
|
+
}
|
|
462
|
+
</style>
|
|
463
|
+
```
|