@fy-/fws-vue-core 3.0.3 → 3.0.5

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 (121) hide show
  1. package/package.json +6 -8
  2. package/src/components/fws/CmsArticleBoxed.vue +247 -0
  3. package/src/components/fws/CmsArticleSingle.vue +201 -0
  4. package/src/components/fws/DataTable.vue +659 -0
  5. package/src/components/fws/FilterData.vue +423 -0
  6. package/src/components/fws/UserData.vue +220 -0
  7. package/src/components/fws/UserFlow.vue +955 -0
  8. package/src/components/fws/UserOAuth2.vue +521 -0
  9. package/src/components/fws/UserProfile.vue +615 -0
  10. package/src/components/fws/UserProfileStrict.vue +233 -0
  11. package/src/components/ssr/ClientOnly.ts +10 -0
  12. package/src/components/ui/DefaultBreadcrumb.vue +99 -0
  13. package/src/components/ui/DefaultConfirm.vue +178 -0
  14. package/src/components/ui/DefaultConfirmWithInput.vue +217 -0
  15. package/src/components/ui/DefaultDropdown.vue +104 -0
  16. package/src/components/ui/DefaultDropdownLink.vue +94 -0
  17. package/src/components/ui/DefaultGallery.vue +1056 -0
  18. package/src/components/ui/DefaultInput.vue +768 -0
  19. package/src/components/ui/DefaultLoader.vue +125 -0
  20. package/src/components/ui/DefaultModal.vue +350 -0
  21. package/src/components/ui/DefaultNotif.vue +332 -0
  22. package/src/components/ui/DefaultPaging.vue +395 -0
  23. package/src/components/ui/DefaultSidebar.vue +267 -0
  24. package/src/components/ui/DefaultTagInput.vue +415 -0
  25. package/src/components/ui/transitions/CollapseTransition.vue +19 -0
  26. package/src/components/ui/transitions/ExpandTransition.vue +19 -0
  27. package/src/components/ui/transitions/FadeTransition.vue +17 -0
  28. package/src/components/ui/transitions/ScaleTransition.vue +21 -0
  29. package/src/components/ui/transitions/SlideTransition.vue +32 -0
  30. package/src/composables/event-bus.ts +15 -0
  31. package/src/composables/rest.ts +165 -0
  32. package/src/composables/seo.ts +142 -0
  33. package/src/composables/ssr.ts +103 -0
  34. package/src/composables/templating.ts +133 -0
  35. package/src/composables/translations.ts +45 -0
  36. package/src/env.d.ts +10 -0
  37. package/{dist/src/index.d.ts → src/index.ts} +71 -45
  38. package/src/plugin.ts +42 -0
  39. package/src/safelist.html +11 -0
  40. package/src/stores/serverRouter.ts +62 -0
  41. package/src/stores/user.ts +118 -0
  42. package/src/types.ts +58 -0
  43. package/dist/index.css +0 -2
  44. package/dist/index.js +0 -5767
  45. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts +0 -32
  46. package/dist/src/components/fws/CmsArticleBoxed.vue.d.ts.map +0 -1
  47. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts +0 -29
  48. package/dist/src/components/fws/CmsArticleSingle.vue.d.ts.map +0 -1
  49. package/dist/src/components/fws/DataTable.vue.d.ts +0 -52
  50. package/dist/src/components/fws/DataTable.vue.d.ts.map +0 -1
  51. package/dist/src/components/fws/FilterData.vue.d.ts +0 -15
  52. package/dist/src/components/fws/FilterData.vue.d.ts.map +0 -1
  53. package/dist/src/components/fws/UserData.vue.d.ts +0 -8
  54. package/dist/src/components/fws/UserData.vue.d.ts.map +0 -1
  55. package/dist/src/components/fws/UserFlow.vue.d.ts +0 -116
  56. package/dist/src/components/fws/UserFlow.vue.d.ts.map +0 -1
  57. package/dist/src/components/fws/UserOAuth2.vue.d.ts +0 -17
  58. package/dist/src/components/fws/UserOAuth2.vue.d.ts.map +0 -1
  59. package/dist/src/components/fws/UserProfile.vue.d.ts +0 -40
  60. package/dist/src/components/fws/UserProfile.vue.d.ts.map +0 -1
  61. package/dist/src/components/fws/UserProfileStrict.vue.d.ts +0 -12
  62. package/dist/src/components/fws/UserProfileStrict.vue.d.ts.map +0 -1
  63. package/dist/src/components/ssr/ClientOnly.d.ts +0 -4
  64. package/dist/src/components/ssr/ClientOnly.d.ts.map +0 -1
  65. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts +0 -11
  66. package/dist/src/components/ui/DefaultBreadcrumb.vue.d.ts.map +0 -1
  67. package/dist/src/components/ui/DefaultConfirm.vue.d.ts +0 -81
  68. package/dist/src/components/ui/DefaultConfirm.vue.d.ts.map +0 -1
  69. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts +0 -81
  70. package/dist/src/components/ui/DefaultConfirmWithInput.vue.d.ts.map +0 -1
  71. package/dist/src/components/ui/DefaultDropdown.vue.d.ts +0 -35
  72. package/dist/src/components/ui/DefaultDropdown.vue.d.ts.map +0 -1
  73. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts +0 -23
  74. package/dist/src/components/ui/DefaultDropdownLink.vue.d.ts.map +0 -1
  75. package/dist/src/components/ui/DefaultGallery.vue.d.ts +0 -114
  76. package/dist/src/components/ui/DefaultGallery.vue.d.ts.map +0 -1
  77. package/dist/src/components/ui/DefaultInput.vue.d.ts +0 -61
  78. package/dist/src/components/ui/DefaultInput.vue.d.ts.map +0 -1
  79. package/dist/src/components/ui/DefaultLoader.vue.d.ts +0 -12
  80. package/dist/src/components/ui/DefaultLoader.vue.d.ts.map +0 -1
  81. package/dist/src/components/ui/DefaultModal.vue.d.ts +0 -36
  82. package/dist/src/components/ui/DefaultModal.vue.d.ts.map +0 -1
  83. package/dist/src/components/ui/DefaultNotif.vue.d.ts +0 -3
  84. package/dist/src/components/ui/DefaultNotif.vue.d.ts.map +0 -1
  85. package/dist/src/components/ui/DefaultPaging.vue.d.ts +0 -13
  86. package/dist/src/components/ui/DefaultPaging.vue.d.ts.map +0 -1
  87. package/dist/src/components/ui/DefaultSidebar.vue.d.ts +0 -29
  88. package/dist/src/components/ui/DefaultSidebar.vue.d.ts.map +0 -1
  89. package/dist/src/components/ui/DefaultTagInput.vue.d.ts +0 -34
  90. package/dist/src/components/ui/DefaultTagInput.vue.d.ts.map +0 -1
  91. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts +0 -18
  92. package/dist/src/components/ui/transitions/CollapseTransition.vue.d.ts.map +0 -1
  93. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts +0 -18
  94. package/dist/src/components/ui/transitions/ExpandTransition.vue.d.ts.map +0 -1
  95. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts +0 -18
  96. package/dist/src/components/ui/transitions/FadeTransition.vue.d.ts.map +0 -1
  97. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts +0 -18
  98. package/dist/src/components/ui/transitions/ScaleTransition.vue.d.ts.map +0 -1
  99. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts +0 -21
  100. package/dist/src/components/ui/transitions/SlideTransition.vue.d.ts.map +0 -1
  101. package/dist/src/composables/event-bus.d.ts +0 -8
  102. package/dist/src/composables/event-bus.d.ts.map +0 -1
  103. package/dist/src/composables/rest.d.ts +0 -24
  104. package/dist/src/composables/rest.d.ts.map +0 -1
  105. package/dist/src/composables/seo.d.ts +0 -26
  106. package/dist/src/composables/seo.d.ts.map +0 -1
  107. package/dist/src/composables/ssr.d.ts +0 -24
  108. package/dist/src/composables/ssr.d.ts.map +0 -1
  109. package/dist/src/composables/templating.d.ts +0 -7
  110. package/dist/src/composables/templating.d.ts.map +0 -1
  111. package/dist/src/composables/translations.d.ts +0 -8
  112. package/dist/src/composables/translations.d.ts.map +0 -1
  113. package/dist/src/index.d.ts.map +0 -1
  114. package/dist/src/plugin.d.ts +0 -3
  115. package/dist/src/plugin.d.ts.map +0 -1
  116. package/dist/src/stores/serverRouter.d.ts +0 -34
  117. package/dist/src/stores/serverRouter.d.ts.map +0 -1
  118. package/dist/src/stores/user.d.ts +0 -139
  119. package/dist/src/stores/user.d.ts.map +0 -1
  120. package/dist/src/types.d.ts +0 -48
  121. package/dist/src/types.d.ts.map +0 -1
