@cfasim-ui/docs 0.5.1 → 0.6.1

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.
@@ -0,0 +1,338 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ ComboboxAnchor,
4
+ ComboboxContent,
5
+ ComboboxEmpty,
6
+ ComboboxInput,
7
+ ComboboxItem,
8
+ ComboboxItemIndicator,
9
+ ComboboxPortal,
10
+ ComboboxRoot,
11
+ ComboboxTrigger,
12
+ ComboboxViewport,
13
+ useId,
14
+ } from "reka-ui";
15
+ import { computed, nextTick, ref, watch } from "vue";
16
+ import Icon from "../Icon/Icon.vue";
17
+ import Hint from "../Hint/Hint.vue";
18
+
19
+ export interface MultiSelectOption {
20
+ value: string;
21
+ label: string;
22
+ }
23
+
24
+ const model = defineModel<string[]>({ default: () => [] });
25
+
26
+ const props = defineProps<{
27
+ label?: string;
28
+ hideLabel?: boolean;
29
+ ariaLabel?: string;
30
+ options: MultiSelectOption[];
31
+ placeholder?: string;
32
+ hint?: string;
33
+ }>();
34
+
35
+ const id = useId();
36
+ const fieldRef = ref<HTMLElement | null>(null);
37
+
38
+ // In multiple mode the list stays open after a selection, so reka keeps
39
+ // `isUserInputted` true and suppresses its own `resetSearchTermOnSelect`. Clear
40
+ // the typed text (and the underlying filter) ourselves on every selection
41
+ // change by dispatching a native input event the combobox listens for.
42
+ watch(model, () => {
43
+ nextTick(() => {
44
+ const input = fieldRef.value?.querySelector("input");
45
+ if (input?.value) {
46
+ input.value = "";
47
+ input.dispatchEvent(new Event("input", { bubbles: true }));
48
+ }
49
+ });
50
+ });
51
+
52
+ const selectedOptions = computed(() =>
53
+ model.value
54
+ .map((v) => props.options.find((o) => o.value === v))
55
+ .filter((o): o is MultiSelectOption => Boolean(o)),
56
+ );
57
+
58
+ function remove(value: string) {
59
+ model.value = model.value.filter((v) => v !== value);
60
+ }
61
+
62
+ // Backspace in an empty input removes the last selected chip.
63
+ function onInputKeydown(event: KeyboardEvent) {
64
+ const target = event.target as HTMLInputElement;
65
+ if (event.key === "Backspace" && !target.value && model.value.length) {
66
+ remove(model.value[model.value.length - 1]);
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <template>
72
+ <div class="multi-select">
73
+ <label
74
+ v-if="label"
75
+ :id="`${id}-label`"
76
+ class="multi-select-label"
77
+ :class="{ 'visually-hidden': hideLabel }"
78
+ >
79
+ {{ label }}
80
+ <Hint v-if="hint && !hideLabel" :text="hint" />
81
+ </label>
82
+ <ComboboxRoot
83
+ v-model="model"
84
+ multiple
85
+ open-on-click
86
+ class="multi-select-root"
87
+ >
88
+ <ComboboxAnchor class="multi-select-anchor">
89
+ <div ref="fieldRef" class="multi-select-field">
90
+ <ul v-if="selectedOptions.length" class="multi-select-chips">
91
+ <li
92
+ v-for="opt in selectedOptions"
93
+ :key="opt.value"
94
+ class="multi-select-chip"
95
+ >
96
+ <span class="multi-select-chip-label">{{ opt.label }}</span>
97
+ <button
98
+ type="button"
99
+ class="multi-select-chip-remove"
100
+ :aria-label="`Remove ${opt.label}`"
101
+ @click="remove(opt.value)"
102
+ >
103
+ <Icon icon="close" :size="14" />
104
+ </button>
105
+ </li>
106
+ </ul>
107
+ <ComboboxInput
108
+ class="multi-select-input"
109
+ :placeholder="selectedOptions.length ? undefined : placeholder"
110
+ :aria-labelledby="label ? `${id}-label` : undefined"
111
+ :aria-label="!label ? ariaLabel : undefined"
112
+ @keydown="onInputKeydown"
113
+ />
114
+ </div>
115
+ <ComboboxTrigger
116
+ class="multi-select-trigger"
117
+ aria-label="Toggle options"
118
+ >
119
+ <span class="multi-select-icon" aria-hidden="true">
120
+ <svg
121
+ width="12"
122
+ height="12"
123
+ viewBox="0 0 12 12"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ stroke-width="2"
127
+ stroke-linecap="round"
128
+ stroke-linejoin="round"
129
+ >
130
+ <path d="M3 4.5L6 7.5L9 4.5" />
131
+ </svg>
132
+ </span>
133
+ </ComboboxTrigger>
134
+ </ComboboxAnchor>
135
+ <ComboboxPortal>
136
+ <ComboboxContent
137
+ class="multi-select-content"
138
+ position="popper"
139
+ :side-offset="4"
140
+ :body-lock="false"
141
+ >
142
+ <ComboboxViewport class="multi-select-viewport">
143
+ <ComboboxEmpty class="multi-select-empty">No matches</ComboboxEmpty>
144
+ <ComboboxItem
145
+ v-for="opt in options"
146
+ :key="opt.value"
147
+ :value="opt.value"
148
+ class="multi-select-item"
149
+ >
150
+ <span>{{ opt.label }}</span>
151
+ <ComboboxItemIndicator class="multi-select-indicator">
152
+ <Icon icon="check" :size="14" />
153
+ </ComboboxItemIndicator>
154
+ </ComboboxItem>
155
+ </ComboboxViewport>
156
+ </ComboboxContent>
157
+ </ComboboxPortal>
158
+ </ComboboxRoot>
159
+ </div>
160
+ </template>
161
+
162
+ <style scoped>
163
+ .multi-select {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 0.25em;
167
+ }
168
+
169
+ .multi-select-label {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: space-between;
173
+ font-size: var(--font-size-sm);
174
+ }
175
+
176
+ .multi-select-anchor {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 0.25em;
180
+ min-height: 2.5em;
181
+ padding: 0.25em 0.5em;
182
+ font-size: var(--font-size-sm);
183
+ background: var(--color-bg-0);
184
+ border: 1px solid var(--color-border);
185
+ border-radius: 0.375em;
186
+ transition:
187
+ border-color var(--transition-fast),
188
+ box-shadow var(--transition-fast);
189
+ }
190
+
191
+ .multi-select-anchor:hover {
192
+ border-color: var(--color-border-hover);
193
+ }
194
+
195
+ .multi-select-anchor:focus-within {
196
+ outline: none;
197
+ border-color: var(--color-border-focus);
198
+ box-shadow: var(--shadow-focus);
199
+ }
200
+
201
+ .multi-select-field {
202
+ display: flex;
203
+ flex-wrap: wrap;
204
+ align-items: center;
205
+ gap: 0.25em;
206
+ flex: 1;
207
+ min-width: 0;
208
+ }
209
+
210
+ .multi-select-chips {
211
+ display: contents;
212
+ list-style: none;
213
+ margin: 0;
214
+ padding: 0;
215
+ }
216
+
217
+ .multi-select-chip {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ gap: 0.25em;
221
+ /* Reset list margins (e.g. a consumer's `li + li` spacing) so chips line up. */
222
+ margin: 0;
223
+ padding: 0.125em 0.25em 0.125em 0.5em;
224
+ background: var(--color-bg-2);
225
+ border: 1px solid var(--color-border);
226
+ border-radius: 0.25em;
227
+ line-height: 1.4;
228
+ }
229
+
230
+ .multi-select-chip-remove {
231
+ display: inline-flex;
232
+ align-items: center;
233
+ padding: 0;
234
+ border: none;
235
+ background: none;
236
+ color: var(--color-text-secondary);
237
+ cursor: pointer;
238
+ border-radius: 0.25em;
239
+ }
240
+
241
+ .multi-select-chip-remove:hover {
242
+ color: var(--color-text);
243
+ }
244
+
245
+ .multi-select-chip-remove:focus-visible {
246
+ outline: 2px solid var(--color-primary);
247
+ outline-offset: 1px;
248
+ }
249
+
250
+ .multi-select-input {
251
+ flex: 1;
252
+ min-width: 3em;
253
+ height: 1.75em;
254
+ padding: 0;
255
+ border: none;
256
+ background: none;
257
+ color: var(--color-text);
258
+ font: inherit;
259
+ }
260
+
261
+ .multi-select-input:focus {
262
+ outline: none;
263
+ }
264
+
265
+ .multi-select-input::placeholder {
266
+ color: var(--color-text-tertiary);
267
+ }
268
+
269
+ .multi-select-trigger {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ flex-shrink: 0;
273
+ padding: 0;
274
+ border: none;
275
+ background: none;
276
+ color: inherit;
277
+ cursor: pointer;
278
+ }
279
+
280
+ .multi-select-icon {
281
+ display: flex;
282
+ align-items: center;
283
+ }
284
+ </style>
285
+
286
+ <style>
287
+ .multi-select-content {
288
+ z-index: 100;
289
+ background: var(--color-bg-0);
290
+ border: 1px solid var(--color-border);
291
+ border-radius: 0.25em;
292
+ box-shadow:
293
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
294
+ 0 2px 4px -2px rgba(0, 0, 0, 0.1);
295
+ width: var(--reka-combobox-trigger-width);
296
+ max-height: var(--reka-combobox-content-available-height);
297
+ }
298
+
299
+ .multi-select-viewport {
300
+ padding: 0.25em;
301
+ }
302
+
303
+ .multi-select-empty {
304
+ padding: 0.5em;
305
+ font-size: var(--font-size-sm);
306
+ color: var(--color-text-secondary);
307
+ text-align: center;
308
+ }
309
+
310
+ .multi-select-item {
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: space-between;
314
+ gap: 0.5em;
315
+ padding: 0.25em 0.5em;
316
+ border-radius: 0.25em;
317
+ font-size: var(--font-size-sm);
318
+ white-space: nowrap;
319
+ cursor: pointer;
320
+ user-select: none;
321
+ outline: none;
322
+ }
323
+
324
+ .multi-select-item[data-highlighted] {
325
+ background: var(--color-primary);
326
+ color: white;
327
+ }
328
+
329
+ .multi-select-item[data-state="checked"] {
330
+ font-weight: 600;
331
+ }
332
+
333
+ .multi-select-indicator {
334
+ display: flex;
335
+ align-items: center;
336
+ flex-shrink: 0;
337
+ }
338
+ </style>
@@ -8,6 +8,8 @@ The editor is **lazy-loaded**: the underlying CodeMirror bundle and YAML/TOML pa
8
8
 
9
9
  Users can switch formats from the dropdown; the editor round-trips the current value through the new format. Apply emits an `apply` event with the parsed value — the parent decides what to do with it (typically merge back into its own parameter state).
10
10
 
11
+ Set the editor font size with the `fontSize` prop (any CSS length, e.g. `"16px"` or `"1rem"`); it defaults to `var(--font-size-sm)`.
12
+
11
13
  ## Examples
12
14
 
13
15
  <script setup>
@@ -61,6 +63,22 @@ function onApply(v) {
61
63
  </template>
62
64
  </ComponentDemo>
63
65
 
66
+ ### Custom font size
67
+
68
+ <ComponentDemo>
69
+ <div style="width: 100%">
70
+ <ParamEditor :value="params" font-size="16px" @apply="onApply" />
71
+ </div>
72
+
73
+ <template #code>
74
+
75
+ ```vue
76
+ <ParamEditor :value="params" font-size="16px" @apply="onApply" />
77
+ ```
78
+
79
+ </template>
80
+ </ComponentDemo>
81
+
64
82
  ## Props
65
83
 
66
84
  | Prop | Type | Required | Default |
@@ -68,6 +86,7 @@ function onApply(v) {
68
86
  | `value` | `ParamEditorValue` | Yes | — |
69
87
  | `format` | `ParamEditorFormat` | No | — |
70
88
  | `height` | `string` | No | — |
89
+ | `fontSize` | `string` | No | — |
71
90
  | `filename` | `string` | No | — |
72
91
 
73
92
  ## Events
@@ -9,6 +9,9 @@ defineProps<{
9
9
  value: ParamEditorValue;
10
10
  format?: ParamEditorFormat;
11
11
  height?: string;
12
+ /** Editor font size, as any CSS length (e.g. `"16px"`, `"1rem"`).
13
+ * Defaults to `var(--font-size-sm)`. */
14
+ fontSize?: string;
12
15
  /** Basename for the Export download (no extension). Defaults to
13
16
  * `params-YYYYMMDD-HHMMSS` computed at click time. */
14
17
  filename?: string;
@@ -33,6 +36,7 @@ const Impl = defineAsyncComponent({
33
36
  :value="value"
34
37
  :format="format"
35
38
  :height="height"
39
+ :font-size="fontSize"
36
40
  :filename="filename"
37
41
  @apply="$emit('apply', $event)"
38
42
  />
@@ -27,9 +27,10 @@ const props = withDefaults(
27
27
  value: Value;
28
28
  format?: Format;
29
29
  height?: string;
30
+ fontSize?: string;
30
31
  filename?: string;
31
32
  }>(),
32
- { format: "json", height: "320px" },
33
+ { format: "json", height: "320px", fontSize: "var(--font-size-sm)" },
33
34
  );
34
35
 
35
36
  const emit = defineEmits<{ apply: [value: Value] }>();
@@ -264,7 +265,7 @@ async function onFileChange(e: Event) {
264
265
  <Codemirror
265
266
  v-model="text"
266
267
  :extensions="extensions"
267
- :style="{ height }"
268
+ :style="{ height, fontSize }"
268
269
  :indent-with-tab="false"
269
270
  :tab-size="2"
270
271
  class="param-editor-cm"
@@ -317,7 +318,6 @@ async function onFileChange(e: Event) {
317
318
  .param-editor-cm {
318
319
  border: 1px solid var(--color-border);
319
320
  border-radius: 0.375em;
320
- font-size: var(--font-size-sm);
321
321
  overflow: hidden;
322
322
  }
323
323
 
@@ -1,12 +1,15 @@
1
1
  # SelectBox
2
2
 
3
- A dropdown select built on reka-ui.
3
+ A single-select dropdown built on reka-ui. Set `autocomplete` to turn it into a
4
+ filterable single-select combobox: type to narrow the options, just like
5
+ [MultiSelect](./multi-select) but bound to a single `string`.
4
6
 
5
7
  ## Examples
6
8
 
7
9
  <script setup>
8
10
  import { ref } from 'vue'
9
11
  const interval = ref('weekly')
12
+ const state = ref('')
10
13
  </script>
11
14
 
12
15
  <ComponentDemo>
@@ -83,6 +86,73 @@ DOM.
83
86
  </template>
84
87
  </ComponentDemo>
85
88
 
89
+ ### Autocomplete
90
+
91
+ Add `autocomplete` to make the field filterable. Type to narrow the list; the
92
+ selected option's label fills the input. `v-model` is still a single `string`.
93
+
94
+ <ComponentDemo>
95
+ <div style="width: 240px">
96
+ <SelectBox
97
+ v-model="state"
98
+ autocomplete
99
+ label="State"
100
+ placeholder="Search states…"
101
+ :options="[
102
+ { value: 'ca', label: 'California' },
103
+ { value: 'ny', label: 'New York' },
104
+ { value: 'tx', label: 'Texas' },
105
+ { value: 'wa', label: 'Washington' },
106
+ { value: 'fl', label: 'Florida' },
107
+ ]"
108
+ />
109
+ </div>
110
+
111
+ <template #code>
112
+
113
+ ```vue
114
+ <script setup>
115
+ import { ref } from "vue";
116
+ const state = ref("");
117
+ </script>
118
+
119
+ <SelectBox
120
+ v-model="state"
121
+ autocomplete
122
+ label="State"
123
+ placeholder="Search states…"
124
+ :options="[
125
+ { value: 'ca', label: 'California' },
126
+ { value: 'ny', label: 'New York' },
127
+ { value: 'tx', label: 'Texas' },
128
+ { value: 'wa', label: 'Washington' },
129
+ { value: 'fl', label: 'Florida' },
130
+ ]"
131
+ />
132
+ ```
133
+
134
+ </template>
135
+ </ComponentDemo>
136
+
137
+ ## Accessibility
138
+
139
+ In the default mode the field is a reka-ui Select; with `autocomplete` it's a
140
+ reka-ui Combobox following the
141
+ [ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/):
142
+
143
+ - The input has `role="combobox"` with `aria-expanded`, `aria-controls`, and
144
+ `aria-autocomplete="list"`; the popup is a `role="listbox"` and each option a
145
+ `role="option"` that reflects its state via `aria-selected`.
146
+ - Keyboard support is handled for you: type to filter, ↑/↓ to move through the
147
+ list, Enter to select the highlighted option, and Escape to close.
148
+ - Pass `label` so the field is named by a real `<label>`. In autocomplete mode
149
+ the label is also wired to the input via `for`/`id`, so clicking it focuses
150
+ the field; use `hide-label` to keep the label for screen readers while hiding
151
+ it visually, or `aria-label` when there's no visible label text.
152
+ - The chevron is a labelled toggle (`aria-label="Toggle options"`) kept out of
153
+ the tab order, and native browser autofill is disabled so it can't cover the
154
+ option list.
155
+
86
156
  ## Model
87
157
 
88
158
  | Name | Type |
@@ -98,6 +168,7 @@ DOM.
98
168
  | `ariaLabel` | `string` | No | — |
99
169
  | `options` | `SelectOption[]` | Yes | — |
100
170
  | `placeholder` | `string` | No | — |
171
+ | `autocomplete` | `boolean` | No | — |
101
172
 
102
173
 
103
174
  ### SelectOption