orio-ui 1.14.0 → 1.16.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.
- package/dist/module.json +1 -1
- package/dist/runtime/components/CheckBox.d.vue.ts +3 -2
- package/dist/runtime/components/CheckBox.vue +1 -2
- package/dist/runtime/components/CheckBox.vue.d.ts +3 -2
- package/dist/runtime/components/CheckboxGroup.d.vue.ts +35 -0
- package/dist/runtime/components/CheckboxGroup.vue +50 -0
- package/dist/runtime/components/CheckboxGroup.vue.d.ts +35 -0
- package/dist/runtime/components/ControlElement.d.vue.ts +13 -2
- package/dist/runtime/components/ControlElement.vue +12 -4
- package/dist/runtime/components/ControlElement.vue.d.ts +13 -2
- package/dist/runtime/components/RadioButton.d.vue.ts +32 -0
- package/dist/runtime/components/RadioButton.vue +100 -0
- package/dist/runtime/components/RadioButton.vue.d.ts +32 -0
- package/dist/runtime/composables/useUrlSync.d.ts +38 -0
- package/dist/runtime/composables/useUrlSync.js +37 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +13 -0
- package/dist/runtime/utils/urlParams.d.ts +52 -0
- package/dist/runtime/utils/urlParams.js +67 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -75,8 +75,7 @@ defineProps({
|
|
|
75
75
|
content: "";
|
|
76
76
|
width: calc(var(--box-size) * 0.3);
|
|
77
77
|
height: calc(var(--box-size) * 0.6);
|
|
78
|
-
position:
|
|
79
|
-
bottom: 0.1rem;
|
|
78
|
+
position: absolute;
|
|
80
79
|
border: solid var(--color-accent-ink);
|
|
81
80
|
border-width: 0 2px 2px 0;
|
|
82
81
|
transform: rotate(45deg);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ControlProps } from "./ControlElement.vue.js";
|
|
2
|
+
export interface CheckboxOption {
|
|
3
|
+
label: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface CheckboxGroupProps extends Omit<ControlProps, "appearance" | "group" | "id"> {
|
|
7
|
+
options?: CheckboxOption[];
|
|
8
|
+
}
|
|
9
|
+
type __VLS_Props = CheckboxGroupProps;
|
|
10
|
+
type __VLS_ModelProps = {
|
|
11
|
+
modelValue?: unknown[];
|
|
12
|
+
};
|
|
13
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
14
|
+
declare var __VLS_8: {};
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
default?: (props: typeof __VLS_8) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
"update:modelValue": (value: unknown[]) => any;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
21
|
+
"onUpdate:modelValue"?: ((value: unknown[]) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
error: string | null;
|
|
24
|
+
layout: import("./ControlElement.vue.js").ControlLayout;
|
|
25
|
+
size: import("./ControlElement.vue.js").ControlSize;
|
|
26
|
+
options: CheckboxOption[];
|
|
27
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
31
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
32
|
+
new (): {
|
|
33
|
+
$slots: S;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {} from "./ControlElement.vue";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
options: { type: Array, required: false, default: () => [] },
|
|
5
|
+
error: { type: [String, null], required: false, default: null },
|
|
6
|
+
label: { type: String, required: false },
|
|
7
|
+
layout: { type: String, required: false, default: "vertical" },
|
|
8
|
+
size: { type: String, required: false, default: "md" }
|
|
9
|
+
});
|
|
10
|
+
const modelValue = defineModel({ type: Array, ...{ default: () => [] } });
|
|
11
|
+
function isChecked(value) {
|
|
12
|
+
return modelValue.value.includes(value);
|
|
13
|
+
}
|
|
14
|
+
function toggle(value) {
|
|
15
|
+
if (isChecked(value)) {
|
|
16
|
+
modelValue.value = modelValue.value.filter((v) => v !== value);
|
|
17
|
+
} else {
|
|
18
|
+
modelValue.value = [...modelValue.value, value];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<orio-control-element group :label :layout :size :error class="group-control">
|
|
25
|
+
<div class="checkboxes">
|
|
26
|
+
<slot>
|
|
27
|
+
<orio-check-box
|
|
28
|
+
v-for="option in options"
|
|
29
|
+
:key="String(option.value)"
|
|
30
|
+
appearance="minimal"
|
|
31
|
+
:model-value="isChecked(option.value)"
|
|
32
|
+
@update:model-value="toggle(option.value)"
|
|
33
|
+
>
|
|
34
|
+
{{ option.label }}
|
|
35
|
+
</orio-check-box>
|
|
36
|
+
</slot>
|
|
37
|
+
</div>
|
|
38
|
+
</orio-control-element>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<style scoped>
|
|
42
|
+
.checkboxes {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.group-control.control.horizontal {
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ControlProps } from "./ControlElement.vue.js";
|
|
2
|
+
export interface CheckboxOption {
|
|
3
|
+
label: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface CheckboxGroupProps extends Omit<ControlProps, "appearance" | "group" | "id"> {
|
|
7
|
+
options?: CheckboxOption[];
|
|
8
|
+
}
|
|
9
|
+
type __VLS_Props = CheckboxGroupProps;
|
|
10
|
+
type __VLS_ModelProps = {
|
|
11
|
+
modelValue?: unknown[];
|
|
12
|
+
};
|
|
13
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
14
|
+
declare var __VLS_8: {};
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
default?: (props: typeof __VLS_8) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
19
|
+
"update:modelValue": (value: unknown[]) => any;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
21
|
+
"onUpdate:modelValue"?: ((value: unknown[]) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
error: string | null;
|
|
24
|
+
layout: import("./ControlElement.vue.js").ControlLayout;
|
|
25
|
+
size: import("./ControlElement.vue.js").ControlSize;
|
|
26
|
+
options: CheckboxOption[];
|
|
27
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
31
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
32
|
+
new (): {
|
|
33
|
+
$slots: S;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -9,10 +9,20 @@ export interface ControlProps {
|
|
|
9
9
|
* Error message to display below the control
|
|
10
10
|
*/
|
|
11
11
|
error?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Marks this control as a group (adds role="group" and aria-labelledby).
|
|
14
|
+
* The label renders as a <span> instead of <label>.
|
|
15
|
+
* Use for groups of related controls (e.g. CheckboxGroup).
|
|
16
|
+
*/
|
|
17
|
+
group?: boolean;
|
|
12
18
|
/**
|
|
13
19
|
* ID for the control's form element, auto-generated if not provided
|
|
14
20
|
*/
|
|
15
21
|
id?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Label text for the control (or legend text when group is true)
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
16
26
|
/**
|
|
17
27
|
* Label position relative to the control
|
|
18
28
|
*/
|
|
@@ -22,15 +32,16 @@ export interface ControlProps {
|
|
|
22
32
|
*/
|
|
23
33
|
size?: ControlSize;
|
|
24
34
|
}
|
|
25
|
-
declare var
|
|
35
|
+
declare var __VLS_7: {
|
|
26
36
|
id: string;
|
|
27
37
|
};
|
|
28
38
|
type __VLS_Slots = {} & {
|
|
29
|
-
default?: (props: typeof
|
|
39
|
+
default?: (props: typeof __VLS_7) => any;
|
|
30
40
|
};
|
|
31
41
|
declare const __VLS_base: import("vue").DefineComponent<ControlProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ControlProps> & Readonly<{}>, {
|
|
32
42
|
appearance: "normal" | "minimal";
|
|
33
43
|
error: string | null;
|
|
44
|
+
group: boolean;
|
|
34
45
|
id: string;
|
|
35
46
|
layout: ControlLayout;
|
|
36
47
|
size: ControlSize;
|
|
@@ -3,7 +3,9 @@ import { useId } from "vue";
|
|
|
3
3
|
const props = defineProps({
|
|
4
4
|
appearance: { type: String, required: false, default: "normal" },
|
|
5
5
|
error: { type: [String, null], required: false, default: null },
|
|
6
|
+
group: { type: Boolean, required: false, default: false },
|
|
6
7
|
id: { type: String, required: false, default: () => useId() },
|
|
8
|
+
label: { type: String, required: false },
|
|
7
9
|
layout: { type: String, required: false, default: "vertical" },
|
|
8
10
|
size: { type: String, required: false, default: "md" }
|
|
9
11
|
});
|
|
@@ -12,11 +14,17 @@ const props = defineProps({
|
|
|
12
14
|
<template>
|
|
13
15
|
<div
|
|
14
16
|
class="control"
|
|
15
|
-
:class="[appearance, layout, `size-${size}`, { 'has-error': error }]"
|
|
17
|
+
:class="[appearance, layout, `size-${size}`, { 'has-error': error, group }]"
|
|
18
|
+
v-bind="group ? { role: 'group', 'aria-labelledby': id } : {}"
|
|
16
19
|
>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
<component
|
|
21
|
+
:is="group ? 'span' : 'label'"
|
|
22
|
+
v-if="label"
|
|
23
|
+
class="control-label"
|
|
24
|
+
v-bind="group ? { id } : { for: id }"
|
|
25
|
+
>
|
|
26
|
+
{{ label }}
|
|
27
|
+
</component>
|
|
20
28
|
<div class="control-group">
|
|
21
29
|
<div class="slot-wrapper" v-bind="$attrs">
|
|
22
30
|
<slot :id />
|
|
@@ -9,10 +9,20 @@ export interface ControlProps {
|
|
|
9
9
|
* Error message to display below the control
|
|
10
10
|
*/
|
|
11
11
|
error?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Marks this control as a group (adds role="group" and aria-labelledby).
|
|
14
|
+
* The label renders as a <span> instead of <label>.
|
|
15
|
+
* Use for groups of related controls (e.g. CheckboxGroup).
|
|
16
|
+
*/
|
|
17
|
+
group?: boolean;
|
|
12
18
|
/**
|
|
13
19
|
* ID for the control's form element, auto-generated if not provided
|
|
14
20
|
*/
|
|
15
21
|
id?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Label text for the control (or legend text when group is true)
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
16
26
|
/**
|
|
17
27
|
* Label position relative to the control
|
|
18
28
|
*/
|
|
@@ -22,15 +32,16 @@ export interface ControlProps {
|
|
|
22
32
|
*/
|
|
23
33
|
size?: ControlSize;
|
|
24
34
|
}
|
|
25
|
-
declare var
|
|
35
|
+
declare var __VLS_7: {
|
|
26
36
|
id: string;
|
|
27
37
|
};
|
|
28
38
|
type __VLS_Slots = {} & {
|
|
29
|
-
default?: (props: typeof
|
|
39
|
+
default?: (props: typeof __VLS_7) => any;
|
|
30
40
|
};
|
|
31
41
|
declare const __VLS_base: import("vue").DefineComponent<ControlProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ControlProps> & Readonly<{}>, {
|
|
32
42
|
appearance: "normal" | "minimal";
|
|
33
43
|
error: string | null;
|
|
44
|
+
group: boolean;
|
|
34
45
|
id: string;
|
|
35
46
|
layout: ControlLayout;
|
|
36
47
|
size: ControlSize;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RadioButtonProps {
|
|
2
|
+
/** The value this radio represents; compared to v-model to determine checked state */
|
|
3
|
+
value?: unknown;
|
|
4
|
+
/** HTML name attribute — groups radios together so only one is selected at a time */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Inline label text (alternative to default slot) */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Visually hides the label while keeping it accessible to SR (screen readers) */
|
|
9
|
+
hideLabel?: boolean;
|
|
10
|
+
}
|
|
11
|
+
type __VLS_Props = RadioButtonProps;
|
|
12
|
+
type __VLS_ModelProps = {
|
|
13
|
+
modelValue?: unknown;
|
|
14
|
+
};
|
|
15
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
16
|
+
declare var __VLS_8: {};
|
|
17
|
+
type __VLS_Slots = {} & {
|
|
18
|
+
default?: (props: typeof __VLS_8) => any;
|
|
19
|
+
};
|
|
20
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
21
|
+
"update:modelValue": (value: unknown) => any;
|
|
22
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
23
|
+
"onUpdate:modelValue"?: ((value: unknown) => any) | undefined;
|
|
24
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
26
|
+
declare const _default: typeof __VLS_export;
|
|
27
|
+
export default _default;
|
|
28
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
29
|
+
new (): {
|
|
30
|
+
$slots: S;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const modelValue = defineModel({ type: null });
|
|
3
|
+
defineProps({
|
|
4
|
+
value: { type: null, required: false },
|
|
5
|
+
name: { type: String, required: false },
|
|
6
|
+
label: { type: String, required: false },
|
|
7
|
+
hideLabel: { type: Boolean, required: false }
|
|
8
|
+
});
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<orio-control-element class="radio">
|
|
13
|
+
<label class="radio-label">
|
|
14
|
+
<input
|
|
15
|
+
v-model="modelValue"
|
|
16
|
+
type="radio"
|
|
17
|
+
:name="name"
|
|
18
|
+
:value="value"
|
|
19
|
+
class="radio-input"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
/>
|
|
22
|
+
<span class="radio-box" />
|
|
23
|
+
<span
|
|
24
|
+
v-if="label || $slots.default"
|
|
25
|
+
class="radio-text"
|
|
26
|
+
:class="{ 'sr-only': hideLabel }"
|
|
27
|
+
>
|
|
28
|
+
<slot>{{ label }}</slot>
|
|
29
|
+
</span>
|
|
30
|
+
</label>
|
|
31
|
+
</orio-control-element>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.radio {
|
|
36
|
+
--box-size: var(--control-icon-size, 1rem);
|
|
37
|
+
}
|
|
38
|
+
.radio-label {
|
|
39
|
+
position: relative;
|
|
40
|
+
user-select: none;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: var(--control-gap, 0.4rem);
|
|
44
|
+
color: var(--color-text);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.radio-input {
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset: 0;
|
|
50
|
+
width: var(--box-size);
|
|
51
|
+
height: 1rem;
|
|
52
|
+
margin: 0;
|
|
53
|
+
opacity: 0;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
.radio-box {
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
width: var(--box-size);
|
|
60
|
+
height: var(--box-size);
|
|
61
|
+
border: 2px solid var(--color-border);
|
|
62
|
+
border-radius: var(--border-radius-pill);
|
|
63
|
+
background-color: var(--color-bg);
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
.radio .radio-input:checked + .radio-box {
|
|
70
|
+
background-color: var(--color-accent);
|
|
71
|
+
border-color: var(--color-accent);
|
|
72
|
+
}
|
|
73
|
+
.radio .radio-input:checked + .radio-box::after {
|
|
74
|
+
position: absolute;
|
|
75
|
+
content: "";
|
|
76
|
+
width: calc(var(--box-size) * 0.4);
|
|
77
|
+
height: calc(var(--box-size) * 0.4);
|
|
78
|
+
border-radius: var(--border-radius-pill);
|
|
79
|
+
background-color: var(--color-accent-ink);
|
|
80
|
+
}
|
|
81
|
+
.radio-label:hover .radio-box {
|
|
82
|
+
border-color: var(--color-accent);
|
|
83
|
+
}
|
|
84
|
+
.radio .radio-input:focus-visible + .radio-box {
|
|
85
|
+
outline: 2px solid var(--color-accent);
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sr-only {
|
|
90
|
+
position: absolute;
|
|
91
|
+
width: 1px;
|
|
92
|
+
height: 1px;
|
|
93
|
+
padding: 0;
|
|
94
|
+
margin: -1px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
clip: rect(0, 0, 0, 0);
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
border-width: 0;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RadioButtonProps {
|
|
2
|
+
/** The value this radio represents; compared to v-model to determine checked state */
|
|
3
|
+
value?: unknown;
|
|
4
|
+
/** HTML name attribute — groups radios together so only one is selected at a time */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Inline label text (alternative to default slot) */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Visually hides the label while keeping it accessible to SR (screen readers) */
|
|
9
|
+
hideLabel?: boolean;
|
|
10
|
+
}
|
|
11
|
+
type __VLS_Props = RadioButtonProps;
|
|
12
|
+
type __VLS_ModelProps = {
|
|
13
|
+
modelValue?: unknown;
|
|
14
|
+
};
|
|
15
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
16
|
+
declare var __VLS_8: {};
|
|
17
|
+
type __VLS_Slots = {} & {
|
|
18
|
+
default?: (props: typeof __VLS_8) => any;
|
|
19
|
+
};
|
|
20
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
21
|
+
"update:modelValue": (value: unknown) => any;
|
|
22
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
23
|
+
"onUpdate:modelValue"?: ((value: unknown) => any) | undefined;
|
|
24
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
25
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
26
|
+
declare const _default: typeof __VLS_export;
|
|
27
|
+
export default _default;
|
|
28
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
29
|
+
new (): {
|
|
30
|
+
$slots: S;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Ref } from "vue";
|
|
2
|
+
/**
|
|
3
|
+
* Syncs a reactive object's keys to/from URL query params.
|
|
4
|
+
*
|
|
5
|
+
* Behaviour:
|
|
6
|
+
* - **On call (synchronous)**: reads current URL params and pre-populates the
|
|
7
|
+
* state so child components see the correct values before they mount.
|
|
8
|
+
* - **Reactive**: watches the state and updates the URL whenever synced keys
|
|
9
|
+
* change (via `router.replace` — no new history entry).
|
|
10
|
+
*
|
|
11
|
+
* SSR-safe: initial population uses `route.query` (available on the server),
|
|
12
|
+
* so the server renders the same HTML as the client hydrates — no mismatches.
|
|
13
|
+
* Reactive URL writes use `useUrlSearchParams` (client-only, via window).
|
|
14
|
+
*
|
|
15
|
+
* Value handling:
|
|
16
|
+
* - Nested objects → dot notation (`filters.category=shirts`)
|
|
17
|
+
* - Arrays → bracket notation (`tags[0]=cats&tags[1]=dogs`)
|
|
18
|
+
* - Combinations → mixed (`items[0].name=John`)
|
|
19
|
+
* - File / Blob → silently skipped (not serialisable)
|
|
20
|
+
* - Empty strings → removed from URL
|
|
21
|
+
*
|
|
22
|
+
* @param state Reactive object to sync
|
|
23
|
+
* @param keys Explicit list of top-level keys to sync.
|
|
24
|
+
* When omitted, ALL top-level keys currently in the URL are
|
|
25
|
+
* used for initial population, and ALL keys in the state are
|
|
26
|
+
* watched going forward.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Sync specific keys (recommended — avoids accidentally syncing private state)
|
|
30
|
+
* const properties = ref<Record<string, string | File[]>>({})
|
|
31
|
+
* useUrlSync(properties, ['variant', 'size', 'product-color'])
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Sync all — useful when state IS the URL state
|
|
35
|
+
* const filters = ref({ category: '', sort: 'newest' })
|
|
36
|
+
* useUrlSync(filters)
|
|
37
|
+
*/
|
|
38
|
+
export declare function useUrlSync(state: Ref<Record<string, unknown>>, keys?: string[]): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useUrlSearchParams } from "@vueuse/core";
|
|
2
|
+
import {
|
|
3
|
+
flattenParams,
|
|
4
|
+
unflattenParams,
|
|
5
|
+
topLevelKeys
|
|
6
|
+
} from "../utils/urlParams.js";
|
|
7
|
+
import { computed, watch } from "vue";
|
|
8
|
+
export function useUrlSync(state, keys) {
|
|
9
|
+
const routeQuery = useRoute().query;
|
|
10
|
+
const initKeys = keys ?? topLevelKeys(routeQuery);
|
|
11
|
+
for (const key of initKeys) {
|
|
12
|
+
const flat = {};
|
|
13
|
+
for (const [k, v] of Object.entries(routeQuery)) {
|
|
14
|
+
if (v !== null && (k === key || k.startsWith(`${key}.`) || k.startsWith(`${key}[`))) {
|
|
15
|
+
flat[k] = v;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (Object.keys(flat).length === 0) continue;
|
|
19
|
+
const reconstructed = unflattenParams(flat);
|
|
20
|
+
if (key in reconstructed) state.value[key] = reconstructed[key];
|
|
21
|
+
}
|
|
22
|
+
const urlParams = useUrlSearchParams("history");
|
|
23
|
+
const syncKeys = computed(() => keys ?? Object.keys(state.value));
|
|
24
|
+
watch(
|
|
25
|
+
() => syncKeys.value.map((k) => JSON.stringify(flattenParams(state.value[k], k))).join("|"),
|
|
26
|
+
() => {
|
|
27
|
+
for (const key of syncKeys.value) {
|
|
28
|
+
for (const k of Object.keys(urlParams)) {
|
|
29
|
+
if (k === key || k.startsWith(`${key}.`) || k.startsWith(`${key}[`)) {
|
|
30
|
+
delete urlParams[k];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
Object.assign(urlParams, flattenParams(state.value[key], key));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { default as NumberInputVertical } from "./components/NumberInput/Vertica
|
|
|
7
7
|
export { default as NumberInputHorizontal } from "./components/NumberInput/Horizontal.vue.js";
|
|
8
8
|
export { default as Textarea } from "./components/Textarea.vue.js";
|
|
9
9
|
export { default as CheckBox } from "./components/CheckBox.vue.js";
|
|
10
|
+
export { default as CheckboxGroup, type CheckboxOption, type CheckboxGroupProps, } from "./components/CheckboxGroup.vue.js";
|
|
11
|
+
export { default as RadioButton, type RadioButtonProps, } from "./components/RadioButton.vue.js";
|
|
10
12
|
export { default as SwitchButton } from "./components/SwitchButton.vue.js";
|
|
11
13
|
export { default as DatePicker } from "./components/DatePicker.vue.js";
|
|
12
14
|
export { default as DateRangePicker } from "./components/DateRangePicker.vue.js";
|
|
@@ -36,3 +38,5 @@ export { usePressAndHold } from "./composables/usePressAndHold.js";
|
|
|
36
38
|
export { useSound, type SoundOptions } from "./composables/useSound.js";
|
|
37
39
|
export { useValidation, isFilled, isEmail, type ValidationRule, } from "./composables/useValidation.js";
|
|
38
40
|
export { iconRegistry, type IconName } from "./utils/icon-registry.js";
|
|
41
|
+
export { flattenParams, unflattenParams, parsePath, topLevelKeys, } from "./utils/urlParams.js";
|
|
42
|
+
export { useUrlSync } from "./composables/useUrlSync.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -7,6 +7,12 @@ export { default as NumberInputVertical } from "./components/NumberInput/Vertica
|
|
|
7
7
|
export { default as NumberInputHorizontal } from "./components/NumberInput/Horizontal.vue";
|
|
8
8
|
export { default as Textarea } from "./components/Textarea.vue";
|
|
9
9
|
export { default as CheckBox } from "./components/CheckBox.vue";
|
|
10
|
+
export {
|
|
11
|
+
default as CheckboxGroup
|
|
12
|
+
} from "./components/CheckboxGroup.vue";
|
|
13
|
+
export {
|
|
14
|
+
default as RadioButton
|
|
15
|
+
} from "./components/RadioButton.vue";
|
|
10
16
|
export { default as SwitchButton } from "./components/SwitchButton.vue";
|
|
11
17
|
export { default as DatePicker } from "./components/DatePicker.vue";
|
|
12
18
|
export { default as DateRangePicker } from "./components/DateRangePicker.vue";
|
|
@@ -44,3 +50,10 @@ export {
|
|
|
44
50
|
isEmail
|
|
45
51
|
} from "./composables/useValidation.js";
|
|
46
52
|
export { iconRegistry } from "./utils/icon-registry.js";
|
|
53
|
+
export {
|
|
54
|
+
flattenParams,
|
|
55
|
+
unflattenParams,
|
|
56
|
+
parsePath,
|
|
57
|
+
topLevelKeys
|
|
58
|
+
} from "./utils/urlParams.js";
|
|
59
|
+
export { useUrlSync } from "./composables/useUrlSync.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for serialising/deserialising nested values to/from flat URL query params.
|
|
3
|
+
*
|
|
4
|
+
* Encoding conventions:
|
|
5
|
+
* Object keys → dot notation e.g. filters.category=shirts
|
|
6
|
+
* Array items → bracket notation e.g. tags[0]=cats&tags[1]=dogs
|
|
7
|
+
* Combined → e.g. items[0].name=John
|
|
8
|
+
*
|
|
9
|
+
* Non-serialisable values (File, Blob, null, undefined) are silently skipped.
|
|
10
|
+
* Empty strings produce no URL entry.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Recursively flattens a value to flat URL-compatible key-value pairs.
|
|
14
|
+
*
|
|
15
|
+
* @param value The value to flatten (any depth)
|
|
16
|
+
* @param prefix URL param prefix / key name (e.g. "filters", "items[0]")
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* flattenParams({ category: 'shirts', tags: ['a', 'b'] }, 'filters')
|
|
20
|
+
* // → { 'filters.category': 'shirts', 'filters.tags[0]': 'a', 'filters.tags[1]': 'b' }
|
|
21
|
+
*/
|
|
22
|
+
export declare function flattenParams(value: unknown, prefix: string): Record<string, string>;
|
|
23
|
+
/**
|
|
24
|
+
* Reconstructs a nested object from flat URL query params.
|
|
25
|
+
* Handles dot notation (obj.key) and bracket notation (arr[0]), and combinations.
|
|
26
|
+
*
|
|
27
|
+
* When a URL param has multiple values (string[]) only the first is used.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* unflattenParams({ 'filters.category': 'shirts', 'tags[0]': 'cats', 'tags[1]': 'dogs' })
|
|
31
|
+
* // → { filters: { category: 'shirts' }, tags: ['cats', 'dogs'] }
|
|
32
|
+
*/
|
|
33
|
+
export declare function unflattenParams(flat: Record<string, string | string[]>): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Parses a flat URL param key into path segments.
|
|
36
|
+
*
|
|
37
|
+
* Supports dot notation and bracket notation, including keys with hyphens.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* parsePath('a.b.c') // ['a', 'b', 'c']
|
|
41
|
+
* parsePath('product-color') // ['product-color']
|
|
42
|
+
* parsePath('items[0].name') // ['items', 0, 'name']
|
|
43
|
+
* parsePath('a[0][1].c') // ['a', 0, 1, 'c']
|
|
44
|
+
*/
|
|
45
|
+
export declare function parsePath(path: string): (string | number)[];
|
|
46
|
+
/**
|
|
47
|
+
* Extracts the distinct top-level keys from a flat params record.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* topLevelKeys({ 'a': '1', 'b.c': '2', 'd[0]': '3' }) // ['a', 'b', 'd']
|
|
51
|
+
*/
|
|
52
|
+
export declare function topLevelKeys(flat: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function flattenParams(value, prefix) {
|
|
2
|
+
const result = {};
|
|
3
|
+
_flatten(value, prefix, result);
|
|
4
|
+
return result;
|
|
5
|
+
}
|
|
6
|
+
function _flatten(value, key, out) {
|
|
7
|
+
if (value === null || value === void 0) return;
|
|
8
|
+
if (value instanceof File || value instanceof Blob) return;
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
if (value !== "") out[key] = value;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
14
|
+
out[key] = String(value);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
for (const [i, item] of value.entries()) {
|
|
19
|
+
_flatten(item, `${key}[${i}]`, out);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "object") {
|
|
24
|
+
for (const [k, v] of Object.entries(value)) {
|
|
25
|
+
_flatten(v, `${key}.${k}`, out);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function unflattenParams(flat) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, raw] of Object.entries(flat)) {
|
|
32
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
33
|
+
if (value === void 0 || value === "") continue;
|
|
34
|
+
_setByPath(result, parsePath(key), value);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function _setByPath(root, path, value) {
|
|
39
|
+
if (path.length === 0) return;
|
|
40
|
+
let current = root;
|
|
41
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
42
|
+
const seg = path[i];
|
|
43
|
+
const nextSeg = path[i + 1];
|
|
44
|
+
const parent = current;
|
|
45
|
+
if (parent[seg] == null || typeof parent[seg] !== "object") {
|
|
46
|
+
parent[seg] = typeof nextSeg === "number" ? [] : {};
|
|
47
|
+
}
|
|
48
|
+
current = parent[seg];
|
|
49
|
+
}
|
|
50
|
+
const lastSeg = path[path.length - 1];
|
|
51
|
+
current[lastSeg] = value;
|
|
52
|
+
}
|
|
53
|
+
export function parsePath(path) {
|
|
54
|
+
const segments = [];
|
|
55
|
+
for (const match of path.matchAll(/([^.[]+)|\[(\d+)\]/g)) {
|
|
56
|
+
segments.push(match[1] !== void 0 ? match[1] : parseInt(match[2], 10));
|
|
57
|
+
}
|
|
58
|
+
return segments;
|
|
59
|
+
}
|
|
60
|
+
export function topLevelKeys(flat) {
|
|
61
|
+
const keys = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const k of Object.keys(flat)) {
|
|
63
|
+
const match = k.match(/^([^.[]+)/);
|
|
64
|
+
if (match) keys.add(match[1]);
|
|
65
|
+
}
|
|
66
|
+
return [...keys];
|
|
67
|
+
}
|