@@ -0,0 +1,768 @@
1
+ <script setup lang="ts">
2
+ import { useDebounceFn } from '@vueuse/core'
3
+ import { computed, shallowRef, toRef } from 'vue'
4
+ import DefaultTagInput from './DefaultTagInput.vue'
5
+
6
+ type modelValueType = string | number | string[] | number[] | Record<string, any> | undefined
7
+ type checkboxValueType = any[] | Set<any> | undefined | boolean
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ id: string
12
+ showLabel?: boolean
13
+ label?: string
14
+ type?: string
15
+ placeholder?: string
16
+ autocomplete?: string
17
+ checkboxTrueValue?: string | boolean
18
+ checkboxFalseValue?: string | boolean
19
+ req?: boolean
20
+ modelValue?: modelValueType
21
+ checkboxValue?: checkboxValueType
22
+ options?: string[][]
23
+ dpOptions?: Record<string, any>
24
+ help?: string
25
+ error?: string
26
+ color?: string
27
+ disabled?: boolean
28
+ maxLengthPerTag?: number
29
+ copyButton?: boolean
30
+ maxRange?: number
31
+ minRange?: number
32
+ maxTags?: number
33
+ }>(),
34
+ {
35
+ showLabel: true,
36
+ type: 'text',
37
+ req: false,
38
+ options: () => [],
39
+ checkboxTrueValue: true,
40
+ checkboxFalseValue: false,
41
+ disabled: false,
42
+ maxLengthPerTag: 0,
43
+ maxRange: 100,
44
+ minRange: 0,
45
+ copyButton: false,
46
+ dpOptions: () => ({}),
47
+ },
48
+ )
49
+
50
+ const inputRef = shallowRef<HTMLInputElement>()
51
+ const errorProp = toRef(props, 'error')
52
+
53
+ const checkErrors = computed(() => errorProp.value || null)
54
+
55
+ function focus() { inputRef.value?.focus() }
56
+ function blur() { inputRef.value?.blur() }
57
+ function getInputRef() { return inputRef.value }
58
+
59
+ const emit = defineEmits([
60
+ 'update:modelValue',
61
+ 'update:checkboxValue',
62
+ 'focus',
63
+ 'blur',
64
+ ])
65
+
66
+ const handleFocus = useDebounceFn(() => { emit('focus', props.id) }, 50)
67
+ const handleBlur = useDebounceFn(() => { emit('blur', props.id) }, 50)
68
+
69
+ const copyToClipboard = useDebounceFn(() => {
70
+ if (props.modelValue) navigator.clipboard.writeText(props.modelValue.toString())
71
+ }, 200)
72
+
73
+ const model = computed<modelValueType>({
74
+ get: () => props.modelValue,
75
+ set: (v) => { emit('update:modelValue', v) },
76
+ })
77
+
78
+ const modelCheckbox = computed<checkboxValueType>({
79
+ get: () => props.checkboxValue,
80
+ set: (v) => { emit('update:checkboxValue', v) },
81
+ })
82
+
83
+ const rangePercent = computed(() => {
84
+ const val = (model.value as number || props.minRange) - props.minRange
85
+ return (val / (props.maxRange - props.minRange)) * 100
86
+ })
87
+
88
+ defineExpose({ focus, blur, getInputRef })
89
+ </script>
90
+
91
+ <template>
92
+ <div class="fv-input-wrap">
93
+ <!-- TEXT, PASSWORD, EMAIL, SEARCH, DATE, DATETIME, URL, TEXTAREA, SELECT, PHONE, TEL, RANGE, CHIPS, TAGS -->
94
+ <template
95
+ v-if="[
96
+ 'text', 'password', 'email', 'search', 'date', 'datetime', 'url',
97
+ 'textarea', 'textarea-grow', 'select', 'phone', 'tel', 'range',
98
+ 'chips', 'tags',
99
+ ].includes(type)"
100
+ >
101
+ <div class="fv-input-group">
102
+ <!-- Standard input types -->
103
+ <div
104
+ v-if="[
105
+ 'text', 'phone', 'tel', 'password', 'range', 'email',
106
+ 'search', 'url', 'date', 'datetime',
107
+ ].includes(type)"
108
+ >
109
+ <label
110
+ v-if="showLabel && label"
111
+ :for="id"
112
+ :class="['fv-input__label', { 'fv-input__label--error': checkErrors }]"
113
+ >
114
+ {{ label }}
115
+ <template v-if="type === 'range'">({{ model }})</template>
116
+ </label>
117
+
118
+ <div class="fv-input__field-wrap">
119
+ <input
120
+ :id="id"
121
+ ref="inputRef"
122
+ v-model="model"
123
+ :type="type === 'phone' ? 'tel' : type"
124
+ :name="id"
125
+ :class="['fv-input__field', { 'fv-input__field--error': checkErrors, 'fv-input__field--range': type === 'range', 'fv-input__field--copy': copyButton }]"
126
+ :autocomplete="autocomplete"
127
+ :min="type === 'range' ? minRange : undefined"
128
+ :max="type === 'range' ? maxRange : undefined"
129
+ :placeholder="placeholder"
130
+ :disabled="disabled"
131
+ :aria-describedby="help ? `${id}-help` : undefined"
132
+ :required="req"
133
+ :aria-invalid="checkErrors ? 'true' : 'false'"
134
+ @focus="handleFocus"
135
+ @blur="handleBlur"
136
+ >
137
+ <button
138
+ v-if="copyButton && model"
139
+ type="button"
140
+ aria-label="Copy to clipboard"
141
+ class="fv-input__copy-btn"
142
+ @click="copyToClipboard"
143
+ >
144
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
145
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
146
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
147
+ </svg>
148
+ </button>
149
+ </div>
150
+
151
+ <!-- Range ticks -->
152
+ <template v-if="type === 'range'">
153
+ <div class="fv-input__range-meta">
154
+ <div class="fv-input__range-labels">
155
+ <span>{{ minRange }}</span>
156
+ <span>{{ ((maxRange - minRange) / 3 + minRange).toFixed(0) }}</span>
157
+ <span>{{ (((maxRange - minRange) / 3) * 2 + minRange).toFixed(0) }}</span>
158
+ <span>{{ maxRange }}</span>
159
+ </div>
160
+ <div class="fv-input__range-track">
161
+ <div class="fv-input__range-fill" :style="{ width: `${rangePercent}%` }" />
162
+ </div>
163
+ </div>
164
+ </template>
165
+ </div>
166
+
167
+ <!-- CHIPS / TAGS -->
168
+ <div v-if="type === 'chips' || type === 'tags'">
169
+ <label
170
+ v-if="showLabel && (label || placeholder)"
171
+ :for="id"
172
+ class="fv-input__label"
173
+ >
174
+ {{ label || placeholder }}
175
+ </label>
176
+ <!-- @vue-skip -->
177
+ <DefaultTagInput
178
+ :id="id"
179
+ v-model="model"
180
+ :disabled="disabled"
181
+ :color="color"
182
+ :error="checkErrors"
183
+ :max-tags="maxTags"
184
+ :copy-button="copyButton"
185
+ :help="help"
186
+ :max-lenght-per-tag="maxLengthPerTag"
187
+ />
188
+ </div>
189
+
190
+ <!-- TEXTAREA AUTO-GROW -->
191
+ <div v-else-if="type === 'textarea-grow'">
192
+ <label v-if="showLabel && label" :for="id" class="fv-input__label">{{ label }}</label>
193
+ <div class="fv-input__grow-wrap" :data-replicated-value="model?.toString() || ''">
194
+ <!-- @vue-skip -->
195
+ <textarea
196
+ :id="id"
197
+ ref="inputRef"
198
+ v-model="model"
199
+ :name="id"
200
+ :class="['fv-input__textarea', { 'fv-input__field--error': checkErrors }]"
201
+ :placeholder="placeholder"
202
+ :disabled="disabled"
203
+ :aria-describedby="help ? `${id}-help` : undefined"
204
+ :required="req"
205
+ :aria-invalid="checkErrors ? 'true' : 'false'"
206
+ @focus="handleFocus"
207
+ @blur="handleBlur"
208
+ />
209
+ </div>
210
+ <div
211
+ v-if="dpOptions.counterMax && model"
212
+ :class="['fv-input__counter', { 'fv-input__counter--over': model.toString().length > dpOptions.counterMax }]"
213
+ >
214
+ {{ model.toString().length }} / {{ dpOptions.counterMax }}
215
+ </div>
216
+ </div>
217
+
218
+ <!-- TEXTAREA REGULAR -->
219
+ <div v-else-if="type === 'textarea'">
220
+ <label v-if="showLabel && label" :for="id" class="fv-input__label">{{ label }}</label>
221
+ <!-- @vue-skip -->
222
+ <textarea
223
+ :id="id"
224
+ ref="inputRef"
225
+ v-model="model"
226
+ :name="id"
227
+ :class="['fv-input__textarea fv-input__textarea--fixed', { 'fv-input__field--error': checkErrors }]"
228
+ :placeholder="placeholder"
229
+ :disabled="disabled"
230
+ :aria-describedby="help ? `${id}-help` : undefined"
231
+ :required="req"
232
+ :aria-invalid="checkErrors ? 'true' : 'false'"
233
+ @focus="handleFocus"
234
+ @blur="handleBlur"
235
+ />
236
+ <div
237
+ v-if="dpOptions.counterMax && model"
238
+ :class="['fv-input__counter', { 'fv-input__counter--over': model.toString().length > dpOptions.counterMax }]"
239
+ >
240
+ {{ model.toString().length }} / {{ dpOptions.counterMax }}
241
+ </div>
242
+ </div>
243
+
244
+ <!-- SELECT -->
245
+ <div v-else-if="type === 'select'">
246
+ <label v-if="showLabel && label" :for="id" class="fv-input__label">{{ label }}</label>
247
+ <select
248
+ :id="id"
249
+ ref="inputRef"
250
+ v-model="model"
251
+ :name="id"
252
+ :disabled="disabled"
253
+ :aria-describedby="help ? `${id}-help` : undefined"
254
+ :required="req"
255
+ :class="['fv-input__select', { 'fv-input__field--error': checkErrors }]"
256
+ :aria-invalid="checkErrors ? 'true' : 'false'"
257
+ @focus="handleFocus"
258
+ @blur="handleBlur"
259
+ >
260
+ <option
261
+ v-for="opt in (options || [])"
262
+ :key="opt[0]?.toString()"
263
+ :value="opt[0]"
264
+ >
265
+ {{ opt[1] }}
266
+ </option>
267
+ </select>
268
+ </div>
269
+ </div>
270
+ </template>
271
+
272
+ <!-- TOGGLE -->
273
+ <template v-else-if="type === 'toggle'">
274
+ <label :class="['fv-toggle', { 'fv-toggle--disabled': disabled }]">
275
+ <input
276
+ v-model="modelCheckbox"
277
+ type="checkbox"
278
+ :true-value="checkboxTrueValue"
279
+ :false-value="checkboxFalseValue"
280
+ :disabled="disabled"
281
+ class="fv-toggle__input"
282
+ :aria-invalid="checkErrors ? 'true' : 'false'"
283
+ :aria-describedby="help ? `${id}-help` : undefined"
284
+ :aria-checked="modelCheckbox ? 'true' : 'false'"
285
+ role="switch"
286
+ @focus="handleFocus"
287
+ @blur="handleBlur"
288
+ >
289
+ <div class="fv-toggle__track" />
290
+ <span class="fv-toggle__label">
291
+ {{ label }}
292
+ <p v-if="help" :id="`${id}-help`" class="fv-toggle__help">{{ help }}</p>
293
+ </span>
294
+ </label>
295
+ </template>
296
+
297
+ <!-- CHECKBOX / RADIO -->
298
+ <template v-else-if="type === 'checkbox' || type === 'radio'">
299
+ <div :class="['fv-check', { 'fv-check--disabled': disabled }]">
300
+ <div class="fv-check__box">
301
+ <input
302
+ :id="id"
303
+ ref="inputRef"
304
+ v-model="modelCheckbox"
305
+ :class="['fv-check__input', { 'fv-input__field--error': checkErrors }]"
306
+ :aria-describedby="help ? `${id}-help` : undefined"
307
+ :type="type"
308
+ :true-value="checkboxTrueValue"
309
+ :false-value="checkboxFalseValue"
310
+ :disabled="disabled"
311
+ :aria-invalid="checkErrors ? 'true' : 'false'"
312
+ @focus="handleFocus"
313
+ @blur="handleBlur"
314
+ >
315
+ </div>
316
+ <div class="fv-check__content">
317
+ <label :for="id" class="fv-check__label">{{ label }}</label>
318
+ <p v-if="help" :id="`${id}-help`" class="fv-check__help">{{ help }}</p>
319
+ </div>
320
+ </div>
321
+ </template>
322
+
323
+ <!-- Error message -->
324
+ <p v-if="checkErrors" class="fv-input__error" role="alert" aria-live="assertive">
325
+ <svg class="fv-input__error-icon" fill="currentColor" viewBox="0 0 20 20">
326
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
327
+ </svg>
328
+ {{ checkErrors }}
329
+ </p>
330
+
331
+ <!-- Help text -->
332
+ <p
333
+ v-if="help && !['checkbox', 'radio', 'toggle'].includes(type)"
334
+ :id="`${id}-help`"
335
+ class="fv-input__help"
336
+ >
337
+ {{ help }}
338
+ </p>
339
+ </div>
340
+ </template>
341
+
342
+ <style scoped>
343
+ /* ═══ Input system — Modern focus rings, clean labels ═══ */
344
+ .fv-input-wrap { width: 100%; }
345
+ .fv-input-group { display: flex; flex-direction: column; gap: 0.125rem; }
346
+
347
+ /* Label */
348
+ .fv-input__label {
349
+ display: block;
350
+ margin-bottom: 0.375rem;
351
+ font-size: 0.8125rem;
352
+ font-weight: 500;
353
+ color: #171717;
354
+ }
355
+ :is(.dark) .fv-input__label { color: #f5f5f5; }
356
+ /* Superhuman/Cursor warm crimson for errors — not cold red */
357
+ .fv-input__label--error { color: #cf2d56; }
358
+ :is(.dark) .fv-input__label--error { color: #FF6363; }
359
+
360
+ /* Field wrapper */
361
+ .fv-input__field-wrap { position: relative; }
362
+
363
+ /* Standard input field — Vercel shadow-as-border + Stripe focus ring */
364
+ .fv-input__field {
365
+ display: block;
366
+ width: 100%;
367
+ padding: 0.5rem 0.75rem;
368
+ font-size: 0.875rem;
369
+ line-height: 1.5;
370
+ border-radius: 6px;
371
+ border: none;
372
+ background: #ffffff;
373
+ color: #171717;
374
+ transition: box-shadow 200ms;
375
+ outline: none;
376
+
377
+ /* Vercel: shadow-as-border — smoother than CSS border at radius edges */
378
+ box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
379
+ }
380
+ .fv-input__field::placeholder { color: #a3a3a3; opacity: 0.8; }
381
+
382
+ /* Vercel/Stripe dual-mechanism focus: outline (visible) + shadow (glow) */
383
+ .fv-input__field:focus {
384
+ box-shadow:
385
+ 0 0 0 1px var(--color-fv-primary-500, #7c3aed),
386
+ 0 0 0 4px rgba(124, 58, 237, 0.1);
387
+ }
388
+
389
+ :is(.dark) .fv-input__field {
390
+ background: rgba(255, 255, 255, 0.02);
391
+ color: #f5f5f5;
392
+ box-shadow: rgba(255, 255, 255, 0.08) 0px 0px 0px 1px;
393
+ color-scheme: dark;
394
+ }
395
+ :is(.dark) .fv-input__field::placeholder { color: #6a6b6c; }
396
+ :is(.dark) .fv-input__field:focus {
397
+ box-shadow:
398
+ 0 0 0 1px var(--color-fv-primary-400, #a78bfa),
399
+ 0 0 0 4px rgba(167, 139, 250, 0.15);
400
+ }
401
+
402
+ /* Raycast two-layer error: border shift + translucent tint */
403
+ .fv-input__field--error {
404
+ box-shadow: 0 0 0 1px #FF6363 !important;
405
+ background: hsla(0, 100%, 69%, 0.04) !important;
406
+ }
407
+ .fv-input__field--error:focus {
408
+ box-shadow:
409
+ 0 0 0 1px #FF6363,
410
+ 0 0 0 4px hsla(0, 100%, 69%, 0.12) !important;
411
+ }
412
+
413
+ .fv-input__field--copy { padding-right: 2.5rem; }
414
+
415
+ /* Range input */
416
+ .fv-input__field--range {
417
+ background: transparent !important;
418
+ border: none !important;
419
+ box-shadow: none !important;
420
+ padding: 0;
421
+ -webkit-appearance: none;
422
+ appearance: none;
423
+ height: 0.75rem;
424
+ cursor: pointer;
425
+ }
426
+
427
+ /* Copy button */
428
+ .fv-input__copy-btn {
429
+ position: absolute;
430
+ top: 50%;
431
+ right: 0.5rem;
432
+ transform: translateY(-50%);
433
+ padding: 0.25rem;
434
+ color: #737373;
435
+ background: none;
436
+ border: none;
437
+ cursor: pointer;
438
+ border-radius: 0.25rem;
439
+ transition: color 150ms;
440
+ }
441
+ .fv-input__copy-btn:hover { color: var(--color-fv-primary-600, #7c3aed); }
442
+ :is(.dark) .fv-input__copy-btn { color: #a3a3a3; }
443
+ :is(.dark) .fv-input__copy-btn:hover { color: var(--color-fv-primary-400, #a78bfa); }
444
+
445
+ /* Range meta */
446
+ .fv-input__range-meta { margin-top: 0.5rem; }
447
+ .fv-input__range-labels {
448
+ display: flex;
449
+ justify-content: space-between;
450
+ font-size: 0.6875rem;
451
+ color: #737373;
452
+ padding: 0 0.125rem;
453
+ }
454
+ :is(.dark) .fv-input__range-labels { color: #a3a3a3; }
455
+
456
+ .fv-input__range-track {
457
+ width: 100%;
458
+ height: 0.25rem;
459
+ background: #e5e5e5;
460
+ border-radius: 9999px;
461
+ margin-top: 0.25rem;
462
+ overflow: hidden;
463
+ }
464
+ :is(.dark) .fv-input__range-track { background: rgba(255, 255, 255, 0.1); }
465
+
466
+ .fv-input__range-fill {
467
+ height: 100%;
468
+ background: var(--color-fv-primary-500, #7c3aed);
469
+ border-radius: 9999px;
470
+ transition: width 100ms;
471
+ }
472
+ :is(.dark) .fv-input__range-fill { background: var(--color-fv-primary-400, #a78bfa); }
473
+
474
+ /* Textarea — same shadow-as-border system */
475
+ .fv-input__textarea {
476
+ display: block;
477
+ width: 100%;
478
+ padding: 0.5rem 0.75rem;
479
+ font-size: 0.875rem;
480
+ line-height: 1.5;
481
+ border-radius: 6px;
482
+ border: none;
483
+ background: #ffffff;
484
+ color: #171717;
485
+ transition: box-shadow 200ms;
486
+ outline: none;
487
+ resize: vertical;
488
+ box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
489
+ }
490
+ .fv-input__textarea:focus {
491
+ box-shadow:
492
+ 0 0 0 1px var(--color-fv-primary-500, #7c3aed),
493
+ 0 0 0 4px rgba(124, 58, 237, 0.1);
494
+ }
495
+ :is(.dark) .fv-input__textarea {
496
+ background: rgba(255, 255, 255, 0.02);
497
+ color: #f5f5f5;
498
+ box-shadow: rgba(255, 255, 255, 0.08) 0px 0px 0px 1px;
499
+ }
500
+ :is(.dark) .fv-input__textarea:focus {
501
+ box-shadow:
502
+ 0 0 0 1px var(--color-fv-primary-400, #a78bfa),
503
+ 0 0 0 4px rgba(167, 139, 250, 0.15);
504
+ }
505
+ .fv-input__textarea--fixed {
506
+ min-height: 100px;
507
+ max-height: 50vh;
508
+ overflow-y: auto;
509
+ }
510
+
511
+ /* Auto-grow textarea */
512
+ .fv-input__grow-wrap { display: grid; }
513
+ .fv-input__grow-wrap::after {
514
+ content: attr(data-replicated-value) " ";
515
+ white-space: pre-wrap;
516
+ visibility: hidden;
517
+ max-height: 50vh;
518
+ overflow: hidden;
519
+ grid-area: 1 / 1 / 2 / 2;
520
+ padding: 0.5rem 0.75rem;
521
+ font-size: 0.875rem;
522
+ line-height: 1.5;
523
+ border: 1px solid transparent;
524
+ }
525
+ .fv-input__grow-wrap > textarea {
526
+ resize: none;
527
+ overflow-y: auto;
528
+ max-height: 50vh;
529
+ grid-area: 1 / 1 / 2 / 2;
530
+ }
531
+
532
+ /* Counter */
533
+ .fv-input__counter {
534
+ margin-top: 0.25rem;
535
+ font-size: 0.75rem;
536
+ color: #737373;
537
+ text-align: right;
538
+ }
539
+ :is(.dark) .fv-input__counter { color: #a3a3a3; }
540
+ .fv-input__counter--over { color: #ef4444; }
541
+ :is(.dark) .fv-input__counter--over { color: #f87171; }
542
+
543
+ /* Select — Notion whisper-border light, shadow-as-border system */
544
+ .fv-input__select {
545
+ display: block;
546
+ width: 100%;
547
+ padding: 0.5rem 2.5rem 0.5rem 0.75rem;
548
+ font-size: 0.875rem;
549
+ line-height: 1.5;
550
+ border-radius: 6px;
551
+ border: none;
552
+ background: #ffffff;
553
+ color: #171717;
554
+ -webkit-appearance: none;
555
+ appearance: none;
556
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
557
+ background-position: right 0.5rem center;
558
+ background-repeat: no-repeat;
559
+ background-size: 1.25rem;
560
+ transition: box-shadow 200ms;
561
+ outline: none;
562
+ cursor: pointer;
563
+ box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
564
+ }
565
+ .fv-input__select:focus {
566
+ box-shadow:
567
+ 0 0 0 1px var(--color-fv-primary-500, #7c3aed),
568
+ 0 0 0 4px rgba(124, 58, 237, 0.1);
569
+ }
570
+ :is(.dark) .fv-input__select {
571
+ background-color: rgba(255, 255, 255, 0.02);
572
+ color: #f5f5f5;
573
+ box-shadow: rgba(255, 255, 255, 0.08) 0px 0px 0px 1px;
574
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
575
+ /* Force dark native dropdown popup (Chrome renders white otherwise) */
576
+ color-scheme: dark;
577
+ }
578
+ :is(.dark) .fv-input__select option {
579
+ background: #1a1a1e;
580
+ color: #f5f5f5;
581
+ }
582
+ :is(.dark) .fv-input__select:focus {
583
+ box-shadow:
584
+ 0 0 0 1px var(--color-fv-primary-400, #a78bfa),
585
+ 0 0 0 4px rgba(167, 139, 250, 0.15);
586
+ }
587
+
588
+ /* ═══ Toggle — Modern iOS-style switch ═══ */
589
+ .fv-toggle {
590
+ display: inline-flex;
591
+ align-items: center;
592
+ cursor: pointer;
593
+ gap: 0.75rem;
594
+ margin-bottom: 0.5rem;
595
+ }
596
+ .fv-toggle--disabled { opacity: 0.6; cursor: not-allowed; }
597
+
598
+ .fv-toggle__input { position: absolute; width: 0; height: 0; opacity: 0; }
599
+
600
+ .fv-toggle__track {
601
+ position: relative;
602
+ flex-shrink: 0;
603
+ width: 2.75rem;
604
+ height: 1.5rem;
605
+ border-radius: 9999px;
606
+ background: #d4d4d8;
607
+ transition: background-color 200ms;
608
+ }
609
+ .fv-toggle__track::after {
610
+ content: '';
611
+ position: absolute;
612
+ top: 2px;
613
+ left: 2px;
614
+ width: 1.25rem;
615
+ height: 1.25rem;
616
+ border-radius: 50%;
617
+ background: white;
618
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
619
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
620
+ }
621
+ :is(.dark) .fv-toggle__track { background: rgba(255, 255, 255, 0.1); }
622
+
623
+ .fv-toggle__input:checked + .fv-toggle__track {
624
+ background: var(--color-fv-primary-600, #7c3aed);
625
+ }
626
+ .fv-toggle__input:checked + .fv-toggle__track::after {
627
+ transform: translateX(1.25rem);
628
+ }
629
+ .fv-toggle__input:focus-visible + .fv-toggle__track {
630
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
631
+ }
632
+
633
+ .fv-toggle__label {
634
+ font-size: 0.875rem;
635
+ font-weight: 500;
636
+ color: #171717;
637
+ }
638
+ :is(.dark) .fv-toggle__label { color: #d4d4d4; }
639
+
640
+ .fv-toggle__help {
641
+ font-size: 0.75rem;
642
+ color: #737373;
643
+ margin-top: 0.125rem;
644
+ }
645
+ :is(.dark) .fv-toggle__help { color: #a3a3a3; }
646
+
647
+ /* ═══ Checkbox / Radio ═══ */
648
+ .fv-check {
649
+ display: flex;
650
+ margin-bottom: 0.75rem;
651
+ gap: 0.5rem;
652
+ }
653
+ .fv-check--disabled { opacity: 0.6; cursor: not-allowed; }
654
+
655
+ .fv-check__box {
656
+ display: flex;
657
+ align-items: center;
658
+ padding-top: 0.125rem;
659
+ }
660
+ .fv-check__input {
661
+ width: 1rem;
662
+ height: 1rem;
663
+ border-radius: 0.25rem;
664
+ border: 1.5px solid #d4d4d8;
665
+ background: #fafafa;
666
+ color: var(--color-fv-primary-600, #7c3aed);
667
+ cursor: pointer;
668
+ transition: border-color 150ms, box-shadow 150ms;
669
+ }
670
+ .fv-check__input:focus {
671
+ box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15);
672
+ }
673
+ :is(.dark) .fv-check__input {
674
+ background: rgba(38, 38, 42, 0.8);
675
+ border-color: rgba(255, 255, 255, 0.15);
676
+ }
677
+ .fv-check__input[type="radio"] { border-radius: 50%; }
678
+
679
+ .fv-check__label {
680
+ font-size: 0.875rem;
681
+ font-weight: 500;
682
+ color: #171717;
683
+ cursor: pointer;
684
+ }
685
+ :is(.dark) .fv-check__label { color: #d4d4d4; }
686
+ .fv-check--disabled .fv-check__label { cursor: not-allowed; }
687
+
688
+ .fv-check__help {
689
+ font-size: 0.75rem;
690
+ color: #737373;
691
+ margin-top: 0.125rem;
692
+ }
693
+ :is(.dark) .fv-check__help { color: #a3a3a3; }
694
+
695
+ /* ═══ Error & Help ═══ */
696
+ .fv-input__error {
697
+ display: flex;
698
+ align-items: center;
699
+ margin-top: 0.25rem;
700
+ font-size: 0.8125rem;
701
+ color: #dc2626;
702
+ }
703
+ :is(.dark) .fv-input__error { color: #f87171; }
704
+
705
+ .fv-input__error-icon {
706
+ width: 0.875rem;
707
+ height: 0.875rem;
708
+ margin-right: 0.375rem;
709
+ flex-shrink: 0;
710
+ }
711
+
712
+ .fv-input__help {
713
+ margin-top: 0.25rem;
714
+ font-size: 0.75rem;
715
+ color: #737373;
716
+ }
717
+ :is(.dark) .fv-input__help { color: #a3a3a3; }
718
+
719
+ /* ═══ Range slider thumb ═══ */
720
+ input[type="range"]::-webkit-slider-thumb {
721
+ -webkit-appearance: none;
722
+ width: 1rem;
723
+ height: 1rem;
724
+ border-radius: 50%;
725
+ background: var(--color-fv-primary-500, #7c3aed);
726
+ cursor: pointer;
727
+ border: none;
728
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
729
+ margin-top: -0.375rem;
730
+ }
731
+ input[type="range"]::-moz-range-thumb {
732
+ width: 1rem;
733
+ height: 1rem;
734
+ border-radius: 50%;
735
+ background: var(--color-fv-primary-500, #7c3aed);
736
+ cursor: pointer;
737
+ border: none;
738
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
739
+ }
740
+ input[type="range"]::-webkit-slider-runnable-track {
741
+ height: 0.375rem;
742
+ border-radius: 9999px;
743
+ background: #e5e5e5;
744
+ }
745
+ :is(.dark) input[type="range"]::-webkit-slider-runnable-track {
746
+ background: rgba(255, 255, 255, 0.1);
747
+ }
748
+ input[type="range"]::-moz-range-track {
749
+ height: 0.375rem;
750
+ border-radius: 9999px;
751
+ background: #e5e5e5;
752
+ }
753
+ :is(.dark) input[type="range"]::-moz-range-track {
754
+ background: rgba(255, 255, 255, 0.1);
755
+ }
756
+
757
+ /* Disabled state */
758
+ .fv-input__field:disabled,
759
+ .fv-input__textarea:disabled,
760
+ .fv-input__select:disabled {
761
+ opacity: 0.6;
762
+ cursor: not-allowed;
763
+ }
764
+
765
+ @media (prefers-reduced-motion: reduce) {
766
+ .fv-toggle__track::after { transition: none; }
767
+ }
768
+ </style>