@ibobbyts/svelte-ui-utils 0.4.1 → 0.4.3

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/README.md CHANGED
@@ -511,7 +511,12 @@ relational pseudo-class.
511
511
  The original flat `options` API remains available. New callers may opt into
512
512
  grouped options with `optionGroups`; each group can have an
513
513
  optional accessible label and its options keep the same keyboard and disabled
514
- semantics. `width`, `minWidth`, `maxWidth`, and `className` are optional styling
514
+ semantics. Set `groupsCollapsedByDefault` to `"true"` to initially collapse
515
+ all groups, or to `"auto"` to initially expand only groups containing a
516
+ selected option; `"false"` is the default. The initial state is evaluated each
517
+ time the menu opens, and selecting options while it remains open does not
518
+ change the current collapsed state. Group headings can always be toggled
519
+ manually. `width`, `minWidth`, `maxWidth`, and `className` are optional styling
515
520
  hooks. `onTriggerClick` runs before the dropdown toggles, so a nested control
516
521
  can stop event propagation without preventing the dropdown from opening.
517
522
 
@@ -845,3 +850,23 @@ bun add @ibobbyts/svelte-ui-utils@0.3.1 svelte
845
850
  ```
846
851
 
847
852
  Use it after each release when a Bun-based project will consume the package.
853
+
854
+ ## 分段日期输入(本地新增,尚未发布)
855
+
856
+ ```svelte
857
+ <script lang="ts">
858
+ import { SegmentedDateInput } from '@ibobbyts/svelte-ui-utils/segmented-date';
859
+ let dob = $state('');
860
+ </script>
861
+
862
+ <SegmentedDateInput name="dob" bind:value={dob} ariaLabel="生日"
863
+ yearLabel="年" monthLabel="月" dayLabel="日" autocomplete="bday" />
864
+ ```
865
+
866
+ 支持 `precision="year" | "month" | "day"`(默认 `day`)、数字输入、全角数字归一化、完整日期粘贴及月日失焦补零。`value` 可双向绑定;`onvalueinput` 接收用户输入后的组合值。日历有效性和业务范围由调用方校验。
867
+
868
+ 默认用隐藏字段提交组合值;`submitParts={true}` 改为提交 `${name}Year`、`${name}Month`、`${name}Day`(按精度决定字段)。支持 `required`、`readonly`、`disabled`、`id`(关联年输入框)和生日自动填充。`ariaLabel`、各段标签及占位符由调用方提供,以接入应用本地化。
869
+
870
+ 引入公共 `style.css` 后可独立使用,无需 Tailwind 或 DaisyUI。`class` 定制容器;`inputClass` 替换默认输入框装饰样式,布局仍由组件负责。MMS 通过该参数保留 DaisyUI 外观。
871
+
872
+ 纯函数和类型可从 `@ibobbyts/svelte-ui-utils/segmented-date/state` 引入:`splitSegmentedDate`、`composeSegmentedDate`、`parsePastedSegmentedDate`、`SegmentedDateParts`、`SegmentedDatePrecision`;此入口不加载 Svelte,适用于服务端和 Node。
@@ -21,6 +21,7 @@
21
21
  export let multiselect = false;
22
22
  export let options: DropdownOption[] = [];
23
23
  export let optionGroups: DropdownOptionGroup[] | undefined = undefined;
24
+ export let groupsCollapsedByDefault: 'true' | 'false' | 'auto' = 'false';
24
25
  export let ariaLabel: string | undefined = undefined;
25
26
  export let placement: DropdownPlacement = 'auto';
26
27
  export let menuAlign: DropdownMenuAlign = 'left';
@@ -56,11 +57,15 @@
56
57
  let typeaheadBuffer = '';
57
58
  let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
58
59
  let typeaheadGeneration = 0;
60
+ let collapsedGroupIndexes = new Set<number>();
59
61
  // Keep the buffer long enough for users to type multi-character aliases
60
62
  // while the menu is rendering a large option list.
61
63
  const typeaheadTimeoutMs = 1000;
62
64
 
63
65
  $: resolvedOptions = optionGroups === undefined ? options : optionGroups.flatMap((group) => group.options);
66
+ $: visibleOptions = optionGroups === undefined
67
+ ? options
68
+ : optionGroups.flatMap((group, groupIndex) => group.label && collapsedGroupIndexes.has(groupIndex) ? [] : group.options);
64
69
  $: selectedValues = normalizeSelectedValues(multiselect && Array.isArray(value) ? value : []);
65
70
  $: selectedOption = Array.isArray(value) ? undefined : resolvedOptions.find((option) => option.value === value);
66
71
  $: selectedText = multiselect
@@ -89,7 +94,7 @@
89
94
  }
90
95
 
91
96
  function firstEnabledOption(): DropdownOption | undefined {
92
- return resolvedOptions.find((option) => !option.disabled);
97
+ return visibleOptions.find((option) => !option.disabled);
93
98
  }
94
99
 
95
100
  function normalizeSelectedValues(nextValues: DropdownMultiValue): DropdownMultiValue {
@@ -104,7 +109,7 @@
104
109
  }
105
110
 
106
111
  function initialActiveValue(): DropdownValue {
107
- const firstSelectedEnabled = resolvedOptions.find(
112
+ const firstSelectedEnabled = visibleOptions.find(
108
113
  (option) => selectedValues.includes(option.value) && !option.disabled
109
114
  );
110
115
  return firstSelectedEnabled?.value ?? selectedOption?.value ?? firstEnabledOption()?.value ?? '';
@@ -115,21 +120,21 @@
115
120
  }
116
121
 
117
122
  function activeOptionIndex(nextActiveValue: DropdownValue): number {
118
- const index = resolvedOptions.findIndex((option) => option.value === nextActiveValue && !option.disabled);
123
+ const index = visibleOptions.findIndex((option) => option.value === nextActiveValue && !option.disabled);
119
124
  if (index >= 0) {
120
125
  return index;
121
126
  }
122
- const fallbackIndex = resolvedOptions.findIndex(
127
+ const fallbackIndex = visibleOptions.findIndex(
123
128
  (option) => isOptionSelected(option) && !option.disabled
124
129
  );
125
130
  if (fallbackIndex >= 0) {
126
131
  return fallbackIndex;
127
132
  }
128
- return resolvedOptions.findIndex((option) => !option.disabled);
133
+ return visibleOptions.findIndex((option) => !option.disabled);
129
134
  }
130
135
 
131
136
  function moveActiveOption(offset: number) {
132
- const enabledOptions = resolvedOptions.filter((option) => !option.disabled);
137
+ const enabledOptions = visibleOptions.filter((option) => !option.disabled);
133
138
  if (enabledOptions.length === 0) {
134
139
  return;
135
140
  }
@@ -180,7 +185,7 @@
180
185
  : key.toLocaleLowerCase();
181
186
  const nextBuffer = `${typeaheadBuffer}${normalizedKey}`;
182
187
  const findMatch = (prefix: string) =>
183
- resolvedOptions.find(
188
+ visibleOptions.find(
184
189
  (option) =>
185
190
  !option.disabled &&
186
191
  [option.searchText, option.label]
@@ -233,12 +238,42 @@
233
238
  }
234
239
  if (!open) {
235
240
  updateResolvedPlacement();
241
+ initializeCollapsedGroups();
236
242
  }
237
243
  clearTypeaheadBuffer();
238
244
  open = !open;
239
245
  activeValue = initialActiveValue();
240
246
  }
241
247
 
248
+ function isGroupCollapsed(groupIndex: number): boolean {
249
+ return collapsedGroupIndexes.has(groupIndex);
250
+ }
251
+
252
+ function initializeCollapsedGroups() {
253
+ if (optionGroups === undefined || groupsCollapsedByDefault === 'false') {
254
+ collapsedGroupIndexes = new Set();
255
+ return;
256
+ }
257
+ if (groupsCollapsedByDefault === 'true') {
258
+ collapsedGroupIndexes = new Set(
259
+ optionGroups.flatMap((group, index) => group.label ? [index] : [])
260
+ );
261
+ return;
262
+ }
263
+ collapsedGroupIndexes = new Set(
264
+ optionGroups.flatMap((group, index) =>
265
+ group.label && group.options.some((option) => isOptionSelected(option)) ? [] : group.label ? [index] : []
266
+ )
267
+ );
268
+ }
269
+
270
+ function toggleGroup(groupIndex: number) {
271
+ const next = new Set(collapsedGroupIndexes);
272
+ if (next.has(groupIndex)) next.delete(groupIndex);
273
+ else next.add(groupIndex);
274
+ collapsedGroupIndexes = next;
275
+ }
276
+
242
277
  function handleTriggerClick(event: MouseEvent) {
243
278
  onTriggerClick?.(event);
244
279
  toggleOpen();
@@ -284,7 +319,7 @@
284
319
  if ((event.key === 'Enter' || event.key === ' ') && open) {
285
320
  event.preventDefault();
286
321
  const index = activeOptionIndex(activeValue);
287
- const option = index >= 0 ? resolvedOptions[index] : undefined;
322
+ const option = index >= 0 ? visibleOptions[index] : undefined;
288
323
  if (option) {
289
324
  selectOption(option);
290
325
  }
@@ -558,15 +593,25 @@
558
593
  </button>
559
594
  {/each}
560
595
  {:else}
561
- {#each optionGroups as group}
596
+ {#each optionGroups as group, groupIndex}
562
597
  <div class="suu-dropdown__group" role="group" aria-label={group.label}>
563
598
  {#if group.label}
564
- <div class="suu-dropdown__group-label">{group.label}</div>
599
+ <button
600
+ type="button"
601
+ class="suu-dropdown__group-label"
602
+ aria-expanded={!collapsedGroupIndexes.has(groupIndex)}
603
+ on:click|stopPropagation={() => toggleGroup(groupIndex)}
604
+ >
605
+ <span>{group.label}</span>
606
+ <span class="suu-dropdown__group-chevron" aria-hidden="true"></span>
607
+ </button>
565
608
  {/if}
566
- {#each group.options as option}
609
+ {#if !group.label || !collapsedGroupIndexes.has(groupIndex)}
610
+ {#each group.options as option, optionIndex}
567
611
  <button
568
612
  type="button"
569
613
  class="suu-dropdown__option"
614
+ class:suu-dropdown__option--group-first={Boolean(group.label) && optionIndex === 0}
570
615
  class:suu-dropdown__option--active={option.value === activeValue}
571
616
  class:suu-dropdown__option--disabled={option.disabled}
572
617
  role="option"
@@ -591,7 +636,8 @@
591
636
  {/if}
592
637
  <span class="suu-dropdown__label">{option.label}</span>
593
638
  </button>
594
- {/each}
639
+ {/each}
640
+ {/if}
595
641
  </div>
596
642
  {/each}
597
643
  {/if}
@@ -18,6 +18,7 @@ declare const Dropdown: $$__sveltets_2_IsomorphicComponent<{
18
18
  multiselect?: boolean;
19
19
  options?: DropdownOption[];
20
20
  optionGroups?: DropdownOptionGroup[] | undefined;
21
+ groupsCollapsedByDefault?: "true" | "false" | "auto";
21
22
  ariaLabel?: string | undefined;
22
23
  placement?: DropdownPlacement;
23
24
  menuAlign?: DropdownMenuAlign;
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAER,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AA+ftB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAVqb,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUz/B,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAER,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AAuiBtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAV0e,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUpmC,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
@@ -16,6 +16,7 @@
16
16
  export let value: DropdownMultiValue = [];
17
17
  export let options: DropdownOption[] = [];
18
18
  export let optionGroups: DropdownOptionGroup[] | undefined = undefined;
19
+ export let groupsCollapsedByDefault: 'true' | 'false' | 'auto' = 'false';
19
20
  export let ariaLabel: string | undefined = undefined;
20
21
  export let placement: DropdownPlacement = 'auto';
21
22
  export let menuAlign: DropdownMenuAlign = 'left';
@@ -37,6 +38,7 @@
37
38
  multiselect={true}
38
39
  {options}
39
40
  {optionGroups}
41
+ {groupsCollapsedByDefault}
40
42
  {ariaLabel}
41
43
  {placement}
42
44
  {menuAlign}
@@ -17,6 +17,7 @@ declare const DropdownMultiSelect: $$__sveltets_2_IsomorphicComponent<{
17
17
  value?: DropdownMultiValue;
18
18
  options?: DropdownOption[];
19
19
  optionGroups?: DropdownOptionGroup[] | undefined;
20
+ groupsCollapsedByDefault?: "true" | "false" | "auto";
20
21
  ariaLabel?: string | undefined;
21
22
  placement?: DropdownPlacement;
22
23
  menuAlign?: DropdownMenuAlign;
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/DropdownMultiSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,YAAY,CAAC;AA+BtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,mBAAmB;SAV6W,MAAM,GAAG,SAAS;YAAU,kBAAkB;cAAY,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;YAAwG,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,0BAA0B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUr1B,CAAC;AACvF,KAAK,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"DropdownMultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/DropdownMultiSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,YAAY,CAAC;AAgCtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,mBAAmB;SAVka,MAAM,GAAG,SAAS;YAAU,kBAAkB;cAAY,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;YAAwG,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,0BAA0B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUh8B,CAAC;AACvF,KAAK,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,eAAe,mBAAmB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export * from './ordered-list/index.js';
9
9
  export * from './sortable-table/index.js';
10
10
  export { getUiMessages, resolveUiLanguage, uiLanguages } from './i18n.js';
11
11
  export type { UiLanguage, UiMessages } from './i18n.js';
12
+ export * from './segmented-date/index.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1E,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1E,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACxD,cAAc,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export * from './sortable-list/index.js';
8
8
  export * from './ordered-list/index.js';
9
9
  export * from './sortable-table/index.js';
10
10
  export { getUiMessages, resolveUiLanguage, uiLanguages } from './i18n.js';
11
+ export * from './segmented-date/index.js';
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import {
4
+ composeSegmentedDate,
5
+ parsePastedSegmentedDate,
6
+ splitSegmentedDate,
7
+ type SegmentedDatePrecision,
8
+ } from "./state.js";
9
+
10
+ let {
11
+ value = $bindable(""),
12
+ precision = "day",
13
+ name,
14
+ id,
15
+ required = false,
16
+ readonly = false,
17
+ disabled = false,
18
+ ariaLabel,
19
+ yearLabel,
20
+ monthLabel,
21
+ dayLabel,
22
+ yearPlaceholder = "YYYY",
23
+ monthPlaceholder = "MM",
24
+ dayPlaceholder = "DD",
25
+ submitParts = false,
26
+ autocomplete,
27
+ class: className = "",
28
+ inputClass = "",
29
+ onvalueinput,
30
+ } = $props<{
31
+ value?: string;
32
+ precision?: SegmentedDatePrecision;
33
+ name: string;
34
+ id?: string;
35
+ required?: boolean;
36
+ readonly?: boolean;
37
+ disabled?: boolean;
38
+ ariaLabel: string;
39
+ yearLabel: string;
40
+ monthLabel?: string;
41
+ dayLabel?: string;
42
+ yearPlaceholder?: string;
43
+ monthPlaceholder?: string;
44
+ dayPlaceholder?: string;
45
+ submitParts?: boolean;
46
+ autocomplete?: "bday";
47
+ class?: string;
48
+ inputClass?: string;
49
+ onvalueinput?: (value: string) => void;
50
+ }>();
51
+
52
+ let year = $state("");
53
+ let month = $state("");
54
+ let day = $state("");
55
+
56
+ $effect(() => {
57
+ const parts = splitSegmentedDate(value, precision);
58
+ // Keep partially entered segments while the child is emitting an incomplete value.
59
+ const current = untrack(() => composeSegmentedDate({ year, month, day }, precision));
60
+ if (value !== current) {
61
+ year = parts.year;
62
+ month = parts.month;
63
+ day = parts.day;
64
+ }
65
+ });
66
+
67
+ function updateValue() {
68
+ const nextValue = composeSegmentedDate({ year, month, day }, precision);
69
+ if (nextValue === value) return;
70
+ value = nextValue;
71
+ onvalueinput?.(value);
72
+ }
73
+
74
+ function handleInput(part: SegmentedDatePrecision, event: Event) {
75
+ const input = event.currentTarget as HTMLInputElement;
76
+ const normalized = input.value.normalize("NFKC").replace(/\D/g, "")
77
+ .slice(0, part === "year" ? 4 : 2);
78
+ input.value = normalized;
79
+ if (part === "year") year = normalized;
80
+ if (part === "month") month = normalized;
81
+ if (part === "day") day = normalized;
82
+ updateValue();
83
+ }
84
+
85
+ function handleBlur(part: "month" | "day") {
86
+ if (part === "month" && /^\d$/.test(month)) month = month.padStart(2, "0");
87
+ if (part === "day" && /^\d$/.test(day)) day = day.padStart(2, "0");
88
+ updateValue();
89
+ }
90
+
91
+ function handlePaste(event: ClipboardEvent) {
92
+ const parts = parsePastedSegmentedDate(
93
+ event.clipboardData?.getData("text") ?? "",
94
+ precision,
95
+ );
96
+ if (!parts) return;
97
+ event.preventDefault();
98
+ year = parts.year;
99
+ month = parts.month;
100
+ day = parts.day;
101
+ updateValue();
102
+ }
103
+ </script>
104
+
105
+ {#if !submitParts}
106
+ <input type="hidden" {name} {value} {disabled} />
107
+ {/if}
108
+
109
+ <div
110
+ class={["suu-segmented-date", className]}
111
+ data-precision={precision}
112
+ role="group"
113
+ aria-label={ariaLabel}
114
+ >
115
+ <input
116
+ class={["suu-segmented-date__input", inputClass]}
117
+ class:suu-segmented-date__input--default={!inputClass}
118
+ type="text"
119
+ name={submitParts ? `${name}Year` : undefined}
120
+ {id}
121
+ bind:value={year}
122
+ oninput={(event) => handleInput("year", event)}
123
+ onpaste={handlePaste}
124
+ inputmode="numeric"
125
+ pattern="[0-9][0-9][0-9][0-9]"
126
+ maxlength="4"
127
+ placeholder={yearPlaceholder}
128
+ aria-label={yearLabel}
129
+ autocomplete={autocomplete ? "bday-year" : undefined}
130
+ {required}
131
+ {readonly}
132
+ {disabled}
133
+ />
134
+ {#if precision !== "year"}
135
+ <input
136
+ class={["suu-segmented-date__input", inputClass]}
137
+ class:suu-segmented-date__input--default={!inputClass}
138
+ type="text"
139
+ name={submitParts ? `${name}Month` : undefined}
140
+ bind:value={month}
141
+ oninput={(event) => handleInput("month", event)}
142
+ onblur={() => handleBlur("month")}
143
+ onpaste={handlePaste}
144
+ inputmode="numeric"
145
+ pattern="[0-9][0-9]?"
146
+ maxlength="2"
147
+ placeholder={monthPlaceholder}
148
+ aria-label={monthLabel}
149
+ autocomplete={autocomplete ? "bday-month" : undefined}
150
+ {required}
151
+ {readonly}
152
+ {disabled}
153
+ />
154
+ {/if}
155
+ {#if precision === "day"}
156
+ <input
157
+ class={["suu-segmented-date__input", inputClass]}
158
+ class:suu-segmented-date__input--default={!inputClass}
159
+ type="text"
160
+ name={submitParts ? `${name}Day` : undefined}
161
+ bind:value={day}
162
+ oninput={(event) => handleInput("day", event)}
163
+ onblur={() => handleBlur("day")}
164
+ onpaste={handlePaste}
165
+ inputmode="numeric"
166
+ pattern="[0-9][0-9]?"
167
+ maxlength="2"
168
+ placeholder={dayPlaceholder}
169
+ aria-label={dayLabel}
170
+ autocomplete={autocomplete ? "bday-day" : undefined}
171
+ {required}
172
+ {readonly}
173
+ {disabled}
174
+ />
175
+ {/if}
176
+ </div>
177
+
178
+ <style>
179
+ .suu-segmented-date { display: grid; min-width: 0; gap: 0.5rem; }
180
+ .suu-segmented-date[data-precision="year"] { grid-template-columns: minmax(0, 1fr); }
181
+ .suu-segmented-date[data-precision="month"] { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
182
+ .suu-segmented-date[data-precision="day"] { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
183
+ .suu-segmented-date__input { min-width: 0; width: 100%; box-sizing: border-box; }
184
+ .suu-segmented-date__input--default {
185
+ padding: 0.5rem 0.75rem;
186
+ border: 1px solid var(--suu-color-border);
187
+ border-radius: var(--suu-radius);
188
+ background: var(--suu-color-bg);
189
+ color: var(--suu-color-text);
190
+ font: inherit;
191
+ }
192
+ .suu-segmented-date__input--default:focus-visible { outline: 2px solid var(--suu-color-accent); outline-offset: 2px; }
193
+ </style>
@@ -0,0 +1,26 @@
1
+ import { type SegmentedDatePrecision } from "./state.js";
2
+ type $$ComponentProps = {
3
+ value?: string;
4
+ precision?: SegmentedDatePrecision;
5
+ name: string;
6
+ id?: string;
7
+ required?: boolean;
8
+ readonly?: boolean;
9
+ disabled?: boolean;
10
+ ariaLabel: string;
11
+ yearLabel: string;
12
+ monthLabel?: string;
13
+ dayLabel?: string;
14
+ yearPlaceholder?: string;
15
+ monthPlaceholder?: string;
16
+ dayPlaceholder?: string;
17
+ submitParts?: boolean;
18
+ autocomplete?: "bday";
19
+ class?: string;
20
+ inputClass?: string;
21
+ onvalueinput?: (value: string) => void;
22
+ };
23
+ declare const SegmentedDateInput: import("svelte").Component<$$ComponentProps, {}, "value">;
24
+ type SegmentedDateInput = ReturnType<typeof SegmentedDateInput>;
25
+ export default SegmentedDateInput;
26
+ //# sourceMappingURL=SegmentedDateInput.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SegmentedDateInput.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/segmented-date/SegmentedDateInput.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAIC,KAAK,sBAAsB,EAC9B,MAAM,YAAY,CAAC;AAEvB,KAAK,gBAAgB,GAAG;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,CAAC;AAkGN,QAAA,MAAM,kBAAkB,2DAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { default as SegmentedDateInput } from './SegmentedDateInput.svelte';
2
+ export * from './state.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/segmented-date/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,cAAc,YAAY,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as SegmentedDateInput } from './SegmentedDateInput.svelte';
2
+ export * from './state.js';
@@ -0,0 +1,10 @@
1
+ export type SegmentedDatePrecision = "year" | "month" | "day";
2
+ export type SegmentedDateParts = {
3
+ year: string;
4
+ month: string;
5
+ day: string;
6
+ };
7
+ export declare function splitSegmentedDate(value: string, precision: SegmentedDatePrecision): SegmentedDateParts;
8
+ export declare function composeSegmentedDate(parts: SegmentedDateParts, precision: SegmentedDatePrecision): string;
9
+ export declare function parsePastedSegmentedDate(value: string, precision: SegmentedDatePrecision): SegmentedDateParts | null;
10
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/lib/segmented-date/state.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACf,CAAC;AAQF,wBAAgB,kBAAkB,CAC9B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,sBAAsB,GAClC,kBAAkB,CAuBpB;AAED,wBAAgB,oBAAoB,CAChC,KAAK,EAAE,kBAAkB,EACzB,SAAS,EAAE,sBAAsB,GAClC,MAAM,CAqBR;AAED,wBAAgB,wBAAwB,CACpC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,sBAAsB,GAClC,kBAAkB,GAAG,IAAI,CAe3B"}
@@ -0,0 +1,67 @@
1
+ const EMPTY_PARTS = { year: "", month: "", day: "" };
2
+ function normalizePart(value) {
3
+ return value.normalize("NFKC").trim();
4
+ }
5
+ export function splitSegmentedDate(value, precision) {
6
+ const normalized = value.normalize("NFKC").trim();
7
+ const patterns = {
8
+ year: /^([^-]*)$/,
9
+ month: /^([^-]*)-([^-]*)$/,
10
+ day: /^([^-]*)-([^-]*)-([^-]*)$/,
11
+ };
12
+ const match = normalized.match(patterns[precision]);
13
+ if (!match) {
14
+ if (precision === "month" && /^\d{4}$/.test(normalized)) {
15
+ return { year: normalized, month: "", day: "" };
16
+ }
17
+ if (precision === "month" && /^\d{1,2}$/.test(normalized)) {
18
+ return { year: "", month: normalized, day: "" };
19
+ }
20
+ return { ...EMPTY_PARTS };
21
+ }
22
+ return {
23
+ year: match[1] ?? "",
24
+ month: precision === "year" ? "" : (match[2] ?? ""),
25
+ day: precision === "day" ? (match[3] ?? "") : "",
26
+ };
27
+ }
28
+ export function composeSegmentedDate(parts, precision) {
29
+ const year = normalizePart(parts.year);
30
+ const month = normalizePart(parts.month);
31
+ const day = normalizePart(parts.day);
32
+ if (!year && (precision === "year" || !month) && (precision !== "day" || !day)) {
33
+ return "";
34
+ }
35
+ if (precision === "year")
36
+ return year;
37
+ if (precision === "month") {
38
+ if (/^\d{4}$/.test(year) && /^\d{1,2}$/.test(month)) {
39
+ return `${year}-${month.padStart(2, "0")}`;
40
+ }
41
+ if (/^\d{4}$/.test(year) && !month)
42
+ return year;
43
+ if (!year && /^\d{1,2}$/.test(month))
44
+ return month.padStart(2, "0");
45
+ return `${year}-${month}`;
46
+ }
47
+ if (/^\d{4}$/.test(year) && /^\d{1,2}$/.test(month) && /^\d{1,2}$/.test(day)) {
48
+ return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
49
+ }
50
+ return `${year}-${month}-${day}`;
51
+ }
52
+ export function parsePastedSegmentedDate(value, precision) {
53
+ const normalized = value.normalize("NFKC").trim();
54
+ const patterns = {
55
+ year: /^(\d{4})$/,
56
+ month: /^(?:(\d{4})[-/.](\d{1,2})|(\d{1,2}))$/,
57
+ day: /^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/,
58
+ };
59
+ const match = normalized.match(patterns[precision]);
60
+ if (!match)
61
+ return null;
62
+ return {
63
+ year: match[1] ?? "",
64
+ month: precision === "year" ? "" : (match[2] ?? match[3]).padStart(2, "0"),
65
+ day: precision === "day" ? match[3].padStart(2, "0") : "",
66
+ };
67
+ }
package/dist/style.css CHANGED
@@ -1283,6 +1283,7 @@
1283
1283
  .suu-dropdown__panel {
1284
1284
  max-height: var(--suu-dropdown-panel-max-height, 100vh);
1285
1285
  overflow-y: auto;
1286
+ overflow-x: hidden;
1286
1287
  background: var(--suu-color-bg);
1287
1288
  overscroll-behavior: contain;
1288
1289
  }
@@ -1292,12 +1293,33 @@
1292
1293
  }
1293
1294
 
1294
1295
  .suu-dropdown__group-label {
1296
+ display: flex;
1297
+ width: 100%;
1298
+ border: 0;
1295
1299
  padding: 8px 14px 6px;
1296
1300
  background: var(--suu-color-surface);
1297
1301
  color: var(--suu-color-muted);
1302
+ cursor: pointer;
1298
1303
  font-size: 0.75em;
1299
1304
  font-weight: 700;
1300
1305
  letter-spacing: 0.02em;
1306
+ font: inherit;
1307
+ text-align: left;
1308
+ align-items: center;
1309
+ justify-content: space-between;
1310
+ }
1311
+
1312
+ .suu-dropdown__group-chevron {
1313
+ width: 7px;
1314
+ height: 7px;
1315
+ border-right: 2px solid currentColor;
1316
+ border-bottom: 2px solid currentColor;
1317
+ transform: rotate(225deg);
1318
+ transition: transform 120ms ease;
1319
+ }
1320
+
1321
+ .suu-dropdown__group-label[aria-expanded='true'] .suu-dropdown__group-chevron {
1322
+ transform: rotate(45deg);
1301
1323
  }
1302
1324
 
1303
1325
  .suu-dropdown__option {
@@ -1313,6 +1335,10 @@
1313
1335
  text-align: left;
1314
1336
  }
1315
1337
 
1338
+ .suu-dropdown__option--group-first {
1339
+ border-top: 1px solid var(--suu-color-border);
1340
+ }
1341
+
1316
1342
  .suu-dropdown--multiselect .suu-dropdown__option {
1317
1343
  display: flex;
1318
1344
  gap: 10px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibobbyts/svelte-ui-utils",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Reusable Svelte 5 UI utilities for dialogs, toast notifications, dropdown search, and data tables.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -77,7 +77,16 @@
77
77
  "svelte": "./dist/sortable-table/index.js",
78
78
  "default": "./dist/sortable-table/index.js"
79
79
  },
80
- "./style.css": "./dist/style.css"
80
+ "./style.css": "./dist/style.css",
81
+ "./segmented-date": {
82
+ "types": "./dist/segmented-date/index.d.ts",
83
+ "svelte": "./dist/segmented-date/index.js",
84
+ "default": "./dist/segmented-date/index.js"
85
+ },
86
+ "./segmented-date/state": {
87
+ "types": "./dist/segmented-date/state.d.ts",
88
+ "default": "./dist/segmented-date/state.js"
89
+ }
81
90
  },
82
91
  "publishConfig": {
83
92
  "access": "public",