@cfasim-ui/components 0.7.7 → 0.8.0

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 (89) hide show
  1. package/dist/Box/Box.d.ts +7 -11
  2. package/dist/Button/Button.d.ts +7 -11
  3. package/dist/ButtonGroup/ButtonGroup.d.ts +8 -12
  4. package/dist/Container/Container.d.ts +7 -11
  5. package/dist/Expander/Expander.d.ts +12 -14
  6. package/dist/Grid/Grid.d.ts +6 -5
  7. package/dist/Hint/Hint.d.ts +2 -1
  8. package/dist/Icon/Icon.d.ts +3 -2
  9. package/dist/LightDarkToggle/LightDarkToggle.d.ts +2 -1
  10. package/dist/MultiSelect/MultiSelect.d.ts +6 -6
  11. package/dist/NumberInput/NumberInput.d.ts +13 -11
  12. package/dist/ParamEditor/ParamEditor.d.ts +3 -2
  13. package/dist/ParamEditor/ParamEditorImpl.d.ts +3 -5
  14. package/dist/{ParamEditorImpl-D0xZTyAN.js → ParamEditorImpl-CkElC6PI.js} +3075 -3041
  15. package/dist/SelectBox/SelectBox.d.ts +8 -6
  16. package/dist/{SelectBox-CByXZfaC.js → SelectBox-Df8dE2r-.js} +6 -6
  17. package/dist/SidebarLayout/SidebarLayout.d.ts +21 -20
  18. package/dist/Spinner/Spinner.d.ts +3 -2
  19. package/dist/TextInput/TextInput.d.ts +7 -5
  20. package/dist/Toggle/Toggle.d.ts +8 -6
  21. package/dist/ToggleGroup/ToggleGroup.d.ts +8 -6
  22. package/dist/_internal/FieldLabel.d.ts +2 -1
  23. package/dist/index.js +20 -20
  24. package/docs/Box.md +49 -0
  25. package/docs/Button.md +67 -0
  26. package/docs/ButtonGroup.md +64 -0
  27. package/docs/Container.md +103 -0
  28. package/docs/Expander.md +34 -0
  29. package/docs/Grid.md +170 -0
  30. package/docs/Hint.md +29 -0
  31. package/docs/Icon.md +188 -0
  32. package/docs/MultiSelect.md +143 -0
  33. package/docs/NumberInput.md +485 -0
  34. package/docs/ParamEditor.md +97 -0
  35. package/docs/SelectBox.md +182 -0
  36. package/docs/SidebarLayout.md +106 -0
  37. package/docs/Spinner.md +51 -0
  38. package/docs/TextInput.md +83 -0
  39. package/docs/Toggle.md +81 -0
  40. package/docs/ToggleGroup.md +163 -0
  41. package/docs/index.json +194 -0
  42. package/package.json +24 -20
  43. package/src/Box/Box.md +41 -0
  44. package/src/Box/Box.vue +52 -0
  45. package/src/Button/Button.md +59 -0
  46. package/src/Button/Button.vue +81 -0
  47. package/src/ButtonGroup/ButtonGroup.md +62 -0
  48. package/src/ButtonGroup/ButtonGroup.vue +91 -0
  49. package/src/Container/Container.md +99 -0
  50. package/src/Container/Container.vue +62 -0
  51. package/src/Expander/Expander.md +23 -0
  52. package/src/Expander/Expander.vue +95 -0
  53. package/src/Grid/Grid.md +175 -0
  54. package/src/Grid/Grid.vue +145 -0
  55. package/src/Hint/Hint.md +24 -0
  56. package/src/Hint/Hint.vue +83 -0
  57. package/src/Icon/Icon.md +176 -0
  58. package/src/Icon/Icon.vue +104 -0
  59. package/src/Icon/defaultIcons.ts +92 -0
  60. package/src/Icon/github.svg +1 -0
  61. package/src/Icon/registry.ts +68 -0
  62. package/src/LightDarkToggle/LightDarkToggle.vue +49 -0
  63. package/src/MultiSelect/MultiSelect.md +142 -0
  64. package/src/MultiSelect/MultiSelect.vue +279 -0
  65. package/src/NumberInput/NumberInput.md +455 -0
  66. package/src/NumberInput/NumberInput.vue +575 -0
  67. package/src/ParamEditor/ParamEditor.md +87 -0
  68. package/src/ParamEditor/ParamEditor.vue +43 -0
  69. package/src/ParamEditor/ParamEditorImpl.vue +358 -0
  70. package/src/SelectBox/SelectBox.md +169 -0
  71. package/src/SelectBox/SelectBox.vue +260 -0
  72. package/src/SidebarLayout/SidebarLayout.md +106 -0
  73. package/src/SidebarLayout/SidebarLayout.vue +468 -0
  74. package/src/Spinner/Spinner.md +45 -0
  75. package/src/Spinner/Spinner.vue +55 -0
  76. package/src/TextInput/TextInput.md +68 -0
  77. package/src/TextInput/TextInput.vue +54 -0
  78. package/src/Toggle/Toggle.md +68 -0
  79. package/src/Toggle/Toggle.vue +81 -0
  80. package/src/ToggleGroup/ToggleGroup.md +158 -0
  81. package/src/ToggleGroup/ToggleGroup.vue +138 -0
  82. package/src/_internal/FieldLabel.vue +27 -0
  83. package/src/_internal/field.ts +27 -0
  84. package/src/_internal/gap.ts +17 -0
  85. package/src/_internal/input.css +54 -0
  86. package/src/_internal/listbox.css +53 -0
  87. package/src/env.d.ts +4 -0
  88. package/src/index.ts +33 -0
  89. package/src/svg.d.ts +5 -0
