@insymetri/styleguide 0.1.45 → 0.1.47

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.
@@ -41,18 +41,46 @@
41
41
  const showError = $derived(error || !!errorMessage)
42
42
 
43
43
  let placeholder = $state<DateValue>(today(getLocalTimeZone()))
44
+ let inputEl = $state<HTMLElement | null>(null)
45
+ let internalValue = $state<DateValue | undefined>(value)
46
+
47
+ // Sync external value changes into internal state
48
+ $effect(() => {
49
+ internalValue = value
50
+ })
51
+
52
+ function allSegmentsEmpty() {
53
+ if (!inputEl) return true
54
+ const segments = inputEl.querySelectorAll('[data-segment]:not([data-segment="trigger"]):not([data-segment="literal"])')
55
+ return Array.from(segments).every(s => s.hasAttribute('data-placeholder'))
56
+ }
57
+
58
+ function handleValueChange(newValue: DateValue | undefined) {
59
+ if (newValue === undefined && !allSegmentsEmpty()) {
60
+ // A segment was partially cleared — restore the internal value
61
+ // to prevent bits-ui from wiping the date
62
+ internalValue = value
63
+ return
64
+ }
65
+ internalValue = newValue
66
+ value = newValue
67
+ onValueChange?.(newValue)
68
+ }
44
69
  </script>
45
70
 
