nuance-ui 0.2.17 → 0.2.19
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/alert.vue +1 -1
- package/dist/runtime/components/app-shell/app-shell-main.vue +1 -1
- package/dist/runtime/components/app-shell/app-shell.vue +1 -1
- package/dist/runtime/components/badge.d.vue.ts +4 -1
- package/dist/runtime/components/badge.vue +5 -2
- package/dist/runtime/components/badge.vue.d.ts +4 -1
- package/dist/runtime/components/button/button.module.css +1 -1
- package/dist/runtime/components/center.d.vue.ts +18 -0
- package/dist/runtime/components/center.vue +26 -0
- package/dist/runtime/components/center.vue.d.ts +18 -0
- package/dist/runtime/components/floating-indicator.d.vue.ts +20 -0
- package/dist/runtime/components/floating-indicator.vue +50 -0
- package/dist/runtime/components/floating-indicator.vue.d.ts +20 -0
- package/dist/runtime/components/group.d.vue.ts +28 -0
- package/dist/runtime/components/group.vue +39 -0
- package/dist/runtime/components/group.vue.d.ts +28 -0
- package/dist/runtime/components/index.d.ts +2 -0
- package/dist/runtime/components/input/ui/input-base.d.vue.ts +1 -0
- package/dist/runtime/components/input/ui/input-base.vue +3 -2
- package/dist/runtime/components/input/ui/input-base.vue.d.ts +1 -0
- package/dist/runtime/components/link/lib.d.ts +2 -2
- package/dist/runtime/components/nav-link/nav-link.vue +1 -1
- package/dist/runtime/components/paper.vue +1 -1
- package/dist/runtime/components/pin-input/lib.d.ts +2 -0
- package/dist/runtime/components/pin-input/lib.js +19 -0
- package/dist/runtime/components/pin-input/pin-input.d.vue.ts +55 -0
- package/dist/runtime/components/pin-input/pin-input.vue +171 -0
- package/dist/runtime/components/pin-input/pin-input.vue.d.ts +55 -0
- package/dist/runtime/components/pin-input/use-pin-input.d.ts +18 -0
- package/dist/runtime/components/pin-input/use-pin-input.js +94 -0
- package/dist/runtime/components/roving-focus/lib/context.d.ts +2 -2
- package/dist/runtime/components/segmented-control.d.vue.ts +46 -0
- package/dist/runtime/components/segmented-control.vue +130 -0
- package/dist/runtime/components/segmented-control.vue.d.ts +46 -0
- package/dist/runtime/components/stack.d.vue.ts +24 -0
- package/dist/runtime/components/stack.vue +33 -0
- package/dist/runtime/components/stack.vue.d.ts +24 -0
- package/dist/runtime/components/switch/switch.vue +1 -1
- package/dist/runtime/components/table/ui/table.vue +1 -1
- package/dist/runtime/components/theme-toggle.d.vue.ts +14 -0
- package/dist/runtime/components/theme-toggle.vue +27 -0
- package/dist/runtime/components/theme-toggle.vue.d.ts +14 -0
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/use-floating-indicator.d.ts +11 -0
- package/dist/runtime/composables/use-floating-indicator.js +30 -0
- package/dist/runtime/form/segmented-control-field.d.vue.ts +7 -0
- package/dist/runtime/form/segmented-control-field.vue +45 -0
- package/dist/runtime/form/segmented-control-field.vue.d.ts +7 -0
- package/dist/runtime/styles/colors.css +1 -1
- package/dist/runtime/styles/const.css +1 -1
- package/dist/runtime/styles/dark-theme.css +1 -1
- package/dist/runtime/types/theme.d.ts +3 -3
- package/dist/runtime/utils/color/get-color-var.js +1 -2
- package/dist/runtime/utils/color/parse-theme-color.js +1 -2
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVarsResolver } from "@nui/composables";
|
|
3
|
+
import { getSize } from "@nui/utils";
|
|
4
|
+
import { computed, useId } from "vue";
|
|
5
|
+
import Group from "../group.vue";
|
|
6
|
+
import InputBase from "../input/ui/input-base.vue";
|
|
7
|
+
import VisuallyHiddenInput from "../visually-hidden/visually-hidden-input.vue";
|
|
8
|
+
import { createPinArray, pinValidate } from "./lib";
|
|
9
|
+
import { usePinInput } from "./use-pin-input";
|
|
10
|
+
const {
|
|
11
|
+
id,
|
|
12
|
+
name,
|
|
13
|
+
disabled,
|
|
14
|
+
required,
|
|
15
|
+
length = 4,
|
|
16
|
+
otp = true,
|
|
17
|
+
type = "number",
|
|
18
|
+
mask,
|
|
19
|
+
gap,
|
|
20
|
+
placeholder,
|
|
21
|
+
autoFocus,
|
|
22
|
+
manageFocus = true,
|
|
23
|
+
size,
|
|
24
|
+
radius = "md",
|
|
25
|
+
readOnly,
|
|
26
|
+
error
|
|
27
|
+
} = defineProps({
|
|
28
|
+
id: { type: String, required: false },
|
|
29
|
+
name: { type: String, required: false },
|
|
30
|
+
gap: { type: [String, Number], required: false },
|
|
31
|
+
radius: { type: [String, Number], required: false },
|
|
32
|
+
size: { type: String, required: false },
|
|
33
|
+
autoFocus: { type: Boolean, required: false },
|
|
34
|
+
placeholder: { type: String, required: false },
|
|
35
|
+
manageFocus: { type: Boolean, required: false },
|
|
36
|
+
otp: { type: Boolean, required: false },
|
|
37
|
+
disabled: { type: Boolean, required: false },
|
|
38
|
+
error: { type: Boolean, required: false },
|
|
39
|
+
type: { type: null, required: false },
|
|
40
|
+
mask: { type: Boolean, required: false },
|
|
41
|
+
length: { type: Number, required: false },
|
|
42
|
+
readOnly: { type: Boolean, required: false },
|
|
43
|
+
ariaLabel: { type: String, required: false },
|
|
44
|
+
required: { type: Boolean, required: false },
|
|
45
|
+
is: { type: null, required: false },
|
|
46
|
+
mod: { type: [Object, Array, null], required: false }
|
|
47
|
+
});
|
|
48
|
+
defineEmits(["complete"]);
|
|
49
|
+
const uuid = computed(() => id ?? useId());
|
|
50
|
+
const inputMode = computed(() => type === "number" ? "numeric" : "text");
|
|
51
|
+
const model = defineModel({ type: String, ...{ default: "" } });
|
|
52
|
+
const {
|
|
53
|
+
refs,
|
|
54
|
+
focus,
|
|
55
|
+
focusedIx,
|
|
56
|
+
cells,
|
|
57
|
+
setFieldValue,
|
|
58
|
+
handleKeydown
|
|
59
|
+
} = usePinInput({
|
|
60
|
+
length: () => length,
|
|
61
|
+
manageFocus,
|
|
62
|
+
value: model,
|
|
63
|
+
inputMode
|
|
64
|
+
});
|
|
65
|
+
function handleInput(event, ix) {
|
|
66
|
+
const target = event.target;
|
|
67
|
+
const inputValue = target.value;
|
|
68
|
+
if (inputValue.length > 1) {
|
|
69
|
+
const isPaste = inputValue.length > 2;
|
|
70
|
+
if (isPaste) {
|
|
71
|
+
const isValid = pinValidate(inputValue, type);
|
|
72
|
+
if (!isValid)
|
|
73
|
+
return;
|
|
74
|
+
cells.value = createPinArray(length, inputValue);
|
|
75
|
+
const filledCount = Math.min(inputValue.length, length);
|
|
76
|
+
if (filledCount < length)
|
|
77
|
+
focus("next", filledCount - 1);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const newChar = inputValue.split("")[inputValue.length - 1];
|
|
81
|
+
if (pinValidate(newChar, type)) {
|
|
82
|
+
setFieldValue(newChar, ix);
|
|
83
|
+
focus("next", ix);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (inputValue.length === 1) {
|
|
88
|
+
if (pinValidate(inputValue, type)) {
|
|
89
|
+
setFieldValue(inputValue, ix);
|
|
90
|
+
focus("next", ix);
|
|
91
|
+
} else {
|
|
92
|
+
setFieldValue("", ix);
|
|
93
|
+
}
|
|
94
|
+
} else if (inputValue.length === 0) {
|
|
95
|
+
setFieldValue("", ix);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function handlePaste(event) {
|
|
99
|
+
const pasteData = event.clipboardData?.getData("text/plain").replace(/\s+/g, "").trim();
|
|
100
|
+
const isValid = pinValidate(pasteData, type);
|
|
101
|
+
if (!isValid)
|
|
102
|
+
return;
|
|
103
|
+
const pasteArray = createPinArray(length, pasteData);
|
|
104
|
+
cells.value = pasteArray;
|
|
105
|
+
const filledCount = pasteArray.filter((v) => v !== "").length;
|
|
106
|
+
const focusIx = filledCount >= length ? length - 1 : filledCount;
|
|
107
|
+
refs.value[focusIx]?.focus();
|
|
108
|
+
}
|
|
109
|
+
const style = useVarsResolver(() => ({
|
|
110
|
+
root: {
|
|
111
|
+
"--pin-size": getSize(size, "pin-size"),
|
|
112
|
+
"--pin-fz": getSize(size, "pin-fz")
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<Group
|
|
119
|
+
role='group'
|
|
120
|
+
wrap='nowrap'
|
|
121
|
+
:gap
|
|
122
|
+
dir='ltr'
|
|
123
|
+
:class='$style.root'
|
|
124
|
+
:style='style.root'
|
|
125
|
+
>
|
|
126
|
+
<InputBase
|
|
127
|
+
v-for='(ids, ix) in length'
|
|
128
|
+
:id='`${uuid}-${ix + 1}`'
|
|
129
|
+
:key='ids'
|
|
130
|
+
:error
|
|
131
|
+
:class='$style.pinRoot'
|
|
132
|
+
:radius
|
|
133
|
+
>
|
|
134
|
+
<template #default='{ id: _id, css }'>
|
|
135
|
+
<input
|
|
136
|
+
:id='_id'
|
|
137
|
+
:key='ids'
|
|
138
|
+
:ref='refs.set'
|
|
139
|
+
:class='[css, $style.pin]'
|
|
140
|
+
:value='cells[ix]'
|
|
141
|
+
:autocomplete='otp ? "one-time-code" : void 0'
|
|
142
|
+
:input-mode
|
|
143
|
+
:type='mask ? "password" : "text"'
|
|
144
|
+
:placeholder
|
|
145
|
+
:auto-focus='autoFocus && ix === 0'
|
|
146
|
+
:readonly='readOnly'
|
|
147
|
+
:disabled
|
|
148
|
+
@input='handleInput($event, ix)'
|
|
149
|
+
@focus='(event) => {
|
|
150
|
+
if (!readOnly) {
|
|
151
|
+
event.target?.select();
|
|
152
|
+
focusedIx = ix;
|
|
153
|
+
}
|
|
154
|
+
}'
|
|
155
|
+
@blur='focusedIx = -1'
|
|
156
|
+
@paste.prevent='handlePaste'
|
|
157
|
+
@keydown='handleKeydown($event, ix)'
|
|
158
|
+
>
|
|
159
|
+
</template>
|
|
160
|
+
</InputBase>
|
|
161
|
+
</Group>
|
|
162
|
+
<VisuallyHiddenInput
|
|
163
|
+
:name
|
|
164
|
+
:disabled
|
|
165
|
+
:required
|
|
166
|
+
/>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<style module>
|
|
170
|
+
.root{--pin-size-xs:rem(30px);--pin-size-sm:rem(36px);--pin-size-md:rem(42px);--pin-size-lg:rem(50px);--pin-size-xl:rem(60px);--pin-size:var(--pin-size-sm);--pin-fz-xs:1.125rem;--pin-fz-sm:1.5rem;--pin-fz-md:1.75rem;--pin-fz-lg:2rem;--pin-fz-xl:2.5rem;--pin-fz:var(--pin-fz-sm)}.pinRoot{@mixin where-light{--input-bd:var(--color-gray-2);--input-bg:var(--color-gray-0)}@mixin where-dark{--input-bd:var(--color-gray-7);--input-bg:var(--color-dark-9)}}.pin{font-size:var(--pin-fz);font-weight:600;height:calc(var(--pin-size) + var(--pin-size)/3);line-height:1;padding:0;text-align:center;width:var(--pin-size)}
|
|
171
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NuanceRadius, NuanceSize, NuanceSpacing } from '../../types/index.js';
|
|
2
|
+
import type { BoxProps } from '../box.vue.js';
|
|
3
|
+
export interface PinInputEmits {
|
|
4
|
+
complete: [value: string];
|
|
5
|
+
}
|
|
6
|
+
export interface PinInputProps extends BoxProps {
|
|
7
|
+
/** Id auto-generated if not provided */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Hidden input `name` attribute */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Key of `theme.spacing` or any valid CSS value to set `gap` between inputs, numbers are converted to rem @default 'md' */
|
|
12
|
+
gap?: NuanceSpacing;
|
|
13
|
+
/** Key of `theme.radius` or any valid CSS value to set `border-radius`, numbers are converted to rem @default 'md' */
|
|
14
|
+
radius?: NuanceRadius;
|
|
15
|
+
/** Controls inputs `width` and `height` @default 'sm' */
|
|
16
|
+
size?: NuanceSize;
|
|
17
|
+
/** If set, the first input is focused when component is mounted @default false */
|
|
18
|
+
autoFocus?: boolean;
|
|
19
|
+
/** Inputs placeholder */
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Determines whether focus should be moved automatically to the next input once filled @default true */
|
|
22
|
+
manageFocus?: boolean;
|
|
23
|
+
/** Determines whether `autocomplete="one-time-code"` attribute should be set on all inputs @default true */
|
|
24
|
+
otp?: boolean;
|
|
25
|
+
/** Adds disabled attribute to all inputs */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Sets `aria-invalid` attribute and applies error styles to all inputs */
|
|
28
|
+
error?: boolean;
|
|
29
|
+
/** Determines which values can be entered @default 'alphanumeric' */
|
|
30
|
+
type?: 'alphanumeric' | 'number' | RegExp;
|
|
31
|
+
/** Changes input type to `"password"` @default false */
|
|
32
|
+
mask?: boolean;
|
|
33
|
+
/** Number of inputs @default 4 */
|
|
34
|
+
length?: number;
|
|
35
|
+
/** If set, the user cannot edit the value */
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** `aria-label` attribute */
|
|
38
|
+
ariaLabel?: string;
|
|
39
|
+
/** Marks the field as required */
|
|
40
|
+
required?: boolean;
|
|
41
|
+
}
|
|
42
|
+
type __VLS_Props = PinInputProps;
|
|
43
|
+
type __VLS_ModelProps = {
|
|
44
|
+
modelValue?: string;
|
|
45
|
+
};
|
|
46
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
47
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
48
|
+
"update:modelValue": (value: string) => any;
|
|
49
|
+
complete: (value: string) => any;
|
|
50
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
51
|
+
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
52
|
+
onComplete?: ((value: string) => any) | undefined;
|
|
53
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
54
|
+
declare const _default: typeof __VLS_export;
|
|
55
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter, ModelRef } from 'vue';
|
|
2
|
+
export declare function usePinInput<Element extends HTMLInputElement>({ value: model, length, allowedKeys, manageFocus, inputMode, }: {
|
|
3
|
+
value: ModelRef<string>;
|
|
4
|
+
/** current length */
|
|
5
|
+
length: MaybeRefOrGetter<number>;
|
|
6
|
+
/** @default ['Backspace', 'Tab', 'Control', 'Delete', 'ArrowLeft', 'ArrowRight'] */
|
|
7
|
+
allowedKeys?: string[];
|
|
8
|
+
/** @default true */
|
|
9
|
+
manageFocus?: boolean;
|
|
10
|
+
inputMode: MaybeRefOrGetter<'numeric' | 'text'>;
|
|
11
|
+
}): {
|
|
12
|
+
refs: Readonly<import("vue").Ref<Readonly<import("@vueuse/core").TemplateRefsList<Element>>, Readonly<import("@vueuse/core").TemplateRefsList<Element>>>>;
|
|
13
|
+
cells: import("vue").WritableComputedRef<string[], string[]>;
|
|
14
|
+
focusedIx: import("vue").Ref<number, number>;
|
|
15
|
+
focus: (dir: "next" | "prev", ix: number) => void;
|
|
16
|
+
handleKeydown: (event: KeyboardEvent, ix: number) => void;
|
|
17
|
+
setFieldValue: (value: string, ix: number) => string[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useTemplateRefsList } from "@vueuse/core";
|
|
2
|
+
import { computed, ref, toValue } from "vue";
|
|
3
|
+
import { createPinArray } from "./lib.js";
|
|
4
|
+
export function usePinInput({
|
|
5
|
+
value: model,
|
|
6
|
+
length,
|
|
7
|
+
allowedKeys = ["Backspace", "Tab", "Control", "Delete", "ArrowLeft", "ArrowRight"],
|
|
8
|
+
manageFocus = true,
|
|
9
|
+
inputMode
|
|
10
|
+
}) {
|
|
11
|
+
const refs = useTemplateRefsList();
|
|
12
|
+
const focusedIx = ref(-1);
|
|
13
|
+
const cells = computed({
|
|
14
|
+
get: () => createPinArray(toValue(length), model.value),
|
|
15
|
+
set: (value) => {
|
|
16
|
+
model.value = value.join("").trim();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
function setFieldValue(value, ix) {
|
|
20
|
+
const values = [...cells.value];
|
|
21
|
+
values[ix] = value;
|
|
22
|
+
cells.value = values;
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
function focus(dir, ix) {
|
|
26
|
+
if (dir === "next") {
|
|
27
|
+
const nextIx = ix + 1;
|
|
28
|
+
if (nextIx < toValue(length))
|
|
29
|
+
refs.value[nextIx]?.focus();
|
|
30
|
+
} else if (dir === "prev") {
|
|
31
|
+
const prevIx = ix - 1;
|
|
32
|
+
if (prevIx >= 0)
|
|
33
|
+
refs.value[prevIx]?.focus();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function handleKeydown(event, ix) {
|
|
37
|
+
const inputValue = event.target.value;
|
|
38
|
+
if (inputMode === "numeric") {
|
|
39
|
+
const isModifierShortcut = event.ctrlKey || event.metaKey;
|
|
40
|
+
const isAllowedKey = allowedKeys.includes(event.key) || isModifierShortcut || !Number.isNaN(Number(event.key));
|
|
41
|
+
if (!isAllowedKey)
|
|
42
|
+
return event.preventDefault();
|
|
43
|
+
}
|
|
44
|
+
switch (event.key) {
|
|
45
|
+
case "ArrowLeft":
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
focus("prev", ix);
|
|
48
|
+
break;
|
|
49
|
+
case "ArrowRight":
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
focus("next", ix);
|
|
52
|
+
break;
|
|
53
|
+
case "Tab":
|
|
54
|
+
if (event.shiftKey && ix > 0 && manageFocus) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
focus("prev", ix);
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case " ":
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
focus("next", ix);
|
|
62
|
+
break;
|
|
63
|
+
case "Delete":
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
setFieldValue("", ix);
|
|
66
|
+
break;
|
|
67
|
+
case "Backspace":
|
|
68
|
+
if (inputValue === "") {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
focus("prev", ix);
|
|
71
|
+
} else {
|
|
72
|
+
setFieldValue("", ix);
|
|
73
|
+
if (ix < toValue(length) - 1) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
focus("prev", ix);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
if (inputValue.length > 0 && event.key === cells.value[ix]) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
focus("next", ix);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
refs,
|
|
88
|
+
cells,
|
|
89
|
+
focusedIx,
|
|
90
|
+
focus,
|
|
91
|
+
handleKeydown,
|
|
92
|
+
setFieldValue
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -20,7 +20,7 @@ export declare const useProvideRovingFocus: (args_0: State) => {
|
|
|
20
20
|
attr: string;
|
|
21
21
|
list: Readonly<ShallowRef<HTMLElement | null>>;
|
|
22
22
|
loop: boolean | undefined;
|
|
23
|
-
orientation: "
|
|
23
|
+
orientation: "horizontal" | "vertical" | "both" | undefined;
|
|
24
24
|
init: () => number;
|
|
25
25
|
focus: {
|
|
26
26
|
(dir: "first" | "last"): void;
|
|
@@ -38,7 +38,7 @@ export declare const useRovingFocus: () => {
|
|
|
38
38
|
attr: string;
|
|
39
39
|
list: Readonly<ShallowRef<HTMLElement | null>>;
|
|
40
40
|
loop: boolean | undefined;
|
|
41
|
-
orientation: "
|
|
41
|
+
orientation: "horizontal" | "vertical" | "both" | undefined;
|
|
42
42
|
init: () => number;
|
|
43
43
|
focus: {
|
|
44
44
|
(dir: "first" | "last"): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AnyString, NuanceColor, NuanceRadius, NuanceSize } from '@nui/types';
|
|
2
|
+
import type { BoxProps } from './box.vue.js';
|
|
3
|
+
export interface SegmentedControlItem {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SegmentedControlProps extends BoxProps {
|
|
9
|
+
/** Items to render as controls */
|
|
10
|
+
data: (string | SegmentedControlItem)[];
|
|
11
|
+
/** Component size */
|
|
12
|
+
size?: NuanceSize | AnyString;
|
|
13
|
+
/** Border radius */
|
|
14
|
+
radius?: NuanceRadius;
|
|
15
|
+
/** Color from theme */
|
|
16
|
+
color?: NuanceColor;
|
|
17
|
+
/** Indicator `transition-duration` in ms */
|
|
18
|
+
transitionDuration?: number;
|
|
19
|
+
/** Indicator `transition-timing-function` */
|
|
20
|
+
transitionTimingFunction?: string;
|
|
21
|
+
/** Fills parent width */
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
/** Component orientation */
|
|
24
|
+
orientation?: 'horizontal' | 'vertical';
|
|
25
|
+
/** Disables the component */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Prevents value changes */
|
|
28
|
+
readOnly?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Show borders between items
|
|
31
|
+
* @default `true`
|
|
32
|
+
*/
|
|
33
|
+
withItemsBorders?: boolean;
|
|
34
|
+
}
|
|
35
|
+
type __VLS_Props = SegmentedControlProps;
|
|
36
|
+
type __VLS_ModelProps = {
|
|
37
|
+
modelValue?: string;
|
|
38
|
+
};
|
|
39
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
40
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
41
|
+
"update:modelValue": (value: string | undefined) => any;
|
|
42
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
43
|
+
"onUpdate:modelValue"?: ((value: string | undefined) => any) | undefined;
|
|
44
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
45
|
+
declare const _default: typeof __VLS_export;
|
|
46
|
+
export default _default;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVarsResolver } from "@nui/composables";
|
|
3
|
+
import { getFontSize, getRadius, getSize, getThemeColor } from "@nui/utils";
|
|
4
|
+
import { useTemplateRefsList } from "@vueuse/core";
|
|
5
|
+
import { computed, useId, useTemplateRef } from "vue";
|
|
6
|
+
import Box from "./box.vue";
|
|
7
|
+
import FloatingIndicator from "./floating-indicator.vue";
|
|
8
|
+
const {
|
|
9
|
+
is,
|
|
10
|
+
mod,
|
|
11
|
+
data,
|
|
12
|
+
size,
|
|
13
|
+
radius,
|
|
14
|
+
color,
|
|
15
|
+
transitionDuration,
|
|
16
|
+
transitionTimingFunction,
|
|
17
|
+
fullWidth,
|
|
18
|
+
orientation = "horizontal",
|
|
19
|
+
disabled,
|
|
20
|
+
readOnly,
|
|
21
|
+
withItemsBorders = true
|
|
22
|
+
} = defineProps({
|
|
23
|
+
data: { type: Array, required: true },
|
|
24
|
+
size: { type: [String, Object], required: false },
|
|
25
|
+
radius: { type: [String, Number], required: false },
|
|
26
|
+
color: { type: null, required: false },
|
|
27
|
+
transitionDuration: { type: Number, required: false },
|
|
28
|
+
transitionTimingFunction: { type: String, required: false },
|
|
29
|
+
fullWidth: { type: Boolean, required: false },
|
|
30
|
+
orientation: { type: String, required: false },
|
|
31
|
+
disabled: { type: Boolean, required: false },
|
|
32
|
+
readOnly: { type: Boolean, required: false },
|
|
33
|
+
withItemsBorders: { type: Boolean, required: false },
|
|
34
|
+
is: { type: null, required: false },
|
|
35
|
+
mod: { type: [Object, Array, null], required: false }
|
|
36
|
+
});
|
|
37
|
+
const value = defineModel({ type: String });
|
|
38
|
+
const items = computed(
|
|
39
|
+
() => data.map((item) => typeof item === "string" ? { value: item, label: item } : item)
|
|
40
|
+
);
|
|
41
|
+
if (value.value === void 0) {
|
|
42
|
+
const first = items.value.find((i) => !i.disabled);
|
|
43
|
+
if (first)
|
|
44
|
+
value.value = first.value;
|
|
45
|
+
}
|
|
46
|
+
const groupName = useId();
|
|
47
|
+
const rootRef = useTemplateRef("root");
|
|
48
|
+
const labelRefs = useTemplateRefsList();
|
|
49
|
+
const activeTarget = computed(() => {
|
|
50
|
+
if (value.value === void 0)
|
|
51
|
+
return null;
|
|
52
|
+
const index = items.value.findIndex((i) => i.value === value.value);
|
|
53
|
+
return index !== -1 ? labelRefs.value[index] ?? null : null;
|
|
54
|
+
});
|
|
55
|
+
const style = useVarsResolver((theme) => ({
|
|
56
|
+
root: {
|
|
57
|
+
"--sc-radius": getRadius(radius),
|
|
58
|
+
"--sc-color": color ? getThemeColor(color, theme) : void 0,
|
|
59
|
+
"--sc-shadow": color ? void 0 : "var(--shadow-xs)",
|
|
60
|
+
"--sc-transition-duration": transitionDuration === void 0 ? void 0 : `${transitionDuration}ms`,
|
|
61
|
+
"--sc-transition-timing-function": transitionTimingFunction,
|
|
62
|
+
"--sc-padding": getSize(size, "sc-padding"),
|
|
63
|
+
"--sc-font-size": getFontSize(size)
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<Box
|
|
70
|
+
:is
|
|
71
|
+
ref='root'
|
|
72
|
+
:style='style.root'
|
|
73
|
+
:class='$style.root'
|
|
74
|
+
:mod='[{
|
|
75
|
+
"full-width": fullWidth,
|
|
76
|
+
orientation,
|
|
77
|
+
disabled,
|
|
78
|
+
"with-items-borders": withItemsBorders
|
|
79
|
+
}, mod]'
|
|
80
|
+
role='radiogroup'
|
|
81
|
+
>
|
|
82
|
+
<FloatingIndicator
|
|
83
|
+
v-if='typeof value !== void 0'
|
|
84
|
+
:target='activeTarget'
|
|
85
|
+
:parent='rootRef'
|
|
86
|
+
:class='$style.indicator'
|
|
87
|
+
:orientation
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<Box
|
|
91
|
+
v-for='item in items'
|
|
92
|
+
:key='item.value'
|
|
93
|
+
:class='$style.control'
|
|
94
|
+
:mod='{ active: value === item.value, orientation }'
|
|
95
|
+
>
|
|
96
|
+
<input
|
|
97
|
+
:id='`${groupName}-${item.value}`'
|
|
98
|
+
:key='`${item.value}-input`'
|
|
99
|
+
:class='$style.input'
|
|
100
|
+
type='radio'
|
|
101
|
+
:name='groupName'
|
|
102
|
+
:value='item.value'
|
|
103
|
+
:checked='value === item.value'
|
|
104
|
+
:disabled='disabled || item.disabled'
|
|
105
|
+
@change='!readOnly && (value = item.value)'
|
|
106
|
+
>
|
|
107
|
+
|
|
108
|
+
<Box
|
|
109
|
+
is='label'
|
|
110
|
+
:ref='labelRefs.set'
|
|
111
|
+
:for='`${groupName}-${item.value}`'
|
|
112
|
+
:class='$style.label'
|
|
113
|
+
:style='{
|
|
114
|
+
"--sc-label-color": !!color && "var(--color-white)"
|
|
115
|
+
}'
|
|
116
|
+
:mod='{
|
|
117
|
+
"active": value === item.value && !(disabled || item.disabled),
|
|
118
|
+
"disabled": disabled || item.disabled,
|
|
119
|
+
"read-only": readOnly
|
|
120
|
+
}'
|
|
121
|
+
>
|
|
122
|
+
<span :class='$style.innerLabel'>{{ item.label }}</span>
|
|
123
|
+
</Box>
|
|
124
|
+
</Box>
|
|
125
|
+
</Box>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<style module>
|
|
129
|
+
.root{--sc-padding-xs:2px 6px;--sc-padding-sm:3px 10px;--sc-padding-md:4px 14px;--sc-padding-lg:7px 16px;--sc-padding-xl:10px 20px;--sc-padding:var(--sc-padding-sm);--sc-radius:var(--radius-md);--sc-transition-duration:200ms;--sc-transition-timing-function:ease;--sc-font-size:var(--font-size-sm);border-radius:var(--sc-radius,var(--radius-md));display:inline-flex;flex-direction:row;overflow:hidden;padding:4px;position:relative;width:auto}.root:where([data-full-width]){display:flex}.root:where([data-orientation=vertical]){display:flex;flex-direction:column;width:-moz-max-content;width:max-content}.root:where([data-orientation=vertical]):where([data-full-width]){width:auto}.root{@mixin where-light{background-color:var(--color-gray-1)}}.root{@mixin where-dark{background-color:var(--color-dark-8)}}.indicator{border-radius:calc(var(--sc-radius, var(--radius-md)) + 2px);display:block;position:absolute;z-index:1}.indicator:where([data-orientation=horizontal]){bottom:4px;top:4px}.indicator:where([data-orientation=vertical]){left:4px;right:4px}.indicator{@mixin where-light{background-color:var(--sc-color,var(--color-white));box-shadow:var(--sc-shadow,none)}}.indicator{@mixin where-dark{background-color:var(--sc-color,var(--color-dark-5));box-shadow:none}}.label{border-radius:calc(var(--sc-radius, var(--radius-md)) + 2px);cursor:pointer;display:block;font-size:var(--sc-font-size);font-weight:500;outline:var(--segmented-control-outline,none);overflow:hidden;padding:var(--sc-padding);text-align:center;text-overflow:ellipsis;transition:color var(--sc-transition-duration) var(--sc-transition-timing-function);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap;-webkit-tap-highlight-color:transparent;@mixin where-light{color:var(--color-gray-7)}@mixin where-dark{color:var(--color-dark-1)}}.label:where([data-read-only]){cursor:default}.label:where([data-active]){@mixin where-light{color:var(--sc-label-color,var(--color-black))}@mixin where-dark{color:var(--sc-label-color,var(--color-white))}}.label:where([data-active]):before{border-radius:calc(var(--sc-radius, var(--radius-md)) + 2px);content:"";inset:0;position:absolute;z-index:0}.root:where([data-initialized]) .label:where([data-active]):before{display:none}.label:where([data-active]):before{@mixin where-light{box-shadow:var(--sc-shadow,none)}}.label:where([data-active]):before{@mixin where-dark{box-shadow:none}}.label:where([data-active])[data-disabled],fieldset:disabled .label:where([data-active]){color:var(--color-disabled-text);cursor:not-allowed}.label:where(:not([data-disabled],[data-active],[data-read-only])){@mixin hover{@mixin where-light{color:var(--color-black)}@mixin where-dark{color:var(--color-white)}}}fieldset:disabled .label{@mixin hover{color:var(--color-disabled-text)!important}}.input{height:0;opacity:0;overflow:hidden;position:absolute;white-space:nowrap;width:0}.input:focus-visible+.label{--segmented-control-outline:2px solid var(--color-primary-filled)}.control{flex:1;position:relative;transition:border-color var(--sc-transition-duration) var(--sc-transition-timing-function);z-index:2}.root[data-with-items-borders] :where(.control):before{background-color:var(--separator-color);bottom:0;content:"";inset-inline-start:0;position:absolute;top:0;transition:background-color var(--sc-transition-duration) var(--sc-transition-timing-function);width:1px}.control[data-orientation=vertical]:before{bottom:auto;height:1px;top:0;inset-inline:0;width:auto}.control:first-of-type:before,[data-mantine-color-scheme] .control[data-active]+.control:before,[data-mantine-color-scheme] .control[data-active]:before{--separator-color:transparent}.control{@mixin where-light{--separator-color:var(--color-gray-3)}}.control{@mixin where-dark{--separator-color:var(--color-dark-4)}}.innerLabel{position:relative;z-index:2}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AnyString, NuanceColor, NuanceRadius, NuanceSize } from '@nui/types';
|
|
2
|
+
import type { BoxProps } from './box.vue.js';
|
|
3
|
+
export interface SegmentedControlItem {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SegmentedControlProps extends BoxProps {
|
|
9
|
+
/** Items to render as controls */
|
|
10
|
+
data: (string | SegmentedControlItem)[];
|
|
11
|
+
/** Component size */
|
|
12
|
+
size?: NuanceSize | AnyString;
|
|
13
|
+
/** Border radius */
|
|
14
|
+
radius?: NuanceRadius;
|
|
15
|
+
/** Color from theme */
|
|
16
|
+
color?: NuanceColor;
|
|
17
|
+
/** Indicator `transition-duration` in ms */
|
|
18
|
+
transitionDuration?: number;
|
|
19
|
+
/** Indicator `transition-timing-function` */
|
|
20
|
+
transitionTimingFunction?: string;
|
|
21
|
+
/** Fills parent width */
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
/** Component orientation */
|
|
24
|
+
orientation?: 'horizontal' | 'vertical';
|
|
25
|
+
/** Disables the component */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Prevents value changes */
|
|
28
|
+
readOnly?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Show borders between items
|
|
31
|
+
* @default `true`
|
|
32
|
+
*/
|
|
33
|
+
withItemsBorders?: boolean;
|
|
34
|
+
}
|
|
35
|
+
type __VLS_Props = SegmentedControlProps;
|
|
36
|
+
type __VLS_ModelProps = {
|
|
37
|
+
modelValue?: string;
|
|
38
|
+
};
|
|
39
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
40
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
41
|
+
"update:modelValue": (value: string | undefined) => any;
|
|
42
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
43
|
+
"onUpdate:modelValue"?: ((value: string | undefined) => any) | undefined;
|
|
44
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
45
|
+
declare const _default: typeof __VLS_export;
|
|
46
|
+
export default _default;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue';
|
|
2
|
+
import type { NuanceSpacing } from '../types/index.js';
|
|
3
|
+
import type { BoxProps } from './box.vue.js';
|
|
4
|
+
export interface StackProps extends BoxProps {
|
|
5
|
+
/** Key of `theme.spacing` or any valid CSS value to set `gap` property, numbers are converted to rem @default 'md' */
|
|
6
|
+
gap?: NuanceSpacing;
|
|
7
|
+
/** Controls `align-items` CSS property @default 'stretch' */
|
|
8
|
+
align?: CSSProperties['alignItems'];
|
|
9
|
+
/** Controls `justify-content` CSS property @default 'flex-start' */
|
|
10
|
+
justify?: CSSProperties['justifyContent'];
|
|
11
|
+
}
|
|
12
|
+
declare var __VLS_8: {};
|
|
13
|
+
type __VLS_Slots = {} & {
|
|
14
|
+
default?: (props: typeof __VLS_8) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_base: import("vue").DefineComponent<StackProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<StackProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
19
|
+
export default _default;
|
|
20
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
21
|
+
new (): {
|
|
22
|
+
$slots: S;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { getSpacing, useVarsResolver } from "#imports";
|
|
3
|
+
import Box from "./box.vue";
|
|
4
|
+
const {
|
|
5
|
+
gap,
|
|
6
|
+
align,
|
|
7
|
+
justify,
|
|
8
|
+
...props
|
|
9
|
+
} = defineProps({
|
|
10
|
+
gap: { type: [String, Number], required: false },
|
|
11
|
+
align: { type: void 0, required: false },
|
|
12
|
+
justify: { type: void 0, required: false },
|
|
13
|
+
is: { type: null, required: false },
|
|
14
|
+
mod: { type: [Object, Array, null], required: false }
|
|
15
|
+
});
|
|
16
|
+
const style = useVarsResolver(() => ({
|
|
17
|
+
root: {
|
|
18
|
+
"--stack-gap": getSpacing(gap),
|
|
19
|
+
"--stack-align": align ?? void 0,
|
|
20
|
+
"--stack-justify": justify ?? void 0
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<Box v-bind='props' :style='style.root' :class='$style.root'>
|
|
27
|
+
<slot />
|
|
28
|
+
</Box>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style module>
|
|
32
|
+
.root{align-items:var(--stack-align,stretch);display:flex;flex-direction:column;gap:var(--stack-gap,var(--spacing-md));justify-content:var(--stack-justify,flex-start)}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue';
|
|
2
|
+
import type { NuanceSpacing } from '../types/index.js';
|
|
3
|
+
import type { BoxProps } from './box.vue.js';
|
|
4
|
+
export interface StackProps extends BoxProps {
|
|
5
|
+
/** Key of `theme.spacing` or any valid CSS value to set `gap` property, numbers are converted to rem @default 'md' */
|
|
6
|
+
gap?: NuanceSpacing;
|
|
7
|
+
/** Controls `align-items` CSS property @default 'stretch' */
|
|
8
|
+
align?: CSSProperties['alignItems'];
|
|
9
|
+
/** Controls `justify-content` CSS property @default 'flex-start' */
|
|
10
|
+
justify?: CSSProperties['justifyContent'];
|
|
11
|
+
}
|
|
12
|
+
declare var __VLS_8: {};
|
|
13
|
+
type __VLS_Slots = {} & {
|
|
14
|
+
default?: (props: typeof __VLS_8) => any;
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_base: import("vue").DefineComponent<StackProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<StackProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
19
|
+
export default _default;
|
|
20
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
21
|
+
new (): {
|
|
22
|
+
$slots: S;
|
|
23
|
+
};
|
|
24
|
+
};
|