@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.
@@ -1,5 +1,15 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
+ ComboboxAnchor,
4
+ ComboboxContent,
5
+ ComboboxEmpty,
6
+ ComboboxInput,
7
+ ComboboxItem,
8
+ ComboboxItemIndicator,
9
+ ComboboxPortal,
10
+ ComboboxRoot,
11
+ ComboboxTrigger,
12
+ ComboboxViewport,
3
13
  SelectContent,
4
14
  SelectItem,
5
15
  SelectItemIndicator,
@@ -25,9 +35,16 @@ const props = defineProps<{
25
35
  ariaLabel?: string;
26
36
  options: SelectOption[];
27
37
  placeholder?: string;
38
+ /** Turn the field into a filterable single-select autocomplete (combobox). */
39
+ autocomplete?: boolean;
28
40
  }>();
29
41
 
30
42
  const id = useId();
43
+
44
+ // Shows the selected option's label in the combobox input when not filtering.
45
+ function displayValue(value: string) {
46
+ return props.options.find((o) => o.value === value)?.label ?? "";
47
+ }
31
48
  </script>
32
49
 
33
50
  <template>
@@ -35,11 +52,81 @@ const id = useId();
35
52
  <label
36
53
  v-if="label"
37
54
  :id="`${id}-label`"
55
+ :for="autocomplete ? `${id}-input` : undefined"
38
56
  class="select-label"
39
57
  :class="{ 'visually-hidden': hideLabel }"
40
58
  >{{ label }}</label
41
59
  >
42
- <SelectRoot v-model="model">
60
+
61
+ <ComboboxRoot
62
+ v-if="autocomplete"
63
+ v-model="model"
64
+ open-on-click
65
+ class="select-combobox-root"
66
+ >
67
+ <ComboboxAnchor class="select-anchor">
68
+ <ComboboxInput
69
+ :id="`${id}-input`"
70
+ class="select-input"
71
+ :display-value="displayValue"
72
+ :placeholder="placeholder"
73
+ :aria-labelledby="props.label ? `${id}-label` : undefined"
74
+ :aria-label="!props.label ? props.ariaLabel : undefined"
75
+ />
76
+ <ComboboxTrigger class="select-icon-button" aria-label="Toggle options">
77
+ <span class="select-icon" aria-hidden="true">
78
+ <svg
79
+ width="12"
80
+ height="12"
81
+ viewBox="0 0 12 12"
82
+ fill="none"
83
+ stroke="currentColor"
84
+ stroke-width="2"
85
+ stroke-linecap="round"
86
+ stroke-linejoin="round"
87
+ >
88
+ <path d="M3 4.5L6 7.5L9 4.5" />
89
+ </svg>
90
+ </span>
91
+ </ComboboxTrigger>
92
+ </ComboboxAnchor>
93
+ <ComboboxPortal>
94
+ <ComboboxContent
95
+ class="select-content select-content-autocomplete"
96
+ position="popper"
97
+ :side-offset="4"
98
+ :body-lock="false"
99
+ >
100
+ <ComboboxViewport class="select-viewport">
101
+ <ComboboxEmpty class="select-empty">No matches</ComboboxEmpty>
102
+ <ComboboxItem
103
+ v-for="opt in options"
104
+ :key="opt.value"
105
+ :value="opt.value"
106
+ class="select-item"
107
+ >
108
+ <span>{{ opt.label }}</span>
109
+ <ComboboxItemIndicator class="select-indicator">
110
+ <svg
111
+ width="12"
112
+ height="12"
113
+ viewBox="0 0 12 12"
114
+ fill="none"
115
+ stroke="currentColor"
116
+ stroke-width="2"
117
+ stroke-linecap="round"
118
+ stroke-linejoin="round"
119
+ >
120
+ <path d="M2 6L5 9L10 3" />
121
+ </svg>
122
+ </ComboboxItemIndicator>
123
+ </ComboboxItem>
124
+ </ComboboxViewport>
125
+ </ComboboxContent>
126
+ </ComboboxPortal>
127
+ </ComboboxRoot>
128
+
129
+ <SelectRoot v-else v-model="model">
43
130
  <SelectTrigger
44
131
  class="select-trigger"
45
132
  :aria-labelledby="props.label ? `${id}-label` : undefined"
@@ -140,6 +227,62 @@ const id = useId();
140
227
  color: var(--color-text-secondary);
141
228
  }
142
229
 
