@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.
Files changed (102) hide show
  1. package/dist/components/Library/Button/Button.demo.svelte +5 -3
  2. package/dist/components/Library/Button/Button.demo.svelte.d.ts +1 -0
  3. package/dist/components/Library/Calendar/Calendar.demo.svelte +2 -14
  4. package/dist/components/Library/Calendar/Calendar.svelte +54 -50
  5. package/dist/components/Library/Divider/Divider.svelte +10 -0
  6. package/dist/components/Library/Divider/Divider.svelte.d.ts +26 -0
  7. package/dist/components/Library/Divider/index.d.ts +1 -0
  8. package/dist/components/Library/Divider/index.js +1 -0
  9. package/dist/components/Library/Dropdown/Dropdown.demo.svelte +37 -2
  10. package/dist/components/Library/Dropdown/Dropdown.svelte +55 -34
  11. package/dist/components/Library/Dropdown/Dropdown.svelte.d.ts +1 -1
  12. package/dist/components/Library/Dropdown/index.d.ts +1 -1
  13. package/dist/components/Library/Dropdown/types.d.ts +4 -1
  14. package/dist/components/Library/Floating/Floating.svelte +35 -1
  15. package/dist/components/Library/ForEach/ForEach.svelte +14 -0
  16. package/dist/components/Library/ForEach/ForEach.svelte.d.ts +28 -0
  17. package/dist/components/Library/ForEach/index.d.ts +1 -0
  18. package/dist/components/Library/ForEach/index.js +1 -0
  19. package/dist/components/Library/Grid/Grid.svelte +74 -0
  20. package/dist/components/Library/Grid/Grid.svelte.d.ts +13 -0
  21. package/dist/components/Library/Grid/index.d.ts +1 -0
  22. package/dist/components/Library/Grid/index.js +1 -0
  23. package/dist/components/Library/GridItem/GridItem.svelte +65 -0
  24. package/dist/components/Library/GridItem/GridItem.svelte.d.ts +14 -0
  25. package/dist/components/Library/GridItem/index.d.ts +1 -0
  26. package/dist/components/Library/GridItem/index.js +1 -0
  27. package/dist/components/Library/HStack/HStack.svelte +45 -0
  28. package/dist/components/Library/HStack/HStack.svelte.d.ts +9 -0
  29. package/dist/components/Library/HStack/index.d.ts +1 -0
  30. package/dist/components/Library/HStack/index.js +1 -0
  31. package/dist/components/Library/Image/Image.demo.svelte +18 -0
  32. package/dist/components/Library/Image/Image.demo.svelte.d.ts +23 -0
  33. package/dist/components/Library/Image/Image.svelte +57 -0
  34. package/dist/components/Library/Image/Image.svelte.d.ts +17 -0
  35. package/dist/components/Library/Image/ImagePlaceholder.svelte +202 -0
  36. package/dist/components/Library/Image/ImagePlaceholder.svelte.d.ts +7 -0
  37. package/dist/components/Library/Image/index.d.ts +1 -0
  38. package/dist/components/Library/Image/index.js +1 -0
  39. package/dist/components/Library/ImageMask/BrushPreview.svelte +119 -0
  40. package/dist/components/Library/ImageMask/BrushPreview.svelte.d.ts +11 -0
  41. package/dist/components/Library/ImageMask/ImageMask.demo.svelte +117 -0
  42. package/dist/components/Library/ImageMask/ImageMask.demo.svelte.d.ts +10 -0
  43. package/dist/components/Library/ImageMask/ImageMask.svelte +46 -0
  44. package/dist/components/Library/ImageMask/ImageMask.svelte.d.ts +20 -0
  45. package/dist/components/Library/ImageMask/MaskLayer.svelte +341 -0
  46. package/dist/components/Library/ImageMask/MaskLayer.svelte.d.ts +12 -0
  47. package/dist/components/Library/ImageMask/contour.d.ts +11 -0
  48. package/dist/components/Library/ImageMask/contour.js +152 -0
  49. package/dist/components/Library/ImageMask/index.d.ts +2 -0
  50. package/dist/components/Library/ImageMask/index.js +1 -0
  51. package/dist/components/Library/ImageMask/marchingAnts.d.ts +8 -0
  52. package/dist/components/Library/ImageMask/marchingAnts.js +29 -0
  53. package/dist/components/Library/ImageMask/maskSurface.d.ts +5 -0
  54. package/dist/components/Library/ImageMask/maskSurface.js +94 -0
  55. package/dist/components/Library/ImageMask/types.d.ts +23 -0
  56. package/dist/components/Library/ImageMask/types.js +1 -0
  57. package/dist/components/Library/Label/Label.demo.svelte +28 -0
  58. package/dist/components/Library/Label/Label.demo.svelte.d.ts +9 -0
  59. package/dist/components/Library/Label/Label.svelte +177 -0
  60. package/dist/components/Library/Label/Label.svelte.d.ts +18 -0
  61. package/dist/components/Library/Label/index.d.ts +1 -0
  62. package/dist/components/Library/Label/index.js +1 -0
  63. package/dist/components/Library/NumberField/NumberField.demo.svelte +21 -0
  64. package/dist/components/Library/NumberField/NumberField.demo.svelte.d.ts +8 -0
  65. package/dist/components/Library/NumberField/NumberField.svelte +194 -0
  66. package/dist/components/Library/NumberField/NumberField.svelte.d.ts +21 -0
  67. package/dist/components/Library/NumberField/index.d.ts +1 -0
  68. package/dist/components/Library/NumberField/index.js +1 -0
  69. package/dist/components/Library/ScrollView/ScrollView.svelte +25 -9
  70. package/dist/components/Library/ScrollView/ScrollView.svelte.d.ts +4 -4
  71. package/dist/components/Library/Spacer/Spacer.svelte +7 -0
  72. package/dist/components/Library/Spacer/Spacer.svelte.d.ts +26 -0
  73. package/dist/components/Library/Spacer/index.d.ts +1 -0
  74. package/dist/components/Library/Spacer/index.js +1 -0
  75. package/dist/components/Library/TextField/TextField.demo.svelte +14 -0
  76. package/dist/components/Library/TextField/TextField.demo.svelte.d.ts +8 -0
  77. package/dist/components/Library/TextField/TextField.svelte +149 -0
  78. package/dist/components/Library/TextField/TextField.svelte.d.ts +19 -0
  79. package/dist/components/Library/TextField/index.d.ts +1 -0
  80. package/dist/components/Library/TextField/index.js +1 -0
  81. package/dist/components/Library/VStack/VStack.svelte +45 -0
  82. package/dist/components/Library/VStack/VStack.svelte.d.ts +9 -0
  83. package/dist/components/Library/VStack/index.d.ts +1 -0
  84. package/dist/components/Library/VStack/index.js +1 -0
  85. package/dist/components/Local/ComponentGrid.svelte +15 -31
  86. package/dist/components/Local/HeroCard.svelte +26 -36
  87. package/dist/components/Local/HeroCard.svelte.d.ts +0 -2
  88. package/dist/index.d.ts +23 -0
  89. package/dist/index.js +17 -0
  90. package/dist/style/index.css +28 -17
  91. package/dist/style/label.d.ts +6 -0
  92. package/dist/style/label.js +4 -0
  93. package/dist/style/layout.d.ts +57 -0
  94. package/dist/style/layout.js +128 -0
  95. package/dist/style/media.d.ts +12 -0
  96. package/dist/style/media.js +8 -0
  97. package/dist/style/scroll.d.ts +7 -0
  98. package/dist/style/scroll.js +5 -0
  99. package/dist/style/text-editor.d.ts +34 -0
  100. package/dist/style/text-editor.js +29 -0
  101. package/dist/style.css +58 -35
  102. 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