@@ -0,0 +1,468 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ref,
4
+ computed,
5
+ watch,
6
+ onMounted,
7
+ onUnmounted,
8
+ getCurrentInstance,
9
+ } from "vue";
10
+ import { TabsRoot, TabsList, TabsTrigger, TabsIndicator } from "reka-ui";
11
+ import Icon from "../Icon/Icon.vue";
12
+ import LightDarkToggle from "../LightDarkToggle/LightDarkToggle.vue";
13
+
14
+ // Optional vue-router integration (no hard dependency).
15
+ // $router/$route on globalProperties is vue-router's stable public API.
16
+ // $route is defined as a getter that returns the current route, so we
17
+ // must re-read it from globalProperties on each access to track changes.
18
+ const instance = getCurrentInstance();
19
+ const globals = instance?.appContext.config.globalProperties;
20
+ const router = globals?.$router;
21
+
22
+ export interface Tab {
23
+ value: string;
24
+ label: string;
25
+ to?: string;
26
+ }
27
+
28
+ const props = defineProps<{
29
+ hideTopbar?: boolean;
30
+ tabs?: Tab[];
31
+ }>();
32
+
33
+ const tab = defineModel<string>("tab");
34
+
35
+ const mql = window.matchMedia("(max-width: 767px)");
36
+ const isMobile = ref(mql.matches);
37
+ const collapsed = ref(mql.matches);
38
+
39
+ function onMediaChange(e: MediaQueryListEvent) {
40
+ isMobile.value = e.matches;
41
+ collapsed.value = e.matches;
42
+ }
43
+
44
+ onMounted(() => {
45
+ mql.addEventListener("change", onMediaChange);
46
+ });
47
+
48
+ onUnmounted(() => {
49
+ mql.removeEventListener("change", onMediaChange);
50
+ });
51
+
52
+ function toggle() {
53
+ collapsed.value = !collapsed.value;
54
+ }
55
+
56
+ const isRouterMode = computed(() => !!router && props.tabs?.some((t) => t.to));
57
+
58
+ const activeTab = computed({
59
+ get() {
60
+ return tab.value ?? props.tabs?.[0]?.value;
61
+ },
62
+ set(value: string | undefined) {
63
+ if (!value) return;
64
+ tab.value = value;
65
+ if (isRouterMode.value && router) {
66
+ const target = props.tabs?.find((t) => t.value === value);
67
+ if (target?.to) router.push(target.to);
68
+ }
69
+ },
70
+ });
71
+
72
+ // Sync active tab from route changes in router mode
73
+ if (globals) {
74
+ watch(
75
+ () => globals.$route?.path,
76
+ (path) => {
77
+ if (!path || !isRouterMode.value) return;
78
+ const match = props.tabs?.find((t) => t.to === path);
79
+ if (match) tab.value = match.value;
80
+ },
81
+ { immediate: true },
82
+ );
83
+ }
84
+ </script>
85
+
86
+ <template>
87
+ <div class="SidebarLayout" :data-collapsed="collapsed">
88
+ <div class="SidebarRail">
89
+ <aside class="Sidebar" :inert="collapsed">
90
+ <div class="SidebarScroll">
91
+ <div class="SidebarHeader">
92
+ <button
93
+ type="button"
94
+ class="Toggle"
95
+ aria-label="Collapse sidebar"
96
+ title="Collapse sidebar"
97
+ @click="toggle"
98
+ >
99
+ <Icon icon="keyboard_double_arrow_left" size="sm" />
100
+ </button>
101
+ </div>
102
+ <slot name="sidebar" />
103
+ </div>
104
+ </aside>
105
+ <button
106
+ type="button"
107
+ class="Toggle Toggle--expand"
108
+ :inert="!collapsed"
109
+ aria-label="Expand sidebar"
110
+ title="Expand sidebar"
111
+ @click="toggle"
112
+ >
113
+ <Icon icon="keyboard_double_arrow_right" size="sm" />
114
+ </button>
115
+ </div>
116
+ <main class="Main">
117
+ <TabsRoot
118
+ v-if="tabs?.length"
119
+ :model-value="activeTab"
120
+ class="TabsLayout"
121
+ @update:model-value="activeTab = $event as string"
122
+ >
123
+ <div class="TabsBar">
124
+ <button
125
+ v-if="isMobile && collapsed"
126
+ type="button"
127
+ class="Toggle"
128
+ aria-label="Expand sidebar"
129
+ title="Expand sidebar"
130
+ @click="toggle"
131
+ >
132
+ <Icon icon="keyboard_double_arrow_right" size="sm" />
133
+ </button>
134
+ <TabsList class="TabsList" aria-label="Tabs">
135
+ <TabsTrigger
136
+ v-for="t in tabs"
137
+ :key="t.value"
138
+ :value="t.value"
139
+ class="TabsTrigger"
140
+ >
141
+ {{ t.label }}
142
+ </TabsTrigger>
143
+ <TabsIndicator
144
+ class="TabsIndicator"
145
+ :style="{
146
+ width: 'var(--reka-tabs-indicator-size)',
147
+ left: 'var(--reka-tabs-indicator-position)',
148
+ }"
149
+ />
150
+ </TabsList>
151
+ <div class="TabsBarEnd">
152
+ <slot name="topbar" />
153
+ <LightDarkToggle v-if="!hideTopbar" />
154
+ </div>
155
+ </div>
156
+ <div class="MainScroll">
157
+ <div class="MainContent">
158
+ <slot />
159
+ </div>
160
+ </div>
161
+ </TabsRoot>
162
+ <template v-else>
163
+ <div class="Topbar">
164
+ <button
165
+ v-if="isMobile && collapsed"
166
+ type="button"
167
+ class="Toggle"
168
+ aria-label="Expand sidebar"
169
+ title="Expand sidebar"
170
+ @click="toggle"
171
+ >
172
+ <Icon icon="keyboard_double_arrow_right" size="sm" />
173
+ </button>
174
+ <div class="TopbarEnd">
175
+ <slot name="topbar" />
176
+ <LightDarkToggle v-if="!hideTopbar" />
177
+ </div>
178
+ </div>
179
+ <div class="MainScroll">
180
+ <div class="MainContent">
181
+ <slot />
182
+ </div>
183
+ </div>
184
+ </template>
185
+ </main>
186
+ </div>
187
+ </template>
188
+
189
+ <style scoped>
190
+ .SidebarLayout {
191
+ --bar-height: 3rem;
192
+ display: flex;
193
+ height: 100vh;
194
+ height: 100dvh;
195
+ background-color: var(--color-bg-0);
196
+ color: var(--color-text);
197
+ font-family: var(--font-family);
198
+ position: relative;
199
+ }
200
+
201
+ .SidebarRail {
202
+ flex-shrink: 0;
203
+ width: var(--sidebar-width);
204
+ height: 100%;
205
+ overflow: hidden;
206
+ transition: width var(--transition-normal);
207
+ position: relative;
208
+ }
209
+
210
+ @media (min-width: 768px) {
211
+ .SidebarLayout[data-collapsed="true"] .SidebarRail {
212
+ width: var(--toggle-size);
213
+ background-color: var(--color-bg-1);
214
+ border-right: 1px solid var(--color-border);
215
+ box-shadow: var(--shadow-sm);
216
+ }
217
+ }
218
+
219
+ .Sidebar {
220
+ font-size: var(--font-size-sm);
221
+ display: flex;
222
+ width: var(--sidebar-width);
223
+ height: 100%;
224
+ transform: translateX(0);
225
+ transition: transform var(--transition-normal);
226
+ }
227
+
228
+ .SidebarLayout[data-collapsed="true"] .Sidebar {
229
+ transform: translateX(-100%);
230
+ }
231
+
232
+ .SidebarScroll {
233
+ flex: 1;
234
+ min-width: 0;
235
+ overflow-y: auto;
236
+ padding: var(--space-4);
237
+ background-color: var(--color-bg-1);
238
+ border-right: 1px solid var(--color-border);
239
+ box-shadow: var(--shadow-sm);
240
+ scrollbar-width: thin;
241
+ scrollbar-color: transparent transparent;
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: var(--space-3);
245
+ }
246
+
247
+ .SidebarScroll:hover {
248
+ scrollbar-color: var(--color-border) transparent;
249
+ }
250
+
251
+ .SidebarScroll :deep(h2) {
252
+ font-size: var(--font-size-sm);
253
+ font-weight: 600;
254
+ text-transform: uppercase;
255
+ letter-spacing: 0.05em;
256
+ color: var(--color-text-secondary);
257
+ margin: var(--space-4) 0 var(--space-2);
258
+ }
259
+
260
+ .SidebarScroll > :deep(h2:first-child) {
261
+ margin-top: 0;
262
+ }
263
+
264
+ .Toggle {
265
+ flex-shrink: 0;
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ width: var(--toggle-size);
270
+ height: var(--toggle-size);
271
+ margin: 0;
272
+ padding: 0;
273
+ border: none;
274
+ background: transparent;
275
+ color: var(--color-text-secondary);
276
+ cursor: pointer;
277
+ transition:
278
+ color var(--transition-fast),
279
+ background-color var(--transition-fast);
280
+ }
281
+
282
+ .Toggle:hover {
283
+ color: var(--color-text);
284
+ background-color: var(--color-bg-2);
285
+ }
286
+
287
+ .Toggle:focus-visible {
288
+ outline: none;
289
+ box-shadow: var(--shadow-focus);
290
+ }
291
+
292
+ .SidebarHeader {
293
+ display: flex;
294
+ justify-content: flex-end;
295
+ margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4))
296
+ calc(-1 * var(--space-3));
297
+ }
298
+
299
+ .Toggle--expand {
300
+ position: absolute;
301
+ top: 0;
302
+ left: 50%;
303
+ transform: translateX(-50%);
304
+ opacity: 0;
305
+ pointer-events: none;
306
+ transition: opacity var(--transition-fast);
307
+ }
308
+
309
+ .SidebarLayout[data-collapsed="true"] .Toggle--expand {
310
+ opacity: 1;
311
+ pointer-events: auto;
312
+ }
313
+
314
+ .Main {
315
+ flex: 1;
316
+ min-width: 0;
317
+ display: flex;
318
+ flex-direction: column;
319
+ overflow: hidden;
320
+ background-color: var(--color-bg-0);
321
+ font-size: var(--font-size-md);
322
+ container-type: inline-size;
323
+ }
324
+
325
+ .Topbar {
326
+ display: flex;
327
+ align-items: center;
328
+ min-height: var(--bar-height);
329
+ padding: 0 var(--space-4);
330
+ flex-shrink: 0;
331
+ }
332
+
333
+ .TopbarEnd {
334
+ margin-left: auto;
335
+ display: flex;
336
+ align-items: center;
337
+ gap: var(--space-1);
338
+ }
339
+
340
+ @container (min-width: 768px) {
341
+ .Topbar {
342
+ padding: 0 var(--space-4) 0 var(--space-20);
343
+ }
344
+ }
345
+
346
+ .MainScroll {
347
+ flex: 1;
348
+ min-height: 0;
349
+ overflow-y: auto;
350
+ overflow-x: hidden;
351
+ padding: var(--space-6) 0;
352
+ scrollbar-width: thin;
353
+ scrollbar-color: transparent transparent;
354
+ }
355
+
356
+ .MainScroll:hover {
357
+ scrollbar-color: var(--color-border) transparent;
358
+ }
359
+
360
+ .MainContent {
361
+ max-width: 1024px;
362
+ padding: 0 var(--space-4);
363
+ }
364
+
365
+ @container (min-width: 768px) {
366
+ .MainContent {
367
+ padding: 0 var(--space-4) 0 var(--space-20);
368
+ }
369
+ }
370
+
371
+ /* Mobile: use transform instead of width resize */
372
+ @media (max-width: 767px) {
373
+ .SidebarLayout {
374
+ transition: transform var(--transition-normal);
375
+ }
376
+
377
+ .SidebarLayout[data-collapsed="true"] {
378
+ transform: translateX(calc(-1 * var(--sidebar-width)));
379
+ }
380
+
381
+ .SidebarLayout[data-collapsed="true"] .Sidebar {
382
+ transform: translateX(0);
383
+ }
384
+
385
+ .SidebarRail {
386
+ min-width: var(--sidebar-width);
387
+ }
388
+
389
+ .Main {
390
+ min-width: 100vw;
391
+ }
392
+
393
+ .Toggle--expand {
394
+ display: none;
395
+ }
396
+ }
397
+
398
+ /* Tabs */
399
+ .TabsLayout {
400
+ display: flex;
401
+ flex-direction: column;
402
+ flex: 1;
403
+ min-height: 0;
404
+ }
405
+
406
+ .TabsBar {
407
+ flex-shrink: 0;
408
+ display: flex;
409
+ align-items: center;
410
+ min-height: var(--bar-height);
411
+ border-bottom: 1px solid var(--color-border);
412
+ padding: 0 var(--space-4);
413
+ }
414
+
415
+ .TabsBarEnd {
416
+ margin-left: auto;
417
+ display: flex;
418
+ align-items: center;
419
+ gap: var(--space-1);
420
+ }
421
+
422
+ .TabsList {
423
+ display: flex;
424
+ gap: var(--space-1);
425
+ position: relative;
426
+ align-self: stretch;
427
+ }
428
+
429
+ .TabsTrigger {
430
+ position: relative;
431
+ padding: var(--space-2) var(--space-3);
432
+ font-size: var(--font-size-sm);
433
+ font-weight: 500;
434
+ font-family: inherit;
435
+ color: var(--color-text-secondary);
436
+ background: none;
437
+ border: none;
438
+ cursor: pointer;
439
+ transition:
440
+ color var(--transition-fast),
441
+ background-color var(--transition-fast);
442
+ border-radius: var(--radius-md) var(--radius-md) 0 0;
443
+ }
444
+
445
+ .TabsTrigger:hover {
446
+ color: var(--color-text);
447
+ background-color: var(--color-bg-1);
448
+ }
449
+
450
+ .TabsTrigger[data-state="active"] {
451
+ color: var(--color-text);
452
+ }
453
+
454
+ .TabsTrigger:focus-visible {
455
+ outline: none;
456
+ box-shadow: var(--shadow-focus);
457
+ }
458
+
459
+ .TabsIndicator {
460
+ position: absolute;
461
+ bottom: 0;
462
+ height: 2px;
463
+ background-color: var(--color-text);
464
+ transition:
465
+ width var(--transition-fast),
466
+ left var(--transition-fast);
467
+ }
468
+ </style>
@@ -0,0 +1,45 @@
1
+ # Spinner
2
+
3
+ A loading indicator with accessible labeling.
4
+
5
+ ## Examples
6
+
7
+ ### Sizes
8
+
9
+ <ComponentDemo>
10
+ <Spinner size="sm" label="Loading" />
11
+ <Spinner size="md" label="Loading" />
12
+ <Spinner size="lg" label="Loading" />
13
+
14
+ <template #code>
15
+
16
+ ```vue
17
+ <Spinner size="sm" label="Loading" />
18
+ <Spinner size="md" label="Loading" />
19
+ <Spinner size="lg" label="Loading" />
20
+ ```
21
+
22
+ </template>
23
+ </ComponentDemo>
24
+
25
+ ### In context
26
+
27
+ <ComponentDemo>
28
+ <div style="display: flex; align-items: center; gap: 8px;">
29
+ <Spinner size="sm" label="Running model" />
30
+ <span>Running model...</span>
31
+ </div>
32
+
33
+ <template #code>
34
+
35
+ ```vue
36
+ <div style="display: flex; align-items: center; gap: 8px;">
37
+ <Spinner size="sm" label="Running model" />
38
+ <span>Running model...</span>
39
+ </div>
40
+ ```
41
+
42
+ </template>
43
+ </ComponentDemo>
44
+
45
+ <!--@include: ./_api/spinner.md-->
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ export type SpinnerSize = "sm" | "md" | "lg";
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ size?: SpinnerSize;
7
+ label?: string;
8
+ }>(),
9
+ {
10
+ size: "sm",
11
+ },
12
+ );
13
+ </script>
14
+
15
+ <template>
16
+ <span
17
+ class="Spinner"
18
+ :data-size="size"
19
+ role="status"
20
+ :aria-label="label ?? 'Loading'"
21
+ />
22
+ </template>
23
+
24
+ <style>
25
+ .Spinner {
26
+ display: inline-block;
27
+ border: 2px solid currentColor;
28
+ border-top-color: transparent;
29
+ border-radius: 50%;
30
+ animation: cfa-spin 0.6s linear infinite;
31
+ vertical-align: middle;
32
+ }
33
+
34
+ .Spinner[data-size="sm"] {
35
+ width: 14px;
36
+ height: 14px;
37
+ }
38
+
39
+ .Spinner[data-size="md"] {
40
+ width: 20px;
41
+ height: 20px;
42
+ }
43
+
44
+ .Spinner[data-size="lg"] {
45
+ width: 28px;
46
+ height: 28px;
47
+ border-width: 3px;
48
+ }
49
+
50
+ @keyframes cfa-spin {
51
+ to {
52
+ transform: rotate(360deg);
53
+ }
54
+ }
55
+ </style>
@@ -0,0 +1,68 @@
1
+ # TextInput
2
+
3
+ A text input field with optional label and hint.
4
+
5
+ ## Examples
6
+
7
+ <script setup>
8
+ import { ref } from 'vue'
9
+ const name = ref('')
10
+ </script>
11
+
12
+ <ComponentDemo>
13
+ <div style="width: 300px">
14
+ <TextInput
15
+ v-model="name"
16
+ label="Model name"
17
+ placeholder="e.g. my-model"
18
+ hint="A short identifier used in URLs"
19
+ />
20
+ </div>
21
+
22
+ <template #code>
23
+
24
+ ```vue
25
+ <script setup>
26
+ import { ref } from "vue";
27
+ const name = ref("");
28
+ </script>
29
+
30
+ <TextInput
31
+ v-model="name"
32
+ label="Model name"
33
+ placeholder="e.g. my-model"
34
+ hint="A short identifier used in URLs"
35
+ />
36
+ ```
37
+
38
+ </template>
39
+ </ComponentDemo>
40
+
41
+ ### Hidden label
42
+
43
+ Use `hide-label` to visually hide the label while keeping it available to screen
44
+ readers. The label is still a real `<label>` in the DOM, so clicking the input
45
+ area still works and the control is properly named for assistive tech. Prefer
46
+ this over omitting the label whenever the input has no visible text describing
47
+ it.
48
+
49
+ <ComponentDemo>
50
+ <div style="width: 300px">
51
+ <TextInput
52
+ v-model="name"
53
+ label="Search"
54
+ placeholder="Search…"
55
+ hide-label
56
+ />
57
+ </div>
58
+
59
+ <template #code>
60
+
61
+ ```vue
62
+ <TextInput v-model="name" label="Search" placeholder="Search…" hide-label />
63
+ ```
64
+
65
+ </template>
66
+ </ComponentDemo>
67
+
68
+ <!--@include: ./_api/text-input.md-->
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from "vue";
3
+ import Hint from "../Hint/Hint.vue";
4
+ import type { FieldProps } from "../_internal/field";
5
+ import "../_internal/input.css";
6
+
7
+ const model = defineModel<string>();
8
+ const local = ref(model.value);
9
+
10
+ watch(model, (v) => {
11
+ local.value = v;
12
+ });
13
+
14
+ function commit() {
15
+ model.value = local.value;
16
+ }
17
+
18
+ interface Props extends FieldProps {
19
+ placeholder?: string;
20
+ }
21
+
22
+ const props = defineProps<Props>();
23
+ </script>
24
+
25
+ <template>
26
+ <label v-if="props.label" class="cfasim-input-label">
27
+ <span
28
+ class="cfasim-input-label-row"
29
+ :class="{ 'visually-hidden': props.hideLabel }"
30
+ >
31
+ {{ props.label }}
32
+ <Hint v-if="props.hint && !props.hideLabel" :text="props.hint" />
33
+ </span>
34
+ <input
35
+ type="text"
36
+ class="cfasim-input"
37
+ v-model="local"
38
+ :placeholder="props.placeholder"
39
+ @blur="commit"
40
+ @keydown.enter="commit"
41
+ />
42
+ </label>
43
+ <div v-else>
44
+ <input
45
+ type="text"
46
+ class="cfasim-input"
47
+ v-model="local"
48
+ :placeholder="props.placeholder"
49
+ :aria-label="props.ariaLabel"
50
+ @blur="commit"
51
+ @keydown.enter="commit"
52
+ />
53
+ </div>
54
+ </template>