@getdom/studio 0.2.2 → 0.2.4
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.
- package/CHANGELOG.md +84 -0
- package/package.json +1 -1
- package/src/pages/_layout/componentCatalog.generated.js +114 -12
- package/src/pages/charts/_shared/CompactChartTooltip.vue +39 -0
- package/src/pages/charts/_shared/XYChart.vue +7 -2
- package/src/pages/charts/_shared/XYRenderer.vue +18 -11
- package/src/pages/charts/sparkline/DomSparkline.vue +1 -0
- package/src/pages/forms/_shared/DomOptionStatus.vue +18 -0
- package/src/pages/forms/_shared/useOptionMenu.js +18 -2
- package/src/pages/forms/_shared/useOptionPaging.js +42 -0
- package/src/pages/forms/_shared/useSelectedOptions.js +25 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +36 -8
- package/src/pages/forms/combobox/DomCombobox.vue +44 -7
- package/src/pages/forms/font-picker/DomFontPicker.vue +416 -0
- package/src/pages/forms/font-picker/FontPreview.vue +39 -0
- package/src/pages/forms/font-picker/fontPicker.js +102 -0
- package/src/pages/forms/font-picker/googleFonts.js +1951 -0
- package/src/pages/forms/select/DomSelect.vue +118 -29
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +67 -15
- package/src/pages/lib/headless/autocomplete.js +5 -0
- package/src/pages/lib/headless/combobox.js +129 -25
- package/src/pages/lib/headless/dropdown.js +3 -2
- package/src/pages/lib/headless/listbox.d.ts +1 -0
- package/src/pages/lib/headless/listbox.js +41 -5
- package/src/pages/lib/vue/index.d.ts +137 -3
- package/src/pages/lib/vue/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4 - 2026-10-08
|
|
4
|
+
|
|
5
|
+
Reliable chart hover values and optional sparkline tooltips.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Opt-in sparkline tooltips with `tooltip`, showing the inspected label and
|
|
10
|
+
formatted series values on hover or keyboard navigation. The floating panel
|
|
11
|
+
stays outside the compact plot and escapes scrolling-table clipping.
|
|
12
|
+
- Website privacy policy and terms pages, linked from checkout and the footer.
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- Documentation explains how component source, runnable examples and reference
|
|
17
|
+
pages live together, and how that structure supports human and AI development.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Line and area chart tooltips update as the pointer moves between data points,
|
|
22
|
+
without requiring a click. Clicking preserves and selects the hovered record.
|
|
23
|
+
- Shared chart bindings forward Unovis configuration using its expected names,
|
|
24
|
+
including crosshair callbacks and stacked-series inspection.
|
|
25
|
+
- Chart values dismiss when the crosshair leaves the plot.
|
|
26
|
+
- Website licence checkout no longer depends on email delivery being configured.
|
|
27
|
+
Existing installations receive a repair migration for missing licence columns.
|
|
28
|
+
|
|
29
|
+
### Upgrade notes
|
|
30
|
+
|
|
31
|
+
- Sparkline tooltips remain off by default. Add `tooltip` to `DomSparkline` to
|
|
32
|
+
enable them; existing value and label formatters also apply to the tooltip.
|
|
33
|
+
- Email-based licence claiming and recovery still require a configured sender.
|
|
34
|
+
|
|
35
|
+
## 0.2.3 - 2026-09-27
|
|
36
|
+
|
|
37
|
+
Searchable Google Fonts previews, shared paginated option lists, and more reliable
|
|
38
|
+
selection controls.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `DomFontPicker` with searchable family previews, incremental browsing and a
|
|
43
|
+
lightweight snapshot of 1,946 Google Fonts families. Font files are not bundled;
|
|
44
|
+
the mounted picker requests only selected and visible preview families, with
|
|
45
|
+
text subsetting and component-owned stylesheet cleanup.
|
|
46
|
+
- Multiple selection in `DomSelect`, with an array model, checked options and a
|
|
47
|
+
clear-all action. Selected labels are retained when searches replace remote pages.
|
|
48
|
+
- Shared incremental loading for Select, Combobox, Autocomplete and Tag combobox,
|
|
49
|
+
using `hasMore` and `load-more` for scroll, keyboard and explicit-button paging.
|
|
50
|
+
Documentation includes real HTTP examples with retry and stale-request handling.
|
|
51
|
+
- Query and selected-state slot data for custom option rendering, plus a matched-text
|
|
52
|
+
highlighting example that keeps HTML-like labels as plain text.
|
|
53
|
+
|
|
54
|
+
### Improved
|
|
55
|
+
|
|
56
|
+
- Opening a picker shows the loaded choices without treating its committed value
|
|
57
|
+
as a permanent search filter. Selected checkmarks remain distinct from keyboard
|
|
58
|
+
navigation, and loaded single selections open in context.
|
|
59
|
+
- Select and Tag combobox documentation explains compact multi-selection versus
|
|
60
|
+
removable tokens, optional custom entries and paste support, alongside their
|
|
61
|
+
shared option, field-state and paging foundations.
|
|
62
|
+
- Public TypeScript declarations cover the font picker, multiple selection,
|
|
63
|
+
retained selected records and paginated option lists.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Dropdowns reopen reliably while an earlier close animation finishes, and late
|
|
68
|
+
option updates no longer reopen a dismissed combobox or autocomplete.
|
|
69
|
+
- Selected options remain visible and checked in combobox and tag suggestions.
|
|
70
|
+
Disabled and read-only controls do not commit selections through their action buttons.
|
|
71
|
+
- Select keeps its arrow at the right edge and centers the clear button to its
|
|
72
|
+
left. Long selections truncate within narrow containers, and clearing returns
|
|
73
|
+
keyboard focus to the trigger.
|
|
74
|
+
|
|
75
|
+
### Upgrade notes
|
|
76
|
+
|
|
77
|
+
- Single-select models remain scalar values. Use an array model when enabling
|
|
78
|
+
`multiple`; Tag combobox continues to use an array of selected values.
|
|
79
|
+
- For server-filtered lists, set `filterOptions` to false and handle `query` and
|
|
80
|
+
`load-more` in the application. Supply `selectedOptions` for initial selected
|
|
81
|
+
records outside the loaded page. Seeking directly to a later remote page remains
|
|
82
|
+
application-owned; controls do not fetch or paginate an endpoint themselves.
|
|
83
|
+
- Google Fonts previews need no API key. Applications own loading the selected
|
|
84
|
+
font for content outside the picker. A live Developer API catalogue should be
|
|
85
|
+
fetched and cached server-side or at build time, with its key kept out of client code.
|
|
86
|
+
|
|
3
87
|
## 0.2.2 - 2026-09-26
|
|
4
88
|
|
|
5
89
|
More flexible calendar highlights, refined agent chat, and updated website
|
package/package.json
CHANGED
|
@@ -3090,8 +3090,8 @@ export const componentCatalog = [
|
|
|
3090
3090
|
"slots": [
|
|
3091
3091
|
{
|
|
3092
3092
|
"name": "item",
|
|
3093
|
-
"payload": "{ item, index }",
|
|
3094
|
-
"description": "Replaces
|
|
3093
|
+
"payload": "{ item, index, selected, query }",
|
|
3094
|
+
"description": "Replaces each suggestion body without replacing the selection checkmark."
|
|
3095
3095
|
}
|
|
3096
3096
|
],
|
|
3097
3097
|
"events": [
|
|
@@ -3109,6 +3109,19 @@ export const componentCatalog = [
|
|
|
3109
3109
|
"name": "commit",
|
|
3110
3110
|
"payload": "({ value, label, custom })",
|
|
3111
3111
|
"description": "Fired when the current text is committed with Enter."
|
|
3112
|
+
},
|
|
3113
|
+
{
|
|
3114
|
+
"name": "load-more",
|
|
3115
|
+
"payload": "{ query }",
|
|
3116
|
+
"description": "Request the next page when hasMore is true."
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
"name": "open",
|
|
3120
|
+
"description": "Fired when suggestions open."
|
|
3121
|
+
},
|
|
3122
|
+
{
|
|
3123
|
+
"name": "close",
|
|
3124
|
+
"description": "Fired when suggestions close."
|
|
3112
3125
|
}
|
|
3113
3126
|
]
|
|
3114
3127
|
},
|
|
@@ -3308,8 +3321,8 @@ export const componentCatalog = [
|
|
|
3308
3321
|
"slots": [
|
|
3309
3322
|
{
|
|
3310
3323
|
"name": "item",
|
|
3311
|
-
"payload": "{ item, index }",
|
|
3312
|
-
"description": "Replaces
|
|
3324
|
+
"payload": "{ item, index, selected, query }",
|
|
3325
|
+
"description": "Replaces each option body. The control keeps the selected checkmark."
|
|
3313
3326
|
}
|
|
3314
3327
|
],
|
|
3315
3328
|
"events": [
|
|
@@ -3322,6 +3335,24 @@ export const componentCatalog = [
|
|
|
3322
3335
|
"name": "select",
|
|
3323
3336
|
"payload": "({ item, value, label })",
|
|
3324
3337
|
"description": "Fired when an option is selected. item is the original option object."
|
|
3338
|
+
},
|
|
3339
|
+
{
|
|
3340
|
+
"name": "list-scroll",
|
|
3341
|
+
"payload": "(event: Event)",
|
|
3342
|
+
"description": "Fired when the floating option list scrolls."
|
|
3343
|
+
},
|
|
3344
|
+
{
|
|
3345
|
+
"name": "load-more",
|
|
3346
|
+
"payload": "{ query }",
|
|
3347
|
+
"description": "Request the next page when hasMore is true. The parent owns fetching and appending options."
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
"name": "open",
|
|
3351
|
+
"description": "Fired when the options open with an empty transient query."
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
"name": "close",
|
|
3355
|
+
"description": "Fired when the options close."
|
|
3325
3356
|
}
|
|
3326
3357
|
]
|
|
3327
3358
|
},
|
|
@@ -3637,6 +3668,49 @@ export const componentCatalog = [
|
|
|
3637
3668
|
},
|
|
3638
3669
|
"studio": {}
|
|
3639
3670
|
},
|
|
3671
|
+
{
|
|
3672
|
+
"id": "forms/font-picker",
|
|
3673
|
+
"path": "../forms/font-picker/DomFontPicker.vue",
|
|
3674
|
+
"section": "forms",
|
|
3675
|
+
"slug": "font-picker",
|
|
3676
|
+
"exportName": "DomFontPicker",
|
|
3677
|
+
"route": "/forms/font-picker",
|
|
3678
|
+
"doc": {
|
|
3679
|
+
"name": "Font picker",
|
|
3680
|
+
"tag": "<DomFontPicker>",
|
|
3681
|
+
"description": "Searchable Google Fonts picker that previews only visible families and keeps font network loading inside the mounted control.",
|
|
3682
|
+
"icon": "M5 5h14M12 5v14M8 19h8",
|
|
3683
|
+
"playground": {
|
|
3684
|
+
"initial": {
|
|
3685
|
+
"label": "Font family",
|
|
3686
|
+
"modelValue": "Lato",
|
|
3687
|
+
"placeholder": "Search Google Fonts",
|
|
3688
|
+
"category": "",
|
|
3689
|
+
"pageSize": 24,
|
|
3690
|
+
"previewText": "",
|
|
3691
|
+
"loadFonts": true
|
|
3692
|
+
}
|
|
3693
|
+
},
|
|
3694
|
+
"events": [
|
|
3695
|
+
{
|
|
3696
|
+
"name": "update:modelValue",
|
|
3697
|
+
"payload": "string",
|
|
3698
|
+
"description": "Fired with the selected font value."
|
|
3699
|
+
},
|
|
3700
|
+
{
|
|
3701
|
+
"name": "select",
|
|
3702
|
+
"payload": "{ item, value, label }",
|
|
3703
|
+
"description": "Fired when a font family is selected."
|
|
3704
|
+
},
|
|
3705
|
+
{
|
|
3706
|
+
"name": "query",
|
|
3707
|
+
"payload": "string",
|
|
3708
|
+
"description": "Fired as the user searches the catalog."
|
|
3709
|
+
}
|
|
3710
|
+
]
|
|
3711
|
+
},
|
|
3712
|
+
"studio": {}
|
|
3713
|
+
},
|
|
3640
3714
|
{
|
|
3641
3715
|
"id": "forms/form",
|
|
3642
3716
|
"path": "../forms/form/DomForm.vue",
|
|
@@ -4053,13 +4127,13 @@ export const componentCatalog = [
|
|
|
4053
4127
|
"slots": [
|
|
4054
4128
|
{
|
|
4055
4129
|
"name": "value",
|
|
4056
|
-
"payload": "{ option, value, label, placeholder }",
|
|
4057
|
-
"description": "Custom selected-value markup inside the trigger."
|
|
4130
|
+
"payload": "{ option, options, value, label, placeholder }",
|
|
4131
|
+
"description": "Custom selected-value markup inside the trigger. options contains all selected records in multiple mode."
|
|
4058
4132
|
},
|
|
4059
4133
|
{
|
|
4060
4134
|
"name": "option",
|
|
4061
|
-
"payload": "{ option, index, selected, optionId }",
|
|
4062
|
-
"description": "Custom option markup
|
|
4135
|
+
"payload": "{ option, index, selected, query, optionId }",
|
|
4136
|
+
"description": "Custom option markup with the transient query for safe text highlighting. optionId identifies the focusable row."
|
|
4063
4137
|
},
|
|
4064
4138
|
{
|
|
4065
4139
|
"name": "group",
|
|
@@ -4070,13 +4144,36 @@ export const componentCatalog = [
|
|
|
4070
4144
|
"events": [
|
|
4071
4145
|
{
|
|
4072
4146
|
"name": "update:modelValue",
|
|
4073
|
-
"payload": "
|
|
4074
|
-
"description": "Emitted
|
|
4147
|
+
"payload": "string | number | Array<string | number>",
|
|
4148
|
+
"description": "Emitted with one committed value or an array in multiple mode."
|
|
4075
4149
|
},
|
|
4076
4150
|
{
|
|
4077
4151
|
"name": "select",
|
|
4078
4152
|
"payload": "({ option, value, label })",
|
|
4079
4153
|
"description": "Emitted with the selected option record."
|
|
4154
|
+
},
|
|
4155
|
+
{
|
|
4156
|
+
"name": "query",
|
|
4157
|
+
"payload": "string",
|
|
4158
|
+
"description": "Emitted on open with an empty query and as the user types in the separate search input."
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
"name": "load-more",
|
|
4162
|
+
"payload": "{ query }",
|
|
4163
|
+
"description": "Request another page when hasMore is true. The application owns cursors, requests, errors and appending options."
|
|
4164
|
+
},
|
|
4165
|
+
{
|
|
4166
|
+
"name": "list-scroll",
|
|
4167
|
+
"payload": "Event",
|
|
4168
|
+
"description": "Scroll event for optional visibility-based previews."
|
|
4169
|
+
},
|
|
4170
|
+
{
|
|
4171
|
+
"name": "open",
|
|
4172
|
+
"description": "Emitted when the panel opens."
|
|
4173
|
+
},
|
|
4174
|
+
{
|
|
4175
|
+
"name": "close",
|
|
4176
|
+
"description": "Emitted when the panel closes."
|
|
4080
4177
|
}
|
|
4081
4178
|
],
|
|
4082
4179
|
"keyboard": [
|
|
@@ -4129,8 +4226,8 @@ export const componentCatalog = [
|
|
|
4129
4226
|
"slots": [
|
|
4130
4227
|
{
|
|
4131
4228
|
"name": "item",
|
|
4132
|
-
"payload": "{ item, index, active, custom }",
|
|
4133
|
-
"description": "Replaces each suggestion row."
|
|
4229
|
+
"payload": "{ item, index, active, selected, query, custom }",
|
|
4230
|
+
"description": "Replaces each suggestion row while retaining the selected checkmark."
|
|
4134
4231
|
},
|
|
4135
4232
|
{
|
|
4136
4233
|
"name": "tag",
|
|
@@ -4163,6 +4260,11 @@ export const componentCatalog = [
|
|
|
4163
4260
|
"name": "change",
|
|
4164
4261
|
"payload": "Array",
|
|
4165
4262
|
"description": "Fired after the selected values array changes."
|
|
4263
|
+
},
|
|
4264
|
+
{
|
|
4265
|
+
"name": "load-more",
|
|
4266
|
+
"payload": "{ query }",
|
|
4267
|
+
"description": "Request the next server page when hasMore is true."
|
|
4166
4268
|
}
|
|
4167
4269
|
]
|
|
4168
4270
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
|
+
import { applyFloatingPosition, autoUpdateFloating } from '../../lib/headless/floating.js';
|
|
4
|
+
import ChartTooltip from './ChartTooltip.vue';
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
anchor: { type: Object, required: true },
|
|
8
|
+
label: { type: String, default: '' },
|
|
9
|
+
items: { type: Array, default: () => [] },
|
|
10
|
+
});
|
|
11
|
+
const panel = ref(null);
|
|
12
|
+
let stopPositioning;
|
|
13
|
+
|
|
14
|
+
/** Positions the compact tooltip outside the plot and within the viewport. @returns {void} */
|
|
15
|
+
function position() {
|
|
16
|
+
applyFloatingPosition(props.anchor, panel.value, { placement: 'top', align: 'center', offset: 8, padding: 8 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Opens in the top layer so table overflow cannot clip chart values. @returns {void} */
|
|
20
|
+
function open() {
|
|
21
|
+
panel.value.showPopover();
|
|
22
|
+
position();
|
|
23
|
+
stopPositioning = autoUpdateFloating(props.anchor, panel.value, position);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Releases scroll/resize observers when inspection ends or the chart unmounts. @returns {void} */
|
|
27
|
+
function cleanup() {
|
|
28
|
+
stopPositioning?.();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMounted(open);
|
|
32
|
+
onBeforeUnmount(cleanup);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<div ref="panel" popover="manual" class="pointer-events-none m-0 w-max max-w-[calc(100vw-1rem)] overflow-visible border-0 bg-transparent p-0 text-canvas-fg">
|
|
37
|
+
<ChartTooltip :label="label" :items="items" />
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
@@ -5,6 +5,7 @@ import { chartPoints, chartSeries, formatChartValue, percentagePoints } from './
|
|
|
5
5
|
import ChartContainer from './ChartContainer.vue';
|
|
6
6
|
import ChartLegend from './ChartLegend.vue';
|
|
7
7
|
import ChartTooltip from './ChartTooltip.vue';
|
|
8
|
+
import CompactChartTooltip from './CompactChartTooltip.vue';
|
|
8
9
|
|
|
9
10
|
const props = defineProps({
|
|
10
11
|
...chartProps,
|
|
@@ -13,12 +14,14 @@ const props = defineProps({
|
|
|
13
14
|
percentage: { type: Boolean, default: false },
|
|
14
15
|
horizontal: { type: Boolean, default: false },
|
|
15
16
|
compact: { type: Boolean, default: false },
|
|
17
|
+
tooltip: { type: Boolean, default: true },
|
|
16
18
|
});
|
|
17
19
|
const emit = defineEmits(['retry', 'select']);
|
|
18
20
|
const renderer = shallowRef(null);
|
|
19
21
|
const loadError = ref('');
|
|
20
22
|
const hiddenKeys = ref([]);
|
|
21
23
|
const inspected = ref(null);
|
|
24
|
+
const plot = ref(null);
|
|
22
25
|
let disposed = false;
|
|
23
26
|
const allSeries = computed(() => chartSeries(props.series));
|
|
24
27
|
const visibleSeries = computed(() => allSeries.value.filter((item) => !hiddenKeys.value.includes(item.key)));
|
|
@@ -108,16 +111,18 @@ function scrub(event) { inspected.value = points.value[Number(event.target.value
|
|
|
108
111
|
<template>
|
|
109
112
|
<ChartContainer :title="title" :description="description" :height="safeHeight" :compact="compact" :loading="loading" :error="error || validationError || loadError" :empty="!hasData" :empty-label="emptyLabel" @retry="retry">
|
|
110
113
|
<div
|
|
114
|
+
ref="plot"
|
|
111
115
|
class="relative rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
112
116
|
:style="{ height: `${safeHeight}px` }" tabindex="0" role="group"
|
|
113
117
|
:aria-label="`${title}. Use arrow keys to inspect values; Enter selects a row.`"
|
|
114
|
-
@keydown="navigate" @focus="inspect(points[0])" @blur="inspect(null)" @mouseleave="inspect(null)" @click="selectPoint"
|
|
118
|
+
@keydown="navigate" @focus="inspect(inspected || points[0])" @blur="inspect(null)" @mouseleave="inspect(null)" @click="selectPoint"
|
|
115
119
|
>
|
|
116
120
|
<div aria-hidden="true">
|
|
117
121
|
<component :is="renderer" v-if="renderer" :key="`${kind}-${horizontal}-${stacked}-${percentage}`" :points="renderPoints" :series="visibleSeries" :kind="kind" :height="safeHeight" :curve="curve" :stacked="stacked || percentage" :percentage="percentage" :horizontal="horizontal" :compact="compact" :grid="grid" :x-type="xType" :format-x="formatAxisLabel" :format-y="formatAxisValue" @inspect="inspect" />
|
|
118
122
|
<div v-else class="h-full w-full" />
|
|
119
123
|
</div>
|
|
120
|
-
<
|
|
124
|
+
<CompactChartTooltip v-if="inspected && tooltip && compact" :anchor="plot" :label="formatLabel(inspected.row[xKey])" :items="tooltipItems" />
|
|
125
|
+
<ChartTooltip v-else-if="inspected && tooltip" class="absolute right-2 top-2 z-10" :label="formatLabel(inspected.row[xKey])" :items="tooltipItems" />
|
|
121
126
|
<span class="sr-only" aria-live="polite">{{ inspected ? `${formatLabel(inspected.row[xKey])}: ${tooltipItems.map(item => `${item.label} ${item.value}`).join(', ')}` : '' }}</span>
|
|
122
127
|
</div>
|
|
123
128
|
<template #legend>
|
|
@@ -56,35 +56,42 @@ function getX(point) { return point.x; }
|
|
|
56
56
|
/** @param {unknown} _datum Unovis datum. @param {number} index Series index. @returns {string} Stable colour. */
|
|
57
57
|
function getColor(_datum, index) { return props.series[index]?.color || 'var(--primary)'; }
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Synchronizes inspection with the crosshair, including plot-boundary dismissal.
|
|
61
|
+
* Unovis can retain the nearest datum while clearing the pointer coordinate.
|
|
62
|
+
* @param {number|Date|undefined} x Pointer coordinate; absent when hidden.
|
|
63
|
+
* @param {object|undefined} point Nearest datum.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
function inspectPoint(x, point) { emit('inspect', x == null ? null : point || null); }
|
|
61
67
|
|
|
62
68
|
/** @param {object} datum Bar or stack datum. @returns {void} Inspects horizontal and touch-selected bars. */
|
|
63
69
|
function inspectBar(datum) { emit('inspect', datum.datum || datum); }
|
|
64
70
|
</script>
|
|
65
71
|
|
|
66
72
|
<template>
|
|
73
|
+
<!-- Unovis 1.7 forwards undeclared attributes verbatim; config bindings must use camelCase. -->
|
|
67
74
|
<VisXYContainer
|
|
68
75
|
:data="points" :height="height" :duration="0" class="block w-full"
|
|
69
|
-
:
|
|
70
|
-
:
|
|
76
|
+
:xDomain="horizontal ? valueDomain : undefined" :yDomain="horizontal ? undefined : valueDomain"
|
|
77
|
+
:yDirection="horizontal ? 'south' : 'north'"
|
|
71
78
|
:margin="compact ? { top: 4, bottom: 4, left: 4, right: 4 } : { top: 12, right: 12 }"
|
|
72
79
|
>
|
|
73
80
|
<template v-if="kind === 'line'">
|
|
74
|
-
<VisLine v-for="(item, index) in series" :key="item.key" :x="getX" :y="accessors[index]" :color="item.color" :
|
|
81
|
+
<VisLine v-for="(item, index) in series" :key="item.key" :x="getX" :y="accessors[index]" :color="item.color" :curveType="curve" :lineWidth="2" :lineDashArray="item.dashed ? [5, 4] : undefined" :duration="0" />
|
|
75
82
|
<VisScatter v-if="points.length === 1" :x="getX" :y="accessors" :color="getColor" :size="24" :duration="0" />
|
|
76
83
|
</template>
|
|
77
84
|
<template v-else-if="kind === 'area'">
|
|
78
|
-
<VisArea v-if="stacked" :x="getX" :y="accessors" :color="getColor" :
|
|
79
|
-
<VisArea v-for="(item, index) in stacked ? [] : series" :key="item.key" :x="getX" :y="accessors[index]" :color="item.color" :
|
|
85
|
+
<VisArea v-if="stacked" :x="getX" :y="accessors" :color="getColor" :curveType="curve" :opacity="0.45" :duration="0" />
|
|
86
|
+
<VisArea v-for="(item, index) in stacked ? [] : series" :key="item.key" :x="getX" :y="accessors[index]" :color="item.color" :curveType="curve" :opacity="0.15" :line="true" :lineColor="item.color" :duration="0" />
|
|
80
87
|
</template>
|
|
81
88
|
<component
|
|
82
89
|
:is="stacked ? VisStackedBar : VisGroupedBar" v-else
|
|
83
90
|
:x="getX" :y="accessors" :color="getColor" :orientation="horizontal ? 'horizontal' : 'vertical'"
|
|
84
|
-
:
|
|
91
|
+
:barPadding="stacked ? 0.3 : 0.1" :groupPadding="0.3" :roundedCorners="3" :barMinHeight="0" :duration="0" :events="barEvents"
|
|
85
92
|
/>
|
|
86
|
-
<VisAxis v-if="!compact" :key="`category-${horizontal}`" :type="horizontal ? 'y' : 'x'" :
|
|
87
|
-
<VisAxis v-if="!compact" :key="`value-${horizontal}`" :type="horizontal ? 'x' : 'y'" :
|
|
88
|
-
<VisCrosshair v-if="!horizontal" :x="getX" :y="stacked ? [] : accessors" :
|
|
93
|
+
<VisAxis v-if="!compact" :key="`category-${horizontal}`" :type="horizontal ? 'y' : 'x'" :tickValues="categoryTicks" :tickFormat="formatX" :gridLine="false" :domainLine="false" :tickLine="false" :tickTextWidth="horizontal ? 90 : 72" :tickTextHideOverlapping="true" :tickTextAdaptiveSets="true" :duration="0" />
|
|
94
|
+
<VisAxis v-if="!compact" :key="`value-${horizontal}`" :type="horizontal ? 'x' : 'y'" :tickFormat="formatY" :gridLine="grid" :domainLine="false" :tickLine="false" :tickTextWidth="90" :tickTextHideOverlapping="true" :numTicks="4" :duration="0" />
|
|
95
|
+
<VisCrosshair v-if="!horizontal" :x="getX" :y="stacked ? [] : accessors" :yStacked="stacked ? accessors : undefined" :color="getColor" :hideWhenFarFromPointer="false" :onCrosshairMove="inspectPoint" :duration="0" />
|
|
89
96
|
</VisXYContainer>
|
|
90
97
|
</template>
|
|
@@ -21,6 +21,7 @@ const props = defineProps({
|
|
|
21
21
|
height: { type: Number, default: 48 },
|
|
22
22
|
legend: { type: Boolean, default: false },
|
|
23
23
|
showTable: { type: Boolean, default: false },
|
|
24
|
+
tooltip: { type: Boolean, default: false, _edit: { description: 'Show formatted values on hover or keyboard inspection in a floating tooltip outside the compact plot.' } },
|
|
24
25
|
});
|
|
25
26
|
defineEmits(['select', 'retry']);
|
|
26
27
|
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
as: { type: String, default: 'div' },
|
|
4
|
+
loading: Boolean,
|
|
5
|
+
hasMore: Boolean,
|
|
6
|
+
empty: Boolean,
|
|
7
|
+
emptyText: { type: String, default: 'No options found' },
|
|
8
|
+
});
|
|
9
|
+
defineEmits(['load-more']);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<component :is="as" v-if="loading || hasMore || empty" role="presentation" class="px-3 py-3 text-center text-sm text-muted-fg">
|
|
14
|
+
<span v-if="loading" role="status">Loading options…</span>
|
|
15
|
+
<button v-else-if="hasMore" type="button" class="rounded-lg px-3 py-1 underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-ring" @mousedown.prevent @click.stop="$emit('load-more')">Load more options</button>
|
|
16
|
+
<span v-else role="status">{{ emptyText }}</span>
|
|
17
|
+
</component>
|
|
18
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { computed, ref, watch } from 'vue';
|
|
1
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
2
2
|
|
|
3
|
+
/** Manage transient token-query navigation independently from committed selections. */
|
|
3
4
|
export function useOptionMenu(items, options = {}) {
|
|
4
5
|
const {
|
|
5
6
|
emitQuery,
|
|
@@ -11,6 +12,9 @@ export function useOptionMenu(items, options = {}) {
|
|
|
11
12
|
const isOpen = ref(false);
|
|
12
13
|
const activeIndex = ref(0);
|
|
13
14
|
const trimmedQuery = computed(() => query.value.trim());
|
|
15
|
+
let closeTimer = null;
|
|
16
|
+
onBeforeUnmount(() => clearTimeout(closeTimer));
|
|
17
|
+
watch(isOpen, (open) => { if (open) clearTimeout(closeTimer); });
|
|
14
18
|
|
|
15
19
|
watch(query, (value) => {
|
|
16
20
|
emitQuery?.(value);
|
|
@@ -24,28 +28,39 @@ export function useOptionMenu(items, options = {}) {
|
|
|
24
28
|
}
|
|
25
29
|
});
|
|
26
30
|
|
|
31
|
+
/** Wrap keyboard navigation within the currently available entries. */
|
|
27
32
|
function wrapIndex(index) {
|
|
28
33
|
if (!items.value.length) return 0;
|
|
29
34
|
return (index + items.value.length) % items.value.length;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/** Open the menu and move its active descendant by one step. */
|
|
32
38
|
function move(delta) {
|
|
33
39
|
isOpen.value = true;
|
|
34
40
|
activeIndex.value = wrapIndex(activeIndex.value + delta);
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
/** Reset transient input without changing any committed tokens. */
|
|
37
44
|
function reset() {
|
|
45
|
+
clearTimeout(closeTimer);
|
|
38
46
|
query.value = '';
|
|
39
47
|
isOpen.value = false;
|
|
40
48
|
activeIndex.value = 0;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
/** Defer blur closure to allow a pointer-driven option click to commit. */
|
|
43
52
|
function closeSoon() {
|
|
44
|
-
|
|
53
|
+
clearTimeout(closeTimer);
|
|
54
|
+
closeTimer = window.setTimeout(() => {
|
|
45
55
|
isOpen.value = false;
|
|
46
56
|
}, closeDelay);
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
/** Cancel an outstanding blur when focus immediately returns to the input. */
|
|
60
|
+
function cancelClose() {
|
|
61
|
+
clearTimeout(closeTimer);
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
return {
|
|
50
65
|
query,
|
|
51
66
|
isOpen,
|
|
@@ -55,5 +70,6 @@ export function useOptionMenu(items, options = {}) {
|
|
|
55
70
|
move,
|
|
56
71
|
reset,
|
|
57
72
|
closeSoon,
|
|
73
|
+
cancelClose,
|
|
58
74
|
};
|
|
59
75
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { nextTick, watch } from 'vue';
|
|
2
|
+
import { booleanProp } from './options.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Share incremental-list requests without owning the application's cursor or fetch.
|
|
6
|
+
* One automatic request is allowed per query/result length; the explicit button
|
|
7
|
+
* can retry a failed page. Checking after render also fills short scroll viewports.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} props Reactive loading and hasMore props.
|
|
10
|
+
* @param {Function} emit Component event emitter.
|
|
11
|
+
* @param {Object} state Refs for list, options, query and open state.
|
|
12
|
+
* @returns {{ onScroll: Function, requestMore: Function }} Paging event handlers.
|
|
13
|
+
*/
|
|
14
|
+
export function useOptionPaging(props, emit, { list, options, query, open }) {
|
|
15
|
+
let requestedKey = null;
|
|
16
|
+
|
|
17
|
+
/** Request the next application-owned page, suppressing duplicate automatic requests. */
|
|
18
|
+
function requestMore(force = false) {
|
|
19
|
+
if (!open.value || booleanProp(props.loading) || !booleanProp(props.hasMore)) return;
|
|
20
|
+
const key = `${query.value}\u0000${options.value.length}`;
|
|
21
|
+
if (!force && requestedKey === key) return;
|
|
22
|
+
requestedKey = key;
|
|
23
|
+
emit('load-more', { query: query.value });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Ask for another page only while the open list is near its scroll boundary. */
|
|
27
|
+
function checkBoundary() {
|
|
28
|
+
const element = list.value;
|
|
29
|
+
if (!element || element.clientHeight <= 0) return;
|
|
30
|
+
if (element.scrollHeight - element.scrollTop - element.clientHeight <= 64) requestMore();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Forward scroll details for custom previews, then check the shared paging threshold. */
|
|
34
|
+
function onScroll(event) {
|
|
35
|
+
emit('list-scroll', event);
|
|
36
|
+
checkBoundary();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
watch(query, () => { requestedKey = null; });
|
|
40
|
+
watch([open, () => props.loading, () => props.hasMore, () => options.value.length], () => nextTick(checkBoundary));
|
|
41
|
+
return { onScroll, requestMore };
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { computed, shallowRef, watch } from 'vue';
|
|
2
|
+
import { normalizeOption } from './options.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Retain only selected records when a remote query replaces the current page.
|
|
6
|
+
* Applications can supply selectedOptions for initial values outside loaded pages;
|
|
7
|
+
* this does not insert, reorder or seek options in the application's result set.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('vue').ComputedRef<Array>} options Current normalized page records.
|
|
10
|
+
* @param {import('vue').ComputedRef<Array>} values Committed selected values.
|
|
11
|
+
* @param {Function} supplied Getter for optional externally resolved selected records.
|
|
12
|
+
* @returns {import('vue').ComputedRef<Array>} Resolved selections in model order.
|
|
13
|
+
*/
|
|
14
|
+
export function useSelectedOptions(options, values, supplied = () => []) {
|
|
15
|
+
const cache = shallowRef(new Map());
|
|
16
|
+
watch([options, values, supplied], () => {
|
|
17
|
+
const records = new Map(cache.value);
|
|
18
|
+
for (const row of [...supplied(), ...options.value]) {
|
|
19
|
+
const option = normalizeOption(row, { allowEmptyValue: true });
|
|
20
|
+
if (option) records.set(String(option.value), option);
|
|
21
|
+
}
|
|
22
|
+
cache.value = new Map(values.value.map((value) => [String(value), records.get(String(value))]));
|
|
23
|
+
}, { immediate: true, flush: 'sync' });
|
|
24
|
+
return computed(() => values.value.map((value) => cache.value.get(String(value)) || { value, label: String(value ?? '') }));
|
|
25
|
+
}
|