- <div class="hstack items-center gap-3">
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
- </div>
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: 2
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
- <div class="vstack gap-2">
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 & Record<string, unknown>;
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: leadingEmptyDays + daysInMonth }, (_, index): CalendarCell => {
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-${dayNumber}`,
99
- label: String(dayNumber),
100
- isCurrentMonth: true,
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
- dayNumber === todayDate,
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 class="calendar-nav-button" type="button" aria-label="Previous month" onclick={() => shiftMonth(-1)}>
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 class="calendar-nav-button" type="button" aria-label="Next month" onclick={() => shiftMonth(1)}>
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
- {#if cell.isCurrentMonth && cell.value}
139
- <button
140
- type="button"
141
- class="calendar-day"
142
- class:calendar-day-today={cell.isToday}
143
- class:calendar-day-selected={cell.isSelected}
144
- class:calendar-day-weekend={cell.isWeekend}
145
- aria-pressed={cell.isSelected}
146
- onclick={() => selectDate(cell.value!)}
147
- >
148
- {cell.label}
149
- </button>
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(var(--calendar-cell-core-size) + (var(--sveltely-padding-y) * 2.2));
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-empty {
291
+ .calendar-day-outside {
287
292
  border-color: transparent;
288
293
  background: transparent;
289
- color: transparent;
290
- cursor: default;
294
+ color: var(--color-zinc-400);
291
295
  }
292
296
 
293
297
  .calendar-day-today {
@@ -0,0 +1,10 @@
1
+ <div class="divider" role="separator"></div>
2
+
3
+ <style>
4
+ .divider {
5
+ width: var(--divider-width, 100%);
6
+ height: var(--divider-height, 1px);
7
+ align-self: var(--divider-align-self, auto);
8
+ background: var(--color-gray-200);
9
+ }
10
+ </style>
@@ -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 items = [
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" searchable={true} />
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.by(() =>
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 (searchable) return false;
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: ${searchable ? 'var(--sveltely-border-radius)' : 'var(--sveltely-border-radius-nested)'};`}
175
+ style={`${dropdownStyle} --dropdown-item-radius: ${searchEnabled ? 'var(--sveltely-border-radius)' : 'var(--sveltely-border-radius-nested)'};`}
178
176
  >
179
- {#if searchable}
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 isGroup(entry)}
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: calc(var(--sveltely-padding-y) * 0.33) calc(var(--sveltely-padding-x) * 0.67);
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};`