@cfasim-ui/docs 0.5.1 → 0.6.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.
@@ -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
 
@@ -0,0 +1,163 @@
1
+ # ToggleGroup
2
+
3
+ A segmented control that looks like a [`ButtonGroup`](./button-group) but tracks
4
+ which item(s) are pressed. By default it is single-select (like a radio group);
5
+ add `multiple` to let several items be pressed at once. Built on
6
+ [reka-ui's ToggleGroup](https://reka-ui.com/docs/components/toggle-group).
7
+
8
+ ## Examples
9
+
10
+ ### Single select
11
+
12
+ `v-model` holds the pressed value (a `string`). Clicking the active item again
13
+ clears the selection.
14
+
15
+ <script setup>
16
+ import { ref } from 'vue'
17
+ const interval = ref('weekly')
18
+ const days = ref(['mon', 'wed'])
19
+ </script>
20
+
21
+ <ComponentDemo>
22
+ <ToggleGroup
23
+ v-model="interval"
24
+ label="Interval"
25
+ :options="[
26
+ { value: 'daily', label: 'Daily' },
27
+ { value: 'weekly', label: 'Weekly' },
28
+ { value: 'monthly', label: 'Monthly' },
29
+ ]"
30
+ />
31
+ <p>Selected: {{ interval }}</p>
32
+
33
+ <template #code>
34
+
35
+ ```vue
36
+ <script setup>
37
+ import { ref } from "vue";
38
+ const interval = ref("weekly");
39
+ </script>
40
+
41
+ <ToggleGroup
42
+ v-model="interval"
43
+ label="Interval"
44
+ :options="[
45
+ { value: 'daily', label: 'Daily' },
46
+ { value: 'weekly', label: 'Weekly' },
47
+ { value: 'monthly', label: 'Monthly' },
48
+ ]"
49
+ />
50
+ ```
51
+
52
+ </template>
53
+ </ComponentDemo>
54
+
55
+ ### Multiple select
56
+
57
+ With `multiple`, `v-model` is a `string[]` and any number of items can be
58
+ pressed at once.
59
+
60
+ <ComponentDemo>
61
+ <ToggleGroup
62
+ v-model="days"
63
+ multiple
64
+ label="Active days"
65
+ :options="[
66
+ { value: 'mon', label: 'Mon' },
67
+ { value: 'tue', label: 'Tue' },
68
+ { value: 'wed', label: 'Wed' },
69
+ { value: 'thu', label: 'Thu' },
70
+ { value: 'fri', label: 'Fri' },
71
+ ]"
72
+ />
73
+ <p>Selected: {{ days.join(', ') || 'none' }}</p>
74
+
75
+ <template #code>
76
+
77
+ ```vue
78
+ <script setup>
79
+ import { ref } from "vue";
80
+ const days = ref(["mon", "wed"]);
81
+ </script>
82
+
83
+ <ToggleGroup
84
+ v-model="days"
85
+ multiple
86
+ label="Active days"
87
+ :options="[
88
+ { value: 'mon', label: 'Mon' },
89
+ { value: 'tue', label: 'Tue' },
90
+ { value: 'wed', label: 'Wed' },
91
+ { value: 'thu', label: 'Thu' },
92
+ { value: 'fri', label: 'Fri' },
93
+ ]"
94
+ />
95
+ ```
96
+
97
+ </template>
98
+ </ComponentDemo>
99
+
100
+ ### Vertical and disabled options
101
+
102
+ Set `orientation="vertical"` to stack the items, and mark individual options
103
+ `disabled`.
104
+
105
+ <ComponentDemo>
106
+ <ToggleGroup
107
+ model-value="map"
108
+ orientation="vertical"
109
+ aria-label="View"
110
+ :options="[
111
+ { value: 'map', label: 'Map' },
112
+ { value: 'chart', label: 'Chart' },
113
+ { value: 'table', label: 'Table', disabled: true },
114
+ ]"
115
+ />
116
+
117
+ <template #code>
118
+
119
+ ```vue
120
+ <ToggleGroup
121
+ v-model="view"
122
+ orientation="vertical"
123
+ aria-label="View"
124
+ :options="[
125
+ { value: 'map', label: 'Map' },
126
+ { value: 'chart', label: 'Chart' },
127
+ { value: 'table', label: 'Table', disabled: true },
128
+ ]"
129
+ />
130
+ ```
131
+
132
+ </template>
133
+ </ComponentDemo>
134
+
135
+ ## Model
136
+
137
+ | Name | Type |
138
+ |------|------|
139
+ | `v-model` | `string \| string[]` |
140
+
141
+ ## Props
142
+
143
+ | Prop | Type | Required | Default |
144
+ |------|------|----------|---------|
145
+ | `options` | `ToggleGroupOption[]` | Yes | — |
146
+ | `multiple` | `boolean` | No | — |
147
+ | `label` | `string` | No | — |
148
+ | `hideLabel` | `boolean` | No | — |
149
+ | `ariaLabel` | `string` | No | — |
150
+ | `hint` | `string` | No | — |
151
+ | `disabled` | `boolean` | No | — |
152
+ | `orientation` | `"horizontal" \| "vertical"` | No | `"horizontal"` |
153
+
154
+
155
+ ### ToggleGroupOption
156
+
157
+ ```ts
158
+ interface ToggleGroupOption {
159
+ value: string;
160
+ label: string;
161
+ disabled?: boolean;
162
+ }
163
+ ```