@x33025/sveltely 0.1.0 → 0.1.2
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/components/Library/Button/Button.demo.svelte +5 -3
- package/dist/components/Library/Button/Button.demo.svelte.d.ts +1 -0
- package/dist/components/Library/Calendar/Calendar.demo.svelte +2 -14
- package/dist/components/Library/Calendar/Calendar.svelte +54 -50
- package/dist/components/Library/Divider/Divider.svelte +10 -0
- package/dist/components/Library/Divider/Divider.svelte.d.ts +26 -0
- package/dist/components/Library/Divider/index.d.ts +1 -0
- package/dist/components/Library/Divider/index.js +1 -0
- package/dist/components/Library/Dropdown/Dropdown.demo.svelte +37 -2
- package/dist/components/Library/Dropdown/Dropdown.svelte +55 -34
- package/dist/components/Library/Dropdown/Dropdown.svelte.d.ts +1 -1
- package/dist/components/Library/Dropdown/index.d.ts +1 -1
- package/dist/components/Library/Dropdown/types.d.ts +4 -1
- package/dist/components/Library/Floating/Floating.svelte +35 -1
- package/dist/components/Library/ForEach/ForEach.svelte +14 -0
- package/dist/components/Library/ForEach/ForEach.svelte.d.ts +28 -0
- package/dist/components/Library/ForEach/index.d.ts +1 -0
- package/dist/components/Library/ForEach/index.js +1 -0
- package/dist/components/Library/Grid/Grid.svelte +74 -0
- package/dist/components/Library/Grid/Grid.svelte.d.ts +13 -0
- package/dist/components/Library/Grid/index.d.ts +1 -0
- package/dist/components/Library/Grid/index.js +1 -0
- package/dist/components/Library/GridItem/GridItem.svelte +65 -0
- package/dist/components/Library/GridItem/GridItem.svelte.d.ts +14 -0
- package/dist/components/Library/GridItem/index.d.ts +1 -0
- package/dist/components/Library/GridItem/index.js +1 -0
- package/dist/components/Library/HStack/HStack.svelte +45 -0
- package/dist/components/Library/HStack/HStack.svelte.d.ts +9 -0
- package/dist/components/Library/HStack/index.d.ts +1 -0
- package/dist/components/Library/HStack/index.js +1 -0
- package/dist/components/Library/Image/Image.demo.svelte +18 -0
- package/dist/components/Library/Image/Image.demo.svelte.d.ts +23 -0
- package/dist/components/Library/Image/Image.svelte +57 -0
- package/dist/components/Library/Image/Image.svelte.d.ts +17 -0
- package/dist/components/Library/Image/ImagePlaceholder.svelte +202 -0
- package/dist/components/Library/Image/ImagePlaceholder.svelte.d.ts +7 -0
- package/dist/components/Library/Image/index.d.ts +1 -0
- package/dist/components/Library/Image/index.js +1 -0
- package/dist/components/Library/ImageMask/BrushPreview.svelte +119 -0
- package/dist/components/Library/ImageMask/BrushPreview.svelte.d.ts +11 -0
- package/dist/components/Library/ImageMask/ImageMask.demo.svelte +117 -0
- package/dist/components/Library/ImageMask/ImageMask.demo.svelte.d.ts +10 -0
- package/dist/components/Library/ImageMask/ImageMask.svelte +46 -0
- package/dist/components/Library/ImageMask/ImageMask.svelte.d.ts +20 -0
- package/dist/components/Library/ImageMask/MaskLayer.svelte +341 -0
- package/dist/components/Library/ImageMask/MaskLayer.svelte.d.ts +12 -0
- package/dist/components/Library/ImageMask/contour.d.ts +11 -0
- package/dist/components/Library/ImageMask/contour.js +152 -0
- package/dist/components/Library/ImageMask/index.d.ts +2 -0
- package/dist/components/Library/ImageMask/index.js +1 -0
- package/dist/components/Library/ImageMask/marchingAnts.d.ts +8 -0
- package/dist/components/Library/ImageMask/marchingAnts.js +29 -0
- package/dist/components/Library/ImageMask/maskSurface.d.ts +5 -0
- package/dist/components/Library/ImageMask/maskSurface.js +94 -0
- package/dist/components/Library/ImageMask/types.d.ts +23 -0
- package/dist/components/Library/ImageMask/types.js +1 -0
- package/dist/components/Library/Label/Label.demo.svelte +28 -0
- package/dist/components/Library/Label/Label.demo.svelte.d.ts +9 -0
- package/dist/components/Library/Label/Label.svelte +177 -0
- package/dist/components/Library/Label/Label.svelte.d.ts +18 -0
- package/dist/components/Library/Label/index.d.ts +1 -0
- package/dist/components/Library/Label/index.js +1 -0
- package/dist/components/Library/NumberField/NumberField.demo.svelte +21 -0
- package/dist/components/Library/NumberField/NumberField.demo.svelte.d.ts +8 -0
- package/dist/components/Library/NumberField/NumberField.svelte +194 -0
- package/dist/components/Library/NumberField/NumberField.svelte.d.ts +21 -0
- package/dist/components/Library/NumberField/index.d.ts +1 -0
- package/dist/components/Library/NumberField/index.js +1 -0
- package/dist/components/Library/ScrollView/ScrollView.svelte +25 -9
- package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +4 -4
- package/dist/components/Library/Spacer/Spacer.svelte +7 -0
- package/dist/components/Library/Spacer/Spacer.svelte.d.ts +26 -0
- package/dist/components/Library/Spacer/index.d.ts +1 -0
- package/dist/components/Library/Spacer/index.js +1 -0
- package/dist/components/Library/TextField/TextField.demo.svelte +14 -0
- package/dist/components/Library/TextField/TextField.demo.svelte.d.ts +8 -0
- package/dist/components/Library/TextField/TextField.svelte +149 -0
- package/dist/components/Library/TextField/TextField.svelte.d.ts +19 -0
- package/dist/components/Library/TextField/index.d.ts +1 -0
- package/dist/components/Library/TextField/index.js +1 -0
- package/dist/components/Library/VStack/VStack.svelte +45 -0
- package/dist/components/Library/VStack/VStack.svelte.d.ts +9 -0
- package/dist/components/Library/VStack/index.d.ts +1 -0
- package/dist/components/Library/VStack/index.js +1 -0
- package/dist/components/Local/ComponentGrid.svelte +15 -31
- package/dist/components/Local/HeroCard.svelte +26 -36
- package/dist/components/Local/HeroCard.svelte.d.ts +0 -2
- package/dist/index.d.ts +23 -0
- package/dist/index.js +17 -0
- package/dist/style/index.css +28 -17
- package/dist/style/label.d.ts +6 -0
- package/dist/style/label.js +4 -0
- package/dist/style/layout.d.ts +57 -0
- package/dist/style/layout.js +128 -0
- package/dist/style/media.d.ts +12 -0
- package/dist/style/media.js +8 -0
- package/dist/style/scroll.d.ts +7 -0
- package/dist/style/scroll.js +5 -0
- package/dist/style/text-editor.d.ts +34 -0
- package/dist/style/text-editor.js +29 -0
- package/dist/style.css +58 -35
- package/package.json +1 -1
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
export const demo = {
|
|
3
3
|
name: 'Button',
|
|
4
|
-
description: 'Token-aware button primitive with optional icon support.'
|
|
4
|
+
description: 'Token-aware button primitive with optional icon support.',
|
|
5
|
+
columnSpan: 2
|
|
5
6
|
};
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<script lang="ts">
|
|
9
10
|
import { SaveIcon } from '@lucide/svelte';
|
|
10
11
|
import Button from './Button.svelte';
|
|
12
|
+
import HStack from '../HStack';
|
|
11
13
|
</script>
|
|
12
14
|
|
|
13
|
-
<
|
|
15
|
+
<HStack align="center" gap={0.75}>
|
|
14
16
|
<Button label="Default" />
|
|
15
17
|
<Button label="Solid" variant="solid" />
|
|
16
18
|
<Button icon={SaveIcon} label="With icon" />
|
|
17
|
-
</
|
|
19
|
+
</HStack>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const demo: {
|
|
2
2
|
name: string;
|
|
3
3
|
description: string;
|
|
4
|
+
columnSpan: number;
|
|
4
5
|
};
|
|
5
6
|
import Button from './Button.svelte';
|
|
6
7
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
name: 'Calendar',
|
|
4
4
|
description: 'Compact month calendar with token-driven spacing and radius.',
|
|
5
5
|
columnSpan: 2,
|
|
6
|
-
rowSpan:
|
|
6
|
+
rowSpan: 3
|
|
7
7
|
};
|
|
8
8
|
</script>
|
|
9
9
|
|
|
@@ -15,16 +15,4 @@
|
|
|
15
15
|
let weekStart = $state<'monday' | 'sunday'>('monday');
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
|
-
<
|
|
19
|
-
<div class="hstack gap-2">
|
|
20
|
-
<button type="button" onclick={() => (weekStart = 'monday')}>Monday start</button>
|
|
21
|
-
<button type="button" onclick={() => (weekStart = 'sunday')}>Sunday start</button>
|
|
22
|
-
</div>
|
|
23
|
-
<Calendar bind:value bind:month {weekStart} />
|
|
24
|
-
<p class="text-xs text-zinc-500">
|
|
25
|
-
Selected: {value ? value.toLocaleDateString() : 'none'} | Month: {month.toLocaleDateString(undefined, {
|
|
26
|
-
month: 'long',
|
|
27
|
-
year: 'numeric'
|
|
28
|
-
})} | Week starts: {weekStart}
|
|
29
|
-
</p>
|
|
30
|
-
</div>
|
|
18
|
+
<Calendar bind:value bind:month {weekStart} />
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
const startOfDay = (value: Date) =>
|
|
6
6
|
new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
7
7
|
|
|
8
|
-
const startOfMonth = (value: Date) =>
|
|
9
|
-
new Date(value.getFullYear(), value.getMonth(), 1);
|
|
8
|
+
const startOfMonth = (value: Date) => new Date(value.getFullYear(), value.getMonth(), 1);
|
|
10
9
|
|
|
11
10
|
type CalendarCell = {
|
|
12
11
|
key: string;
|
|
@@ -23,7 +22,8 @@
|
|
|
23
22
|
month?: Date;
|
|
24
23
|
weekdayLabels?: string[];
|
|
25
24
|
weekStart?: 'monday' | 'sunday';
|
|
26
|
-
} & StyleProps &
|
|
25
|
+
} & StyleProps &
|
|
26
|
+
Record<string, unknown>;
|
|
27
27
|
|
|
28
28
|
const defaultWeekdayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
29
29
|
const today = new Date();
|
|
@@ -42,14 +42,10 @@
|
|
|
42
42
|
const rootStyle = $derived.by(() => surfaceStyle(styleProps, 'calendar'));
|
|
43
43
|
|
|
44
44
|
const orderedWeekdayLabels = $derived(
|
|
45
|
-
weekStart === 'sunday'
|
|
46
|
-
? [weekdayLabels[6], ...weekdayLabels.slice(0, 6)]
|
|
47
|
-
: weekdayLabels
|
|
45
|
+
weekStart === 'sunday' ? [weekdayLabels[6], ...weekdayLabels.slice(0, 6)] : weekdayLabels
|
|
48
46
|
);
|
|
49
47
|
|
|
50
|
-
const weekendColumnIndexes = $derived(
|
|
51
|
-
weekStart === 'sunday' ? [0, 6] : [5, 6]
|
|
52
|
-
);
|
|
48
|
+
const weekendColumnIndexes = $derived(weekStart === 'sunday' ? [0, 6] : [5, 6]);
|
|
53
49
|
|
|
54
50
|
function shiftMonth(offset: number) {
|
|
55
51
|
month = new Date(month.getFullYear(), month.getMonth() + offset, 1);
|
|
@@ -59,6 +55,17 @@
|
|
|
59
55
|
value = startOfDay(nextValue);
|
|
60
56
|
}
|
|
61
57
|
|
|
58
|
+
function activateCell(cell: CalendarCell) {
|
|
59
|
+
if (!cell.value) return;
|
|
60
|
+
|
|
61
|
+
if (cell.isCurrentMonth) {
|
|
62
|
+
selectDate(cell.value);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
month = startOfMonth(cell.value);
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
const displayYear = $derived(month.getFullYear());
|
|
63
70
|
const displayMonth = $derived(month.getMonth());
|
|
64
71
|
const todayYear = today.getFullYear();
|
|
@@ -74,37 +81,28 @@
|
|
|
74
81
|
|
|
75
82
|
const cells = $derived.by(() => {
|
|
76
83
|
const firstDay = new Date(displayYear, displayMonth, 1);
|
|
77
|
-
const daysInMonth = new Date(displayYear, displayMonth + 1, 0).getDate();
|
|
78
84
|
const leadingEmptyDays =
|
|
79
85
|
weekStart === 'sunday' ? firstDay.getDay() : (firstDay.getDay() + 6) % 7;
|
|
80
86
|
|
|
81
|
-
return Array.from({ length:
|
|
87
|
+
return Array.from({ length: 42 }, (_, index): CalendarCell => {
|
|
82
88
|
const dayNumber = index - leadingEmptyDays + 1;
|
|
83
|
-
if (dayNumber < 1) {
|
|
84
|
-
return {
|
|
85
|
-
key: `empty-${index}`,
|
|
86
|
-
label: '',
|
|
87
|
-
isCurrentMonth: false,
|
|
88
|
-
isToday: false,
|
|
89
|
-
isWeekend: false,
|
|
90
|
-
isSelected: false
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
89
|
const cellDate = new Date(displayYear, displayMonth, dayNumber);
|
|
95
90
|
const dayOfWeek = cellDate.getDay();
|
|
91
|
+
const isCurrentMonth = cellDate.getMonth() === displayMonth;
|
|
96
92
|
|
|
97
93
|
return {
|
|
98
|
-
key: `day-${
|
|
99
|
-
label: String(
|
|
100
|
-
isCurrentMonth
|
|
94
|
+
key: `day-${cellDate.getFullYear()}-${cellDate.getMonth()}-${cellDate.getDate()}`,
|
|
95
|
+
label: String(cellDate.getDate()),
|
|
96
|
+
isCurrentMonth,
|
|
101
97
|
value: cellDate,
|
|
102
98
|
isToday:
|
|
99
|
+
isCurrentMonth &&
|
|
103
100
|
displayYear === todayYear &&
|
|
104
101
|
displayMonth === todayMonth &&
|
|
105
|
-
|
|
102
|
+
cellDate.getDate() === todayDate,
|
|
106
103
|
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
|
|
107
104
|
isSelected:
|
|
105
|
+
isCurrentMonth &&
|
|
108
106
|
value !== null &&
|
|
109
107
|
cellDate.getFullYear() === value.getFullYear() &&
|
|
110
108
|
cellDate.getMonth() === value.getMonth() &&
|
|
@@ -116,16 +114,26 @@
|
|
|
116
114
|
|
|
117
115
|
<div class="calendar vstack" style={rootStyle} {...props}>
|
|
118
116
|
<div class="calendar-grid">
|
|
119
|
-
<button
|
|
117
|
+
<button
|
|
118
|
+
class="calendar-nav-button"
|
|
119
|
+
type="button"
|
|
120
|
+
aria-label="Previous month"
|
|
121
|
+
onclick={() => shiftMonth(-1)}
|
|
122
|
+
>
|
|
120
123
|
<ChevronLeftIcon class="calendar-nav-icon" strokeWidth={2} />
|
|
121
124
|
</button>
|
|
122
125
|
<div class="calendar-title-cell">
|
|
123
126
|
<h2 class="calendar-title">{monthLabel}</h2>
|
|
124
127
|
</div>
|
|
125
|
-
<button
|
|
128
|
+
<button
|
|
129
|
+
class="calendar-nav-button"
|
|
130
|
+
type="button"
|
|
131
|
+
aria-label="Next month"
|
|
132
|
+
onclick={() => shiftMonth(1)}
|
|
133
|
+
>
|
|
126
134
|
<ChevronRightIcon class="calendar-nav-icon" strokeWidth={2} />
|
|
127
135
|
</button>
|
|
128
|
-
{#each orderedWeekdayLabels as weekday, index}
|
|
136
|
+
{#each orderedWeekdayLabels as weekday, index (weekday)}
|
|
129
137
|
<div
|
|
130
138
|
class="calendar-weekday"
|
|
131
139
|
class:calendar-weekday-weekend={weekendColumnIndexes.includes(index)}
|
|
@@ -135,23 +143,18 @@
|
|
|
135
143
|
{/each}
|
|
136
144
|
|
|
137
145
|
{#each cells as cell (cell.key)}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
{:else}
|
|
151
|
-
<div class="calendar-day calendar-day-empty">
|
|
152
|
-
{cell.label}
|
|
153
|
-
</div>
|
|
154
|
-
{/if}
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
class="calendar-day"
|
|
149
|
+
class:calendar-day-outside={!cell.isCurrentMonth}
|
|
150
|
+
class:calendar-day-today={cell.isToday}
|
|
151
|
+
class:calendar-day-selected={cell.isSelected}
|
|
152
|
+
class:calendar-day-weekend={cell.isWeekend}
|
|
153
|
+
aria-pressed={cell.isSelected}
|
|
154
|
+
onclick={() => activateCell(cell)}
|
|
155
|
+
>
|
|
156
|
+
{cell.label}
|
|
157
|
+
</button>
|
|
155
158
|
{/each}
|
|
156
159
|
</div>
|
|
157
160
|
</div>
|
|
@@ -165,7 +168,9 @@
|
|
|
165
168
|
--calendar-weekend-header-color: var(--color-zinc-400);
|
|
166
169
|
--calendar-weekend-selected-color: white;
|
|
167
170
|
--calendar-cell-width: calc(var(--calendar-cell-core-size) + (var(--sveltely-padding-x) * 2.2));
|
|
168
|
-
--calendar-cell-height: calc(
|
|
171
|
+
--calendar-cell-height: calc(
|
|
172
|
+
var(--calendar-cell-core-size) + (var(--sveltely-padding-y) * 2.2)
|
|
173
|
+
);
|
|
169
174
|
--calendar-title-size: calc(var(--calendar-font-size) * 1.03);
|
|
170
175
|
--calendar-weekday-size: calc(var(--calendar-font-size) * 0.75);
|
|
171
176
|
font-size: var(--calendar-font-size);
|
|
@@ -283,11 +288,10 @@
|
|
|
283
288
|
background: var(--sveltely-hover-color);
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
.calendar-day-
|
|
291
|
+
.calendar-day-outside {
|
|
287
292
|
border-color: transparent;
|
|
288
293
|
background: transparent;
|
|
289
|
-
color:
|
|
290
|
-
cursor: default;
|
|
294
|
+
color: var(--color-zinc-400);
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
.calendar-day-today {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default Divider;
|
|
2
|
+
type Divider = SvelteComponent<{
|
|
3
|
+
[x: string]: never;
|
|
4
|
+
}, {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
}, {}> & {
|
|
7
|
+
$$bindings?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const Divider: $$__sveltets_2_IsomorphicComponent<{
|
|
10
|
+
[x: string]: never;
|
|
11
|
+
}, {
|
|
12
|
+
[evt: string]: CustomEvent<any>;
|
|
13
|
+
}, {}, {}, string>;
|
|
14
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
15
|
+
new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
|
|
16
|
+
$$bindings?: Bindings;
|
|
17
|
+
} & Exports;
|
|
18
|
+
(internal: unknown, props: {
|
|
19
|
+
$$events?: Events;
|
|
20
|
+
$$slots?: Slots;
|
|
21
|
+
}): Exports & {
|
|
22
|
+
$set?: any;
|
|
23
|
+
$on?: any;
|
|
24
|
+
};
|
|
25
|
+
z_$$bindings?: Bindings;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Divider.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Divider.svelte';
|
|
@@ -7,19 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
import Dropdown from './Dropdown.svelte';
|
|
10
|
+
import type { DropdownEntry } from './types';
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const allItems: DropdownEntry<string>[] = [
|
|
12
13
|
{
|
|
13
14
|
type: 'group' as const,
|
|
14
15
|
label: 'Publishing',
|
|
15
16
|
items: [
|
|
16
17
|
{ label: 'Draft', value: 'draft' },
|
|
17
18
|
{ label: 'Scheduled', value: 'scheduled' },
|
|
19
|
+
{ type: 'divider' },
|
|
18
20
|
{
|
|
19
21
|
type: 'submenu' as const,
|
|
20
22
|
label: 'Advanced',
|
|
21
23
|
items: [
|
|
22
24
|
{ label: 'Review queue', value: 'review' },
|
|
25
|
+
{ type: 'divider' },
|
|
23
26
|
{
|
|
24
27
|
type: 'action' as const,
|
|
25
28
|
label: 'Open workflow',
|
|
@@ -48,7 +51,39 @@
|
|
|
48
51
|
}
|
|
49
52
|
];
|
|
50
53
|
|
|
54
|
+
const filterEntries = (entries: DropdownEntry<string>[], query: string): DropdownEntry<string>[] => {
|
|
55
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
56
|
+
if (!normalizedQuery) return allItems;
|
|
57
|
+
|
|
58
|
+
return entries.flatMap<DropdownEntry<string>>((entry) => {
|
|
59
|
+
if (entry.type === 'divider') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (entry.type === 'group') {
|
|
64
|
+
const nextItems = filterEntries(entry.items, normalizedQuery);
|
|
65
|
+
return nextItems.length > 0 ? [{ ...entry, items: nextItems }] : [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entry.type === 'submenu') {
|
|
69
|
+
const nextItems = filterEntries(entry.items, normalizedQuery);
|
|
70
|
+
const matches = entry.label.toLowerCase().includes(normalizedQuery);
|
|
71
|
+
if (!matches && nextItems.length === 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return [{ ...entry, items: matches ? entry.items : nextItems }];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return entry.label.toLowerCase().includes(normalizedQuery) ? [entry] : [];
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let items = $state(allItems);
|
|
51
82
|
let value = $state<string | null>('draft');
|
|
83
|
+
|
|
84
|
+
const handleSearch = async (query: string) => {
|
|
85
|
+
items = filterEntries(allItems, query);
|
|
86
|
+
};
|
|
52
87
|
</script>
|
|
53
88
|
|
|
54
|
-
<Dropdown {items} bind:value placeholder="Choose status"
|
|
89
|
+
<Dropdown {items} bind:value placeholder="Choose status" onSearch={handleSearch} />
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends string | number = string">
|
|
2
2
|
import { CheckIcon, ChevronDownIcon, ChevronRightIcon } from '@lucide/svelte';
|
|
3
|
+
import Divider from '../Divider';
|
|
3
4
|
import Floating from '../Floating/Floating.svelte';
|
|
4
5
|
import SearchInput from '../SearchInput';
|
|
5
6
|
import { surfaceStyle, type StyleProps } from '../../../style/surface';
|
|
6
7
|
import type { Anchor } from '../../../utils/positioning';
|
|
7
8
|
import type {
|
|
8
9
|
DropdownAction,
|
|
10
|
+
DropdownDivider,
|
|
9
11
|
DropdownEntry,
|
|
10
12
|
DropdownGroup,
|
|
11
13
|
DropdownItem,
|
|
@@ -21,9 +23,9 @@
|
|
|
21
23
|
disabled?: boolean;
|
|
22
24
|
closeOnSelect?: boolean;
|
|
23
25
|
showCheck?: boolean;
|
|
24
|
-
searchable?: boolean;
|
|
25
26
|
searchPlaceholder?: string;
|
|
26
27
|
placement?: Anchor;
|
|
28
|
+
onSearch?: (query: string) => void | Promise<void>;
|
|
27
29
|
onSelect?: (item: DropdownItem<T>) => void;
|
|
28
30
|
} & StyleProps;
|
|
29
31
|
|
|
@@ -31,6 +33,8 @@
|
|
|
31
33
|
'type' in entry && entry.type === 'group';
|
|
32
34
|
const isAction = (entry: DropdownItem<T> | DropdownAction): entry is DropdownAction =>
|
|
33
35
|
'type' in entry && entry.type === 'action';
|
|
36
|
+
const isDivider = (entry: DropdownEntry<T>): entry is DropdownDivider =>
|
|
37
|
+
'type' in entry && entry.type === 'divider';
|
|
34
38
|
const isSubmenu = (entry: DropdownEntry<T>): entry is DropdownSubmenu<T> =>
|
|
35
39
|
'type' in entry && entry.type === 'submenu';
|
|
36
40
|
|
|
@@ -43,9 +47,9 @@
|
|
|
43
47
|
disabled = false,
|
|
44
48
|
closeOnSelect = true,
|
|
45
49
|
showCheck = true,
|
|
46
|
-
searchable = false,
|
|
47
50
|
searchPlaceholder = 'Search',
|
|
48
51
|
placement = 'bottom',
|
|
52
|
+
onSearch,
|
|
49
53
|
onSelect,
|
|
50
54
|
...styleProps
|
|
51
55
|
}: Props = $props();
|
|
@@ -53,12 +57,19 @@
|
|
|
53
57
|
const dropdownStyle = $derived.by(() => surfaceStyle(styleProps, 'dropdown'));
|
|
54
58
|
|
|
55
59
|
let query = $state('');
|
|
60
|
+
let lastSearchedQuery = '';
|
|
61
|
+
|
|
62
|
+
const searchEnabled = $derived(!!onSearch);
|
|
56
63
|
|
|
57
64
|
const flattenItems = (
|
|
58
65
|
entries: DropdownEntry<T>[],
|
|
59
66
|
inheritedDisabled = false
|
|
60
67
|
): DropdownItem<T>[] =>
|
|
61
68
|
entries.flatMap((entry) => {
|
|
69
|
+
if (isDivider(entry)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
const nextDisabled = inheritedDisabled || !!entry.disabled;
|
|
63
74
|
if (isGroup(entry) || isSubmenu(entry)) {
|
|
64
75
|
return flattenItems(entry.items, nextDisabled);
|
|
@@ -69,25 +80,6 @@
|
|
|
69
80
|
return [{ ...entry, disabled: nextDisabled || entry.disabled }];
|
|
70
81
|
});
|
|
71
82
|
|
|
72
|
-
const filterEntries = (entries: DropdownEntry<T>[]): DropdownEntry<T>[] =>
|
|
73
|
-
entries.flatMap((entry) => {
|
|
74
|
-
if (isGroup(entry)) {
|
|
75
|
-
const nextItems = filterEntries(entry.items);
|
|
76
|
-
return nextItems.length > 0 ? [{ ...entry, items: nextItems }] : [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (isSubmenu(entry)) {
|
|
80
|
-
const nextItems = filterEntries(entry.items);
|
|
81
|
-
const matches = entry.label.toLowerCase().includes(normalizedQuery);
|
|
82
|
-
if (!matches && nextItems.length === 0) {
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
return [{ ...entry, items: matches ? entry.items : nextItems }];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return entry.label.toLowerCase().includes(normalizedQuery) ? [entry] : [];
|
|
89
|
-
});
|
|
90
|
-
|
|
91
83
|
const findFirstRenderableLabel = (entries: DropdownEntry<T>[]): string | null => {
|
|
92
84
|
for (const entry of entries) {
|
|
93
85
|
if (isGroup(entry)) {
|
|
@@ -95,13 +87,14 @@
|
|
|
95
87
|
if (nested) return nested;
|
|
96
88
|
continue;
|
|
97
89
|
}
|
|
90
|
+
if (isDivider(entry)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
98
93
|
return entry.label;
|
|
99
94
|
}
|
|
100
95
|
return null;
|
|
101
96
|
};
|
|
102
97
|
|
|
103
|
-
const normalizedQuery = $derived(query.trim().toLowerCase());
|
|
104
|
-
|
|
105
98
|
const flatItems = $derived.by(() => flattenItems(items));
|
|
106
99
|
|
|
107
100
|
const selectedItem = $derived.by(
|
|
@@ -110,14 +103,12 @@
|
|
|
110
103
|
|
|
111
104
|
const triggerText = $derived(selectedItem?.label ?? placeholder);
|
|
112
105
|
|
|
113
|
-
const filteredItems = $derived
|
|
114
|
-
normalizedQuery ? filterEntries(items) : items
|
|
115
|
-
);
|
|
106
|
+
const filteredItems = $derived(items);
|
|
116
107
|
|
|
117
108
|
const firstRenderableLabel = $derived.by(() => findFirstRenderableLabel(filteredItems));
|
|
118
109
|
|
|
119
110
|
const itemRadiusSourceEnabled = $derived.by(() => {
|
|
120
|
-
if (
|
|
111
|
+
if (searchEnabled) return false;
|
|
121
112
|
const firstEntry = filteredItems[0];
|
|
122
113
|
if (!firstEntry) return false;
|
|
123
114
|
if (isGroup(firstEntry)) {
|
|
@@ -143,6 +134,13 @@
|
|
|
143
134
|
disabled || inheritedDisabled || !!entry.disabled;
|
|
144
135
|
|
|
145
136
|
const isRadiusSource = (label: string) => itemRadiusSourceEnabled && label === firstRenderableLabel;
|
|
137
|
+
|
|
138
|
+
$effect(() => {
|
|
139
|
+
if (!onSearch) return;
|
|
140
|
+
if (query === lastSearchedQuery) return;
|
|
141
|
+
lastSearchedQuery = query;
|
|
142
|
+
void onSearch(query);
|
|
143
|
+
});
|
|
146
144
|
</script>
|
|
147
145
|
|
|
148
146
|
<Floating
|
|
@@ -167,26 +165,27 @@
|
|
|
167
165
|
aria-haspopup="dialog"
|
|
168
166
|
onclick={floating.toggle}
|
|
169
167
|
>
|
|
170
|
-
<span>{triggerText}</span>
|
|
168
|
+
<span class="dropdown-trigger-label">{triggerText}</span>
|
|
171
169
|
<ChevronDownIcon class="size-4 text-zinc-500" />
|
|
172
170
|
</button>
|
|
173
171
|
{/snippet}
|
|
174
172
|
|
|
175
173
|
<div
|
|
176
174
|
class="dropdown-content vstack"
|
|
177
|
-
style={`${dropdownStyle} --dropdown-item-radius: ${
|
|
175
|
+
style={`${dropdownStyle} --dropdown-item-radius: ${searchEnabled ? 'var(--sveltely-border-radius)' : 'var(--sveltely-border-radius-nested)'};`}
|
|
178
176
|
>
|
|
179
|
-
{#if
|
|
177
|
+
{#if searchEnabled}
|
|
180
178
|
<SearchInput
|
|
181
179
|
bind:value={query}
|
|
182
180
|
placeholder={searchPlaceholder}
|
|
183
181
|
radiusSource={true}
|
|
184
|
-
class="w-64"
|
|
185
182
|
/>
|
|
186
183
|
{/if}
|
|
187
184
|
{#snippet renderEntries(entries: DropdownEntry<T>[], inheritedDisabled = false)}
|
|
188
|
-
{#each entries as entry, index (`${index}-${entry.type ?? 'option'}-${entry.label}`)}
|
|
189
|
-
{#if
|
|
185
|
+
{#each entries as entry, index (`${index}-${entry.type ?? 'option'}-${isDivider(entry) ? 'divider' : entry.label}`)}
|
|
186
|
+
{#if isDivider(entry)}
|
|
187
|
+
<Divider />
|
|
188
|
+
{:else if isGroup(entry)}
|
|
190
189
|
<div class="dropdown-group vstack">
|
|
191
190
|
{#if entry.label}
|
|
192
191
|
<div class="dropdown-group-label">{entry.label}</div>
|
|
@@ -287,6 +286,7 @@
|
|
|
287
286
|
.dropdown-trigger {
|
|
288
287
|
display: inline-flex;
|
|
289
288
|
min-width: 8rem;
|
|
289
|
+
max-width: 100%;
|
|
290
290
|
align-items: center;
|
|
291
291
|
border: 1px solid var(--sveltely-border-color);
|
|
292
292
|
border-radius: var(--sveltely-border-radius);
|
|
@@ -299,6 +299,18 @@
|
|
|
299
299
|
transition: color 150ms, border-color 150ms, background-color 150ms;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
.dropdown-trigger-label {
|
|
303
|
+
min-width: 0;
|
|
304
|
+
flex: 1 1 auto;
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
text-overflow: ellipsis;
|
|
307
|
+
white-space: nowrap;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.dropdown-trigger :global(svg) {
|
|
311
|
+
flex: 0 0 auto;
|
|
312
|
+
}
|
|
313
|
+
|
|
302
314
|
.dropdown-trigger:hover {
|
|
303
315
|
background: var(--sveltely-hover-color);
|
|
304
316
|
}
|
|
@@ -313,6 +325,8 @@
|
|
|
313
325
|
}
|
|
314
326
|
|
|
315
327
|
.dropdown-content {
|
|
328
|
+
--dropdown-item-padding-x: calc(var(--sveltely-padding-x) * 0.67);
|
|
329
|
+
--dropdown-item-padding-y: calc(var(--sveltely-padding-y) * 0.33);
|
|
316
330
|
gap: var(--sveltely-inset);
|
|
317
331
|
}
|
|
318
332
|
|
|
@@ -320,11 +334,18 @@
|
|
|
320
334
|
gap: var(--sveltely-inset);
|
|
321
335
|
}
|
|
322
336
|
|
|
337
|
+
.dropdown-content :global(.divider) {
|
|
338
|
+
--divider-width: auto;
|
|
339
|
+
margin-inline: var(--dropdown-item-padding-x);
|
|
340
|
+
margin-block: calc(var(--sveltely-inset) * 0.5);
|
|
341
|
+
background: var(--sveltely-border-color);
|
|
342
|
+
}
|
|
343
|
+
|
|
323
344
|
.dropdown-item {
|
|
324
345
|
width: 100%;
|
|
325
346
|
gap: calc(var(--sveltely-inset) * 2);
|
|
326
347
|
border-radius: var(--dropdown-item-radius, var(--sveltely-border-radius-nested));
|
|
327
|
-
padding:
|
|
348
|
+
padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
|
|
328
349
|
}
|
|
329
350
|
|
|
330
351
|
.dropdown-item :global(*) {
|
|
@@ -11,9 +11,9 @@ declare function $$render<T extends string | number = string>(): {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
closeOnSelect?: boolean;
|
|
13
13
|
showCheck?: boolean;
|
|
14
|
-
searchable?: boolean;
|
|
15
14
|
searchPlaceholder?: string;
|
|
16
15
|
placement?: Anchor;
|
|
16
|
+
onSearch?: (query: string) => void | Promise<void>;
|
|
17
17
|
onSelect?: (item: DropdownItem<T>) => void;
|
|
18
18
|
} & StyleProps;
|
|
19
19
|
exports: {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default } from './Dropdown.svelte';
|
|
2
|
-
export type { DropdownAction, DropdownEntry, DropdownGroup, DropdownItem, DropdownSubmenu } from './types';
|
|
2
|
+
export type { DropdownAction, DropdownDivider, DropdownEntry, DropdownGroup, DropdownItem, DropdownSubmenu } from './types';
|
|
@@ -11,6 +11,9 @@ export type DropdownAction = {
|
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
onSelect: () => void;
|
|
13
13
|
};
|
|
14
|
+
export type DropdownDivider = {
|
|
15
|
+
type: 'divider';
|
|
16
|
+
};
|
|
14
17
|
export type DropdownSubmenu<TValue extends string | number = string> = {
|
|
15
18
|
type: 'submenu';
|
|
16
19
|
label: string;
|
|
@@ -24,4 +27,4 @@ export type DropdownGroup<TValue extends string | number = string> = {
|
|
|
24
27
|
disabled?: boolean;
|
|
25
28
|
items: DropdownEntry<TValue>[];
|
|
26
29
|
};
|
|
27
|
-
export type DropdownEntry<TValue extends string | number = string> = DropdownItem<TValue> | DropdownAction | DropdownGroup<TValue> | DropdownSubmenu<TValue>;
|
|
30
|
+
export type DropdownEntry<TValue extends string | number = string> = DropdownItem<TValue> | DropdownAction | DropdownDivider | DropdownGroup<TValue> | DropdownSubmenu<TValue>;
|
|
@@ -177,6 +177,39 @@
|
|
|
177
177
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
178
178
|
};
|
|
179
179
|
|
|
180
|
+
const resolveCssLength = (element: HTMLElement, value: string) => {
|
|
181
|
+
const trimmedValue = value.trim();
|
|
182
|
+
if (!trimmedValue) return 0;
|
|
183
|
+
if (trimmedValue.endsWith('px')) return parsePx(trimmedValue);
|
|
184
|
+
|
|
185
|
+
const probe = document.createElement('div');
|
|
186
|
+
probe.style.position = 'absolute';
|
|
187
|
+
probe.style.visibility = 'hidden';
|
|
188
|
+
probe.style.pointerEvents = 'none';
|
|
189
|
+
probe.style.width = trimmedValue;
|
|
190
|
+
element.appendChild(probe);
|
|
191
|
+
const width = probe.getBoundingClientRect().width;
|
|
192
|
+
probe.remove();
|
|
193
|
+
return width;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getFloatingInset = () => {
|
|
197
|
+
if (!panelEl) return 0;
|
|
198
|
+
const styles = getComputedStyle(panelEl);
|
|
199
|
+
return resolveCssLength(panelEl, styles.getPropertyValue('--sveltely-floating-inset'));
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const offsetCoords = (
|
|
203
|
+
coords: { top: number; left: number },
|
|
204
|
+
currentAnchor: Anchor,
|
|
205
|
+
offset: number
|
|
206
|
+
) => {
|
|
207
|
+
if (currentAnchor === 'bottom') return { ...coords, top: coords.top + offset };
|
|
208
|
+
if (currentAnchor === 'top') return { ...coords, top: coords.top - offset };
|
|
209
|
+
if (currentAnchor === 'right') return { ...coords, left: coords.left + offset };
|
|
210
|
+
return { ...coords, left: coords.left - offset };
|
|
211
|
+
};
|
|
212
|
+
|
|
180
213
|
const updateRadiusFromSource = () => {
|
|
181
214
|
if (!matchPanelRadiusToSource || !panelEl || !contentEl) {
|
|
182
215
|
computedPanelRadius = null;
|
|
@@ -222,7 +255,7 @@
|
|
|
222
255
|
placement,
|
|
223
256
|
preferredAlign
|
|
224
257
|
);
|
|
225
|
-
panelCoords = { top: result.top, left: result.left };
|
|
258
|
+
panelCoords = offsetCoords({ top: result.top, left: result.left }, result.anchor, getFloatingInset());
|
|
226
259
|
panelTransform = result.transform;
|
|
227
260
|
resolvedAnchor = result.anchor;
|
|
228
261
|
};
|
|
@@ -317,6 +350,7 @@
|
|
|
317
350
|
|
|
318
351
|
const resolvedPanelStyle = $derived.by(() => {
|
|
319
352
|
const declarations = [
|
|
353
|
+
'--sveltely-floating-inset: var(--sveltely-inset);',
|
|
320
354
|
`top: ${panelCoords.top}px;`,
|
|
321
355
|
`left: ${panelCoords.left}px;`,
|
|
322
356
|
`transform: ${panelTransform};`
|