46
- <div class={cn('flex flex-col gap-4', className)}>
47
- <DatePicker.Root bind:value bind:placeholder {onValueChange} {disabled}>
71
+ <div class={cn('relative flex flex-col gap-4 pb-20', className)}>
72
+ <DatePicker.Root value={internalValue} bind:placeholder onValueChange={handleValueChange} {disabled}>
48
73
  {#if label}
49
74
  <DatePicker.Label class="text-small-emphasis text-secondary">{label}</DatePicker.Label>
50
75
  {/if}
51
76
  <DatePicker.Input
77
+ bind:ref={inputEl}
52
78
  class={cn(
53
- 'flex items-center gap-4 px-12 border bg-input-bg border-input-border transition-all duration-fast hover:border-input-border-hover [&:has(:focus)]:border-accent [&:has(:focus)]:ring-3 [&:has(:focus)]:ring-primary motion-reduce:transition-none',
79
+ 'flex items-center gap-4 px-12 border bg-input-bg border-input-border transition-all duration-fast [&:has(:focus)]:ring-3 motion-reduce:transition-none',
54
80
  densityClasses[density.value],
55
- showError && 'border-input-border-error [&:has(:focus)]:border-input-border-error [&:has(:focus)]:ring-error',
81
+ showError
82
+ ? 'border-input-border-error hover:border-input-border-error [&:has(:focus)]:border-input-border-error [&:has(:focus)]:ring-error'
83
+ : 'hover:[&:not(:has(:focus))]:border-input-border-hover [&:has(:focus)]:border-accent [&:has(:focus)]:ring-primary',
56
84
  disabled && 'bg-input-bg-disabled cursor-not-allowed'
57
85
  )}
58
86
  >
@@ -81,8 +109,8 @@
81
109
  </DatePicker.Content>
82
110
  </DatePicker.Root>
83
111
  {#if showError && errorMessage}
84
- <span class="text-tiny text-error">{errorMessage}</span>
112
+ <span class="absolute bottom-0 left-0 text-tiny text-error" data-field-error>{errorMessage}</span>
85
113
  {:else if helperText}
86
- <span class="text-tiny text-secondary">{helperText}</span>
114
+ <span class="absolute bottom-0 left-0 text-tiny text-secondary">{helperText}</span>
87
115
  {/if}
88
116
  </div>
@@ -15,7 +15,10 @@
15
15
  placeholder?: string
16
16
  label?: string
17
17
  disabled?: boolean
18
+ error?: boolean
19
+ errorMessage?: string
18
20
  onSelect?: (value: string) => void
21
+ onblur?: () => void
19
22
  matchTriggerWidth?: boolean
20
23
  renderItem?: Snippet<[item: MenuItem, selected: boolean]>
21
24
  renderSelected?: Snippet<[item: MenuItem]>
@@ -30,7 +33,10 @@
30
33
  placeholder = 'Select...',
31
34
  label,
32
35
  disabled = false,
36
+ error = false,
37
+ errorMessage,
33
38
  onSelect,
39
+ onblur,
34
40
  matchTriggerWidth = false,
35
41
  renderItem,
36
42
  renderSelected,
@@ -39,6 +45,8 @@
39
45
  class: className,
40
46
  }: Props = $props()
41
47
 
48
+ const showError = $derived(error || !!errorMessage)
49
+
42
50
  const density = useDensity()
43
51
 
44
52
  const densityClasses = {
@@ -79,6 +87,7 @@
79
87
  value = item.value
80
88
  open = false
81
89
  onSelect?.(item.value)
90
+ onblur?.()
82
91
  triggerEl?.focus()
83
92
  }
84
93
 
@@ -144,6 +153,7 @@
144
153
  function close() {
145
154
  open = false
146
155
  triggerEl?.focus()
156
+ onblur?.()
147
157
  }
148
158
 
149
159
  $effect(() => {
@@ -214,7 +224,7 @@
214
224
  }
215
225
  </script>
216
226
 
217
- <div class="flex flex-col">
227
+ <div class="relative flex flex-col pb-20">
218
228
  {#if label}
219
229
  <span class="text-small-emphasis text-secondary mb-4">{label}</span>
220
230
  {/if}
@@ -226,9 +236,12 @@
226
236
  aria-expanded={open}
227
237
  {disabled}
228
238
  class={cn(
229
- 'flex items-center justify-between gap-4 py-5 pl-12 pr-8 border bg-input-bg cursor-default text-button-secondary box-border appearance-none font-inherit outline-none transition-colors duration-base ease-in-out hover:text-button-secondary-hover hover:border-button-secondary-hover focus:border-accent focus:ring-3 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed',
239
+ 'flex items-center justify-between gap-4 py-5 pl-12 pr-8 border bg-input-bg cursor-default text-button-secondary box-border appearance-none font-inherit outline-none transition-colors duration-base ease-in-out hover:text-button-secondary-hover focus-visible:ring-3 disabled:opacity-50 disabled:cursor-not-allowed',
230
240
  densityClasses[density.value],
231
- open ? 'border-button-secondary-hover' : 'border-button-secondary',
241
+ showError
242
+ ? 'border-input-border-error hover:border-input-border-error focus-visible:border-input-border-error focus-visible:ring-error'
243
+ : cn('hover:not-focus-visible:border-button-secondary-hover focus-visible:border-accent focus-visible:ring-primary', open ? 'border-button-secondary-hover' : 'border-button-secondary'),
244
+
232
245
  className
233
246
  )}
234
247
  onclick={toggle}
@@ -304,4 +317,7 @@
304
317
  </div>
305
318
  </div>
306
319
  {/if}
320
+ {#if showError && errorMessage}
321
+ <span class="absolute bottom-0 left-0 text-tiny text-error" data-field-error>{errorMessage}</span>
322
+ {/if}
307
323
  </div>
@@ -6,7 +6,10 @@ type Props = {
6
6
  placeholder?: string;
7
7
  label?: string;
8
8
  disabled?: boolean;
9
+ error?: boolean;
10
+ errorMessage?: string;
9
11
  onSelect?: (value: string) => void;
12
+ onblur?: () => void;
10
13
  matchTriggerWidth?: boolean;
11
14
  renderItem?: Snippet<[item: MenuItem, selected: boolean]>;
12
15
  renderSelected?: Snippet<[item: MenuItem]>;
@@ -47,22 +47,9 @@
47
47
  const isSearch = $derived(type === 'search')
48
48
  const showClear = $derived(isSearch && !!value)
49
49
  const hasAddons = $derived(!!prefix || !!suffix || isSearch)
50
- let shouldShake = $state(false)
51
- let prevShowError = $state(false)
52
-
53
- $effect(() => {
54
- if (showError && !prevShowError) {
55
- shouldShake = true
56
- const timer = setTimeout(() => {
57
- shouldShake = false
58
- }, 400)
59
- return () => clearTimeout(timer)
60
- }
61
- prevShowError = showError
62
- })
63
50
  </script>
64
51
 
65
- <div class={cn('flex flex-col', className)}>
52
+ <div class={cn('relative flex flex-col pb-20', className)}>
66
53
  {#if label}
67
54
  <label for={restProps.id} class="text-small-emphasis text-secondary mb-4">
68
55
  {label}
@@ -71,11 +58,12 @@
71
58
  {#if hasAddons}
72
59
  <div
73
60
  class={cn(
74
- 'flex items-center bg-input-bg border border-button-secondary transition-all duration-fast hover:border-button-secondary-hover focus-within:border-accent focus-within:ring-3 focus-within:ring-primary motion-reduce:transition-none',
61
+ 'flex items-center bg-input-bg border border-button-secondary transition-all duration-fast focus-within:ring-3 motion-reduce:transition-none',
75
62
  densityClasses[density.value],
76
- showError && 'border-input-border-error focus-within:border-input-border-error focus-within:ring-error',
77
- shouldShake && 'animate-shake',
78
- disabled && 'bg-input-bg-disabled cursor-not-allowed'
63
+ showError
64
+ ? 'border-input-border-error hover:border-input-border-error focus-within:border-input-border-error focus-within:ring-error'
65
+ : 'hover:not-focus-within:border-button-secondary-hover focus-within:border-accent focus-within:ring-primary',
66
+ disabled && 'bg-input-bg-disabled cursor-not-allowed'
79
67
  )}
80
68
  >
81
69
  {#if prefix}
@@ -117,17 +105,18 @@
117
105
  {type}
118
106
  {disabled}
119
107
  class={cn(
120
- 'py-5 px-12 font-[family-name:var(--font-family)] text-input-text bg-input-bg border border-button-secondary transition-all duration-fast outline-none w-full box-border placeholder:text-input-placeholder hover:border-button-secondary-hover focus:border-accent focus:ring-3 focus:ring-primary disabled:bg-input-bg-disabled disabled:text-input-text-disabled disabled:cursor-not-allowed motion-reduce:transition-none motion-reduce:animate-none',
108
+ 'py-5 px-12 font-[family-name:var(--font-family)] text-input-text bg-input-bg border border-button-secondary transition-all duration-fast outline-none w-full box-border placeholder:text-input-placeholder focus:ring-3 disabled:bg-input-bg-disabled disabled:text-input-text-disabled disabled:cursor-not-allowed motion-reduce:transition-none motion-reduce:animate-none',
121
109
  densityClasses[density.value],
122
- showError && 'border-input-border-error focus:border-input-border-error focus:ring-error',
123
- shouldShake && 'animate-shake'
110
+ showError
111
+ ? 'border-input-border-error hover:border-input-border-error focus:border-input-border-error focus:ring-error'
112
+ : 'hover:not-focus:border-button-secondary-hover focus:border-accent focus:ring-primary'
124
113
  )}
125
114
  {...restProps}
126
115
  />
127
116
  {/if}
128
117
  {#if showError && errorMessage}
129
- <span class="text-tiny text-error mt-4">{errorMessage}</span>
118
+ <span class="absolute bottom-0 left-0 text-tiny text-error" data-field-error>{errorMessage}</span>
130
119
  {:else if helperText}
131
- <span class="text-tiny text-secondary mt-4">{helperText}</span>
120
+ <span class="absolute bottom-0 left-0 text-tiny text-secondary">{helperText}</span>
132
121
  {/if}
133
122
  </div>
@@ -69,7 +69,7 @@
69
69
  --color-input-border: #d1d5db;
70
70
  --color-input-border-hover: #9ca3af;
71
71
  --color-input-border-active: #111827;
72
- --color-input-border-error: var(--ii-error);
72
+ --color-input-border-error: var(--ii-error-text);
73
73
  --color-input-text: var(--ii-text-primary);
74
74
  --color-input-text-disabled: var(--ii-text-secondary);
75
75
  --color-input-placeholder: var(--ii-text-tertiary);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insymetri/styleguide",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Insymetri shared UI component library built with Svelte 5",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -54,6 +54,9 @@
54
54
  "bits-ui": "^2.11.4",
55
55
  "svelte-sonner": "^1.0.6"
56
56
  },
57
+ "overrides": {
58
+ "esbuild": "0.27.4"
59
+ },
57
60
  "devDependencies": {
58
61
  "@storybook/addon-docs": "^10.2.10",
59
62
  "@storybook/svelte-vite": "^10.2.10",
@@ -73,9 +76,6 @@
73
76
  "type": "git",
74
77
  "url": ""
75
78
  },
76
- "overrides": {
77
- "esbuild": "0.27.4"
78
- },
79
79
  "optionalDependencies": {
80
80
  "@rollup/rollup-darwin-arm64": "^4.60.1"
81
81
  }