230
+ /* Autocomplete (combobox) field */
231
+ .select-anchor {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 0.5em;
235
+ height: 2.5em;
236
+ padding: 0 0.5em 0 0.75em;
237
+ font-size: var(--font-size-sm);
238
+ background: var(--color-bg-0);
239
+ border: 1px solid var(--color-border);
240
+ border-radius: 0.375em;
241
+ transition:
242
+ border-color var(--transition-fast),
243
+ box-shadow var(--transition-fast);
244
+ }
245
+
246
+ .select-anchor:hover {
247
+ border-color: var(--color-border-hover);
248
+ }
249
+
250
+ .select-anchor:focus-within {
251
+ outline: none;
252
+ border-color: var(--color-border-focus);
253
+ box-shadow: var(--shadow-focus);
254
+ }
255
+
256
+ .select-input {
257
+ flex: 1;
258
+ min-width: 0;
259
+ height: 100%;
260
+ padding: 0;
261
+ border: none;
262
+ background: none;
263
+ color: inherit;
264
+ font: inherit;
265
+ }
266
+
267
+ .select-input:focus {
268
+ outline: none;
269
+ }
270
+
271
+ .select-input::placeholder {
272
+ color: var(--color-text-secondary);
273
+ }
274
+
275
+ .select-icon-button {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ flex-shrink: 0;
279
+ padding: 0;
280
+ border: none;
281
+ background: none;
282
+ color: inherit;
283
+ cursor: pointer;
284
+ }
285
+
143
286
  .select-icon {
144
287
  display: flex;
145
288
  align-items: center;
@@ -160,10 +303,23 @@ const id = useId();
160
303
  max-height: var(--reka-select-content-available-height);
161
304
  }
162
305
 
306
+ /* Combobox exposes the anchor width under its own custom property. */
307
+ .select-content-autocomplete {
308
+ width: var(--reka-combobox-trigger-width);
309
+ max-height: var(--reka-combobox-content-available-height);
310
+ }
311
+
163
312
  .select-viewport {
164
313
  padding: 0.25em;
165
314
  }
166
315
 
316
+ .select-empty {
317
+ padding: 0.5em;
318
+ font-size: var(--font-size-sm);
319
+ color: var(--color-text-secondary);
320
+ text-align: center;
321
+ }
322
+
167
323
  .select-item {
168
324
  display: flex;
169
325
  align-items: center;
@@ -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
+ ```
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ import { ToggleGroupItem, ToggleGroupRoot, useId } from "reka-ui";
3
+ import { computed } from "vue";
4
+ import Hint from "../Hint/Hint.vue";
5
+
6
+ export interface ToggleGroupOption {
7
+ value: string;
8
+ label: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ // `string` in single mode, `string[]` in multiple mode.
13
+ const model = defineModel<string | string[]>();
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ options: ToggleGroupOption[];
18
+ multiple?: boolean;
19
+ label?: string;
20
+ hideLabel?: boolean;
21
+ ariaLabel?: string;
22
+ hint?: string;
23
+ disabled?: boolean;
24
+ orientation?: "horizontal" | "vertical";
25
+ }>(),
26
+ { orientation: "horizontal" },
27
+ );
28
+
29
+ const id = useId();
30
+ const type = computed<"single" | "multiple">(() =>
31
+ props.multiple ? "multiple" : "single",
32
+ );
33
+ </script>
34
+
35
+ <template>
36
+ <div class="toggle-group-field">
37
+ <label
38
+ v-if="label"
39
+ :id="`${id}-label`"
40
+ class="toggle-group-label"
41
+ :class="{ 'visually-hidden': hideLabel }"
42
+ >
43
+ {{ label }}
44
+ <Hint v-if="hint && !hideLabel" :text="hint" />
45
+ </label>
46
+ <ToggleGroupRoot
47
+ v-model="model"
48
+ :type="type"
49
+ :disabled="disabled"
50
+ :orientation="orientation"
51
+ :class="['toggle-group', `toggle-group--${orientation}`]"
52
+ :aria-labelledby="label ? `${id}-label` : undefined"
53
+ :aria-label="!label ? ariaLabel : undefined"
54
+ >
55
+ <ToggleGroupItem
56
+ v-for="opt in options"
57
+ :key="opt.value"
58
+ :value="opt.value"
59
+ :disabled="opt.disabled"
60
+ class="toggle-group-item"
61
+ >
62
+ {{ opt.label }}
63
+ </ToggleGroupItem>
64
+ </ToggleGroupRoot>
65
+ </div>
66
+ </template>
67
+
68
+ <style scoped>
69
+ .toggle-group-field {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 0.25em;
73
+ align-items: flex-start;
74
+ }
75
+
76
+ .toggle-group-label {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 0.25em;
80
+ font-size: var(--font-size-sm);
81
+ }
82
+
83
+ .toggle-group {
84
+ display: inline-flex;
85
+ /* 1px gaps reveal the group background as dividers between items. */
86
+ gap: 1px;
87
+ background: var(--color-border);
88
+ border: 1px solid var(--color-border);
89
+ border-radius: 0.375em;
90
+ /* Clip items to the rounded outer corners. Focus uses an inset outline
91
+ * (below) so it isn't clipped away. */
92
+ overflow: hidden;
93
+ }
94
+
95
+ .toggle-group--vertical {
96
+ flex-direction: column;
97
+ }
98
+
99
+ .toggle-group-item {
100
+ appearance: none;
101
+ display: inline-flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ min-height: 2.5em;
105
+ padding: 0 1em;
106
+ margin: 0;
107
+ border: none;
108
+ background: var(--color-bg-0);
109
+ color: var(--color-text);
110
+ font: inherit;
111
+ font-size: var(--font-size-sm);
112
+ font-weight: 500;
113
+ white-space: nowrap;
114
+ cursor: pointer;
115
+ transition:
116
+ background var(--transition-fast),
117
+ color var(--transition-fast);
118
+ }
119
+
120
+ .toggle-group-item:hover:not([data-disabled]) {
121
+ background: var(--color-bg-2);
122
+ }
123
+
124
+ .toggle-group-item[data-state="on"] {
125
+ background: var(--color-primary);
126
+ color: var(--color-text-on-primary);
127
+ }
128
+
129
+ .toggle-group-item[data-state="on"]:hover:not([data-disabled]) {
130
+ background: var(--color-primary-hover);
131
+ }
132
+
133
+ .toggle-group-item:focus-visible {
134
+ outline: 2px solid var(--color-primary);
135
+ outline-offset: -2px;
136
+ z-index: 1;
137
+ }
138
+
139
+ .toggle-group-item[data-disabled] {
140
+ opacity: 0.5;
141
+ cursor: not-allowed;
142
+ }
143
+ </style>
@@ -1,6 +1,7 @@
1
1
  export { default as Box } from "./Box/Box.vue";
2
2
  export type { BoxVariant } from "./Box/Box.vue";
3
3
  export { default as Button } from "./Button/Button.vue";
4
+ export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.vue";
4
5
  export { default as Container } from "./Container/Container.vue";
5
6
  export type { ContainerGap } from "./Container/Container.vue";
6
7
  export { default as Expander } from "./Expander/Expander.vue";
@@ -12,6 +13,8 @@ export type { IconSize } from "./Icon/Icon.vue";
12
13
  export { registerIcons, hasIcon } from "./Icon/registry";
13
14
  export type { IconRegistration, IconVariants } from "./Icon/registry";
14
15
  export { default as LightDarkToggle } from "./LightDarkToggle/LightDarkToggle.vue";
16
+ export { default as MultiSelect } from "./MultiSelect/MultiSelect.vue";
17
+ export type { MultiSelectOption } from "./MultiSelect/MultiSelect.vue";
15
18
  export { default as NumberInput } from "./NumberInput/NumberInput.vue";
16
19
  export type { NumberRange } from "./NumberInput/NumberInput.vue";
17
20
  export { default as ParamEditor } from "./ParamEditor/ParamEditor.vue";
@@ -26,3 +29,5 @@ export type { Tab } from "./SidebarLayout/SidebarLayout.vue";
26
29
  export { default as Spinner } from "./Spinner/Spinner.vue";
27
30
  export { default as TextInput } from "./TextInput/TextInput.vue";
28
31
  export { default as Toggle } from "./Toggle/Toggle.vue";
32
+ export { default as ToggleGroup } from "./ToggleGroup/ToggleGroup.vue";
33
+ export type { ToggleGroupOption } from "./ToggleGroup/ToggleGroup.vue";
package/index.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.1",
2
+ "version": "0.6.1",
3
3
  "package": "@cfasim-ui/docs",
4
4
  "content": {
5
5
  "components": [
@@ -23,6 +23,20 @@
23
23
  "secondary"
24
24
  ]
25
25
  },
26
+ {
27
+ "name": "ButtonGroup",
28
+ "slug": "button-group",
29
+ "docs": "components/ButtonGroup/ButtonGroup.md",
30
+ "source": "components/ButtonGroup/ButtonGroup.vue",
31
+ "keywords": [
32
+ "button group",
33
+ "buttons",
34
+ "group",
35
+ "toolbar",
36
+ "segmented",
37
+ "joined"
38
+ ]
39
+ },
26
40
  {
27
41
  "name": "Container",
28
42
  "slug": "container",
@@ -76,6 +90,23 @@
76
90
  "source": "components/Icon/Icon.vue",
77
91
  "keywords": []
78
92
  },
93
+ {
94
+ "name": "MultiSelect",
95
+ "slug": "multi-select",
96
+ "docs": "components/MultiSelect/MultiSelect.md",
97
+ "source": "components/MultiSelect/MultiSelect.vue",
98
+ "keywords": [
99
+ "multiselect",
100
+ "multi-select",
101
+ "autocomplete",
102
+ "combobox",
103
+ "tags",
104
+ "chips",
105
+ "filter",
106
+ "search",
107
+ "typeahead"
108
+ ]
109
+ },
79
110
  {
80
111
  "name": "NumberInput",
81
112
  "slug": "number-input",
@@ -105,7 +136,15 @@
105
136
  "slug": "select-box",
106
137
  "docs": "components/SelectBox/SelectBox.md",
107
138
  "source": "components/SelectBox/SelectBox.vue",
108
- "keywords": []
139
+ "keywords": [
140
+ "select",
141
+ "dropdown",
142
+ "autocomplete",
143
+ "combobox",
144
+ "filter",
145
+ "search",
146
+ "typeahead"
147
+ ]
109
148
  },
110
149
  {
111
150
  "name": "SidebarLayout",
@@ -134,6 +173,21 @@
134
173
  "docs": "components/Toggle/Toggle.md",
135
174
  "source": "components/Toggle/Toggle.vue",
136
175
  "keywords": []
176
+ },
177
+ {
178
+ "name": "ToggleGroup",
179
+ "slug": "toggle-group",
180
+ "docs": "components/ToggleGroup/ToggleGroup.md",
181
+ "source": "components/ToggleGroup/ToggleGroup.vue",
182
+ "keywords": [
183
+ "toggle group",
184
+ "segmented control",
185
+ "single select",
186
+ "multi select",
187
+ "button group",
188
+ "radio",
189
+ "pressed"
190
+ ]
137
191
  }
138
192
  ],
139
193
  "charts": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfasim-ui/docs",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "LLM-friendly component and chart documentation for cfasim-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,51 +0,0 @@
1
- /* Shared chart styles for the Expand / fullscreen menu item.
2
- Bundled into @cfasim-ui/charts/style.css via index.ts so it ships
3
- once for the whole package instead of repeating in every chart SFC. */
4
-
5
- .line-chart-wrapper:hover .chart-menu-button,
6
- .bar-chart-wrapper:hover .chart-menu-button,
7
- .choropleth-wrapper:hover .chart-menu-button,
8
- .line-chart-wrapper:focus-within .chart-menu-button,
9
- .bar-chart-wrapper:focus-within .chart-menu-button,
10
- .choropleth-wrapper:focus-within .chart-menu-button,
11
- .line-chart-wrapper.is-fullscreen .chart-menu-button,
12
- .bar-chart-wrapper.is-fullscreen .chart-menu-button,
13
- .choropleth-wrapper.is-fullscreen .chart-menu-button {
14
- opacity: 1;
15
- }
16
-
17
- .line-chart-wrapper.is-fullscreen,
18
- .bar-chart-wrapper.is-fullscreen,
19
- .choropleth-wrapper.is-fullscreen {
20
- position: fixed;
21
- inset: 0;
22
- z-index: var(--cfasim-z-fullscreen, 1000);
23
- background: var(--color-bg-0, #fff);
24
- color: var(--color-text, inherit);
25
- padding: 2em;
26
- box-sizing: border-box;
27
- display: flex;
28
- flex-direction: column;
29
- justify-content: center;
30
- }
31
-
32
- /* ChoroplethMap doesn't go through useChartFoundation, so its SVG keeps
33
- its prop-driven width/height. Stretch it to fill the expanded box. */
34
- .choropleth-wrapper.is-fullscreen svg {
35
- flex: 1 1 auto;
36
- min-height: 0;
37
- height: 100%;
38
- width: 100%;
39
- }
40
-
41
- .chart-sr-only {
42
- position: absolute;
43
- width: 1px;
44
- height: 1px;
45
- padding: 0;
46
- margin: -1px;
47
- overflow: hidden;
48
- clip: rect(0, 0, 0, 0);
49
- white-space: nowrap;
50
- border: 0;
51
- }