Tracking de l'application VApp (IHM du jeu)

This commit is contained in:
2025-05-11 18:04:12 +02:00
commit 89e9db9b62
17763 changed files with 3718499 additions and 0 deletions

View File

@ -0,0 +1,57 @@
.v-otp-input {
border-radius: 4px;
align-items: center;
display: flex;
justify-content: center;
padding: 0.5rem 0;
position: relative;
}
.v-otp-input .v-field {
height: 100%;
}
.v-otp-input__divider {
margin: 0 8px;
}
.v-otp-input__content {
align-items: center;
display: flex;
gap: 0.5rem;
height: 64px;
padding: 0.5rem;
justify-content: center;
max-width: 320px;
position: relative;
border-radius: inherit;
}
.v-otp-input--divided .v-otp-input__content {
max-width: 360px;
}
.v-otp-input__field {
color: inherit;
font-size: 1.25rem;
height: 100%;
outline: none;
text-align: center;
width: 100%;
}
.v-otp-input__field[type=number]::-webkit-outer-spin-button, .v-otp-input__field[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.v-otp-input__field[type=number] {
-moz-appearance: textfield;
}
.v-otp-input__loader {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
}
.v-otp-input__loader .v-progress-linear {
position: absolute;
}

View File

@ -0,0 +1,233 @@
import { mergeProps as _mergeProps, createVNode as _createVNode, Fragment as _Fragment } from "vue";
// Styles
import "./VOtpInput.css";
// Components
import { makeVFieldProps, VField } from "../VField/VField.mjs";
import { VOverlay } from "../VOverlay/VOverlay.mjs";
import { VProgressCircular } from "../VProgressCircular/VProgressCircular.mjs"; // Composables
import { provideDefaults } from "../../composables/defaults.mjs";
import { makeDimensionProps, useDimension } from "../../composables/dimensions.mjs";
import { makeFocusProps, useFocus } from "../../composables/focus.mjs";
import { useLocale } from "../../composables/locale.mjs";
import { useProxiedModel } from "../../composables/proxiedModel.mjs"; // Utilities
import { computed, nextTick, ref, watch } from 'vue';
import { filterInputAttrs, focusChild, genericComponent, only, propsFactory, useRender } from "../../util/index.mjs"; // Types
// Types
export const makeVOtpInputProps = propsFactory({
autofocus: Boolean,
divider: String,
focusAll: Boolean,
label: {
type: String,
default: '$vuetify.input.otp'
},
length: {
type: [Number, String],
default: 6
},
modelValue: {
type: [Number, String],
default: undefined
},
placeholder: String,
type: {
type: String,
default: 'number'
},
...makeDimensionProps(),
...makeFocusProps(),
...only(makeVFieldProps({
variant: 'outlined'
}), ['baseColor', 'bgColor', 'class', 'color', 'disabled', 'error', 'loading', 'rounded', 'style', 'theme', 'variant'])
}, 'VOtpInput');
export const VOtpInput = genericComponent()({
name: 'VOtpInput',
props: makeVOtpInputProps(),
emits: {
finish: val => true,
'update:focused': val => true,
'update:modelValue': val => true
},
setup(props, _ref) {
let {
attrs,
emit,
slots
} = _ref;
const {
dimensionStyles
} = useDimension(props);
const {
isFocused,
focus,
blur
} = useFocus(props);
const model = useProxiedModel(props, 'modelValue', '', val => String(val).split(''), val => val.join(''));
const {
t
} = useLocale();
const length = computed(() => Number(props.length));
const fields = computed(() => Array(length.value).fill(0));
const focusIndex = ref(-1);
const contentRef = ref();
const inputRef = ref([]);
const current = computed(() => inputRef.value[focusIndex.value]);
function onInput() {
// The maxlength attribute doesn't work for the number type input, so the text type is used.
// The following logic simulates the behavior of a number input.
if (props.type === 'number' && /[^0-9]/g.test(current.value.value)) {
current.value.value = '';
return;
}
const array = model.value.slice();
const value = current.value.value;
array[focusIndex.value] = value;
let target = null;
if (focusIndex.value > model.value.length) {
target = model.value.length + 1;
} else if (focusIndex.value + 1 !== length.value) {
target = 'next';
}
model.value = array;
if (target) focusChild(contentRef.value, target);
}
function onKeydown(e) {
const array = model.value.slice();
const index = focusIndex.value;
let target = null;
if (!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete'].includes(e.key)) return;
e.preventDefault();
if (e.key === 'ArrowLeft') {
target = 'prev';
} else if (e.key === 'ArrowRight') {
target = 'next';
} else if (['Backspace', 'Delete'].includes(e.key)) {
array[focusIndex.value] = '';
model.value = array;
if (focusIndex.value > 0 && e.key === 'Backspace') {
target = 'prev';
} else {
requestAnimationFrame(() => {
inputRef.value[index]?.select();
});
}
}
requestAnimationFrame(() => {
if (target != null) {
focusChild(contentRef.value, target);
}
});
}
function onPaste(index, e) {
e.preventDefault();
e.stopPropagation();
model.value = (e?.clipboardData?.getData('Text') ?? '').split('');
inputRef.value?.[index].blur();
}
function reset() {
model.value = [];
}
function onFocus(e, index) {
focus();
focusIndex.value = index;
}
function onBlur() {
blur();
focusIndex.value = -1;
}
provideDefaults({
VField: {
color: computed(() => props.color),
bgColor: computed(() => props.color),
baseColor: computed(() => props.baseColor),
disabled: computed(() => props.disabled),
error: computed(() => props.error),
variant: computed(() => props.variant)
}
}, {
scoped: true
});
watch(model, val => {
if (val.length === length.value) emit('finish', val.join(''));
}, {
deep: true
});
watch(focusIndex, val => {
if (val < 0) return;
nextTick(() => {
inputRef.value[val]?.select();
});
});
useRender(() => {
const [rootAttrs, inputAttrs] = filterInputAttrs(attrs);
return _createVNode("div", _mergeProps({
"class": ['v-otp-input', {
'v-otp-input--divided': !!props.divider
}, props.class],
"style": [props.style]
}, rootAttrs), [_createVNode("div", {
"ref": contentRef,
"class": "v-otp-input__content",
"style": [dimensionStyles.value]
}, [fields.value.map((_, i) => _createVNode(_Fragment, null, [props.divider && i !== 0 && _createVNode("span", {
"class": "v-otp-input__divider"
}, [props.divider]), _createVNode(VField, {
"focused": isFocused.value && props.focusAll || focusIndex.value === i,
"key": i
}, {
...slots,
loader: undefined,
default: () => {
return _createVNode("input", {
"ref": val => inputRef.value[i] = val,
"aria-label": t(props.label, i + 1),
"autofocus": i === 0 && props.autofocus,
"autocomplete": "one-time-code",
"class": ['v-otp-input__field'],
"disabled": props.disabled,
"inputmode": props.type === 'number' ? 'numeric' : 'text',
"min": props.type === 'number' ? 0 : undefined,
"maxlength": "1",
"placeholder": props.placeholder,
"type": props.type === 'number' ? 'text' : props.type,
"value": model.value[i],
"onInput": onInput,
"onFocus": e => onFocus(e, i),
"onBlur": onBlur,
"onKeydown": onKeydown,
"onPaste": event => onPaste(i, event)
}, null);
}
})])), _createVNode("input", _mergeProps({
"class": "v-otp-input-input",
"type": "hidden"
}, inputAttrs, {
"value": model.value.join('')
}), null), _createVNode(VOverlay, {
"contained": true,
"content-class": "v-otp-input__loader",
"model-value": !!props.loading,
"persistent": true
}, {
default: () => [slots.loader?.() ?? _createVNode(VProgressCircular, {
"color": typeof props.loading === 'boolean' ? undefined : props.loading,
"indeterminate": true,
"size": "24",
"width": "2"
}, null)]
}), slots.default?.()])]);
});
return {
blur: () => {
inputRef.value?.some(input => input.blur());
},
focus: () => {
inputRef.value?.[0].focus();
},
reset,
isFocused
};
}
});
//# sourceMappingURL=VOtpInput.mjs.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,59 @@
// Imports
@use '../../styles/settings'
@use '../../styles/tools'
@use './variables' as *
.v-otp-input
@include tools.rounded(4px)
align-items: center
display: flex
justify-content: center
padding: $otp-input-padding
position: relative
.v-field
height: 100%
.v-otp-input__divider
margin: $otp-input-divider-margin
.v-otp-input__content
align-items: center
display: flex
gap: $otp-input-content-gap
height: $otp-input-content-height
padding: $otp-input-content-padding
justify-content: center
max-width: $otp-input-content-max-width
position: relative
border-radius: inherit
.v-otp-input--divided &
max-width: $otp-input-divided-content-max-width
.v-otp-input__field
color: inherit
font-size: $otp-input-field-font-size
height: 100%
outline: none
text-align: center
width: 100%
&[type=number]::-webkit-outer-spin-button,
&[type=number]::-webkit-inner-spin-button
-webkit-appearance: none
margin: 0
&[type=number]
-moz-appearance: textfield
.v-otp-input__loader
align-items: center
display: flex
height: 100%
justify-content: center
width: 100%
.v-progress-linear
position: absolute

View File

@ -0,0 +1,11 @@
@use '../../styles/settings';
@use '../../styles/tools';
$otp-input-content-gap: .5rem !default;
$otp-input-content-height: 64px !default;
$otp-input-content-max-width: 320px !default;
$otp-input-content-padding: .5rem !default;
$otp-input-divided-content-max-width: 360px !default;
$otp-input-divider-margin: 0 8px !default;
$otp-input-field-font-size: 1.25rem !default;
$otp-input-padding: .5rem 0 !default;

View File

@ -0,0 +1,397 @@
import * as vue from 'vue';
import { ComponentPropsOptions, ExtractPropTypes, PropType } from 'vue';
interface FilterPropsOptions<PropsOptions extends Readonly<ComponentPropsOptions>, Props = ExtractPropTypes<PropsOptions>> {
filterProps<T extends Partial<Props>, U extends Exclude<keyof Props, Exclude<keyof Props, keyof T>>>(props: T): Partial<Pick<T, U>>;
}
declare const VOtpInput: {
new (...args: any[]): vue.CreateComponentPublicInstance<{
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
focused: boolean;
focusAll: boolean;
} & {
height?: string | number | undefined;
width?: string | number | undefined;
color?: string | undefined;
maxHeight?: string | number | undefined;
maxWidth?: string | number | undefined;
minHeight?: string | number | undefined;
minWidth?: string | number | undefined;
loading?: string | boolean | undefined;
class?: any;
placeholder?: string | undefined;
theme?: string | undefined;
divider?: string | undefined;
rounded?: string | number | boolean | undefined;
modelValue?: string | number | undefined;
bgColor?: string | undefined;
baseColor?: string | undefined;
'onUpdate:focused'?: ((args_0: boolean) => void) | undefined;
} & {
$children?: vue.VNodeChild | (() => vue.VNodeChild) | {
default?: (() => vue.VNodeChild) | undefined;
loader?: (() => vue.VNodeChild) | undefined;
};
'v-slots'?: {
default?: false | (() => vue.VNodeChild) | undefined;
loader?: false | (() => vue.VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => vue.VNodeChild) | undefined;
"v-slot:loader"?: false | (() => vue.VNodeChild) | undefined;
} & {
"onUpdate:modelValue"?: ((val: string) => any) | undefined;
"onUpdate:focused"?: ((val: boolean) => any) | undefined;
onFinish?: ((val: string) => any) | undefined;
}, {
blur: () => void;
focus: () => void;
reset: () => void;
isFocused: vue.Ref<boolean> & {
readonly externalValue: boolean;
};
}, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
finish: (val: string) => true;
'update:focused': (val: boolean) => true;
'update:modelValue': (val: string) => true;
}, vue.VNodeProps & vue.AllowedComponentProps & vue.ComponentCustomProps & {
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
focused: boolean;
focusAll: boolean;
} & {
height?: string | number | undefined;
width?: string | number | undefined;
color?: string | undefined;
maxHeight?: string | number | undefined;
maxWidth?: string | number | undefined;
minHeight?: string | number | undefined;
minWidth?: string | number | undefined;
loading?: string | boolean | undefined;
class?: any;
placeholder?: string | undefined;
theme?: string | undefined;
divider?: string | undefined;
rounded?: string | number | boolean | undefined;
modelValue?: string | number | undefined;
bgColor?: string | undefined;
baseColor?: string | undefined;
'onUpdate:focused'?: ((args_0: boolean) => void) | undefined;
} & {
$children?: vue.VNodeChild | (() => vue.VNodeChild) | {
default?: (() => vue.VNodeChild) | undefined;
loader?: (() => vue.VNodeChild) | undefined;
};
'v-slots'?: {
default?: false | (() => vue.VNodeChild) | undefined;
loader?: false | (() => vue.VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => vue.VNodeChild) | undefined;
"v-slot:loader"?: false | (() => vue.VNodeChild) | undefined;
} & {
"onUpdate:modelValue"?: ((val: string) => any) | undefined;
"onUpdate:focused"?: ((val: boolean) => any) | undefined;
onFinish?: ((val: string) => any) | undefined;
}, {
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
rounded: string | number | boolean;
modelValue: string | number;
focused: boolean;
focusAll: boolean;
}, true, {}, vue.SlotsType<Partial<{
default: () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>[];
loader: () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>[];
}>>, {
P: {};
B: {};
D: {};
C: {};
M: {};
Defaults: {};
}, {
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
focused: boolean;
focusAll: boolean;
} & {
height?: string | number | undefined;
width?: string | number | undefined;
color?: string | undefined;
maxHeight?: string | number | undefined;
maxWidth?: string | number | undefined;
minHeight?: string | number | undefined;
minWidth?: string | number | undefined;
loading?: string | boolean | undefined;
class?: any;
placeholder?: string | undefined;
theme?: string | undefined;
divider?: string | undefined;
rounded?: string | number | boolean | undefined;
modelValue?: string | number | undefined;
bgColor?: string | undefined;
baseColor?: string | undefined;
'onUpdate:focused'?: ((args_0: boolean) => void) | undefined;
} & {
$children?: vue.VNodeChild | (() => vue.VNodeChild) | {
default?: (() => vue.VNodeChild) | undefined;
loader?: (() => vue.VNodeChild) | undefined;
};
'v-slots'?: {
default?: false | (() => vue.VNodeChild) | undefined;
loader?: false | (() => vue.VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => vue.VNodeChild) | undefined;
"v-slot:loader"?: false | (() => vue.VNodeChild) | undefined;
} & {
"onUpdate:modelValue"?: ((val: string) => any) | undefined;
"onUpdate:focused"?: ((val: boolean) => any) | undefined;
onFinish?: ((val: string) => any) | undefined;
}, {
blur: () => void;
focus: () => void;
reset: () => void;
isFocused: vue.Ref<boolean> & {
readonly externalValue: boolean;
};
}, {}, {}, {}, {
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
rounded: string | number | boolean;
modelValue: string | number;
focused: boolean;
focusAll: boolean;
}>;
__isFragment?: undefined;
__isTeleport?: undefined;
__isSuspense?: undefined;
} & vue.ComponentOptionsBase<{
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
focused: boolean;
focusAll: boolean;
} & {
height?: string | number | undefined;
width?: string | number | undefined;
color?: string | undefined;
maxHeight?: string | number | undefined;
maxWidth?: string | number | undefined;
minHeight?: string | number | undefined;
minWidth?: string | number | undefined;
loading?: string | boolean | undefined;
class?: any;
placeholder?: string | undefined;
theme?: string | undefined;
divider?: string | undefined;
rounded?: string | number | boolean | undefined;
modelValue?: string | number | undefined;
bgColor?: string | undefined;
baseColor?: string | undefined;
'onUpdate:focused'?: ((args_0: boolean) => void) | undefined;
} & {
$children?: vue.VNodeChild | (() => vue.VNodeChild) | {
default?: (() => vue.VNodeChild) | undefined;
loader?: (() => vue.VNodeChild) | undefined;
};
'v-slots'?: {
default?: false | (() => vue.VNodeChild) | undefined;
loader?: false | (() => vue.VNodeChild) | undefined;
} | undefined;
} & {
"v-slot:default"?: false | (() => vue.VNodeChild) | undefined;
"v-slot:loader"?: false | (() => vue.VNodeChild) | undefined;
} & {
"onUpdate:modelValue"?: ((val: string) => any) | undefined;
"onUpdate:focused"?: ((val: boolean) => any) | undefined;
onFinish?: ((val: string) => any) | undefined;
}, {
blur: () => void;
focus: () => void;
reset: () => void;
isFocused: vue.Ref<boolean> & {
readonly externalValue: boolean;
};
}, unknown, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {
finish: (val: string) => true;
'update:focused': (val: boolean) => true;
'update:modelValue': (val: string) => true;
}, string, {
length: string | number;
variant: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
type: "number" | "text" | "password";
error: boolean;
label: string;
style: vue.StyleValue;
autofocus: boolean;
disabled: boolean;
rounded: string | number | boolean;
modelValue: string | number;
focused: boolean;
focusAll: boolean;
}, {}, string, vue.SlotsType<Partial<{
default: () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>[];
loader: () => vue.VNode<vue.RendererNode, vue.RendererElement, {
[key: string]: any;
}>[];
}>>> & vue.VNodeProps & vue.AllowedComponentProps & vue.ComponentCustomProps & FilterPropsOptions<{
variant: Omit<{
type: PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
default: string;
validator: (v: any) => boolean;
}, "type" | "default"> & {
type: PropType<NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">>;
default: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
};
error: BooleanConstructor;
color: StringConstructor;
loading: (StringConstructor | BooleanConstructor)[];
style: {
type: PropType<vue.StyleValue>;
default: null;
};
disabled: {
type: BooleanConstructor;
default: null;
};
class: PropType<any>;
theme: StringConstructor;
rounded: {
type: (StringConstructor | BooleanConstructor | NumberConstructor)[];
default: undefined;
};
bgColor: StringConstructor;
baseColor: StringConstructor;
focused: BooleanConstructor;
'onUpdate:focused': PropType<(args_0: boolean) => void>;
height: (StringConstructor | NumberConstructor)[];
maxHeight: (StringConstructor | NumberConstructor)[];
maxWidth: (StringConstructor | NumberConstructor)[];
minHeight: (StringConstructor | NumberConstructor)[];
minWidth: (StringConstructor | NumberConstructor)[];
width: (StringConstructor | NumberConstructor)[];
autofocus: BooleanConstructor;
divider: StringConstructor;
focusAll: BooleanConstructor;
label: {
type: StringConstructor;
default: string;
};
length: {
type: (StringConstructor | NumberConstructor)[];
default: number;
};
modelValue: {
type: (StringConstructor | NumberConstructor)[];
default: undefined;
};
placeholder: StringConstructor;
type: {
type: PropType<"number" | "text" | "password">;
default: string;
};
}, vue.ExtractPropTypes<{
variant: Omit<{
type: PropType<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
default: string;
validator: (v: any) => boolean;
}, "type" | "default"> & {
type: PropType<NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">>;
default: NonNullable<"filled" | "outlined" | "plain" | "underlined" | "solo" | "solo-inverted" | "solo-filled">;
};
error: BooleanConstructor;
color: StringConstructor;
loading: (StringConstructor | BooleanConstructor)[];
style: {
type: PropType<vue.StyleValue>;
default: null;
};
disabled: {
type: BooleanConstructor;
default: null;
};
class: PropType<any>;
theme: StringConstructor;
rounded: {
type: (StringConstructor | BooleanConstructor | NumberConstructor)[];
default: undefined;
};
bgColor: StringConstructor;
baseColor: StringConstructor;
focused: BooleanConstructor;
'onUpdate:focused': PropType<(args_0: boolean) => void>;
height: (StringConstructor | NumberConstructor)[];
maxHeight: (StringConstructor | NumberConstructor)[];
maxWidth: (StringConstructor | NumberConstructor)[];
minHeight: (StringConstructor | NumberConstructor)[];
minWidth: (StringConstructor | NumberConstructor)[];
width: (StringConstructor | NumberConstructor)[];
autofocus: BooleanConstructor;
divider: StringConstructor;
focusAll: BooleanConstructor;
label: {
type: StringConstructor;
default: string;
};
length: {
type: (StringConstructor | NumberConstructor)[];
default: number;
};
modelValue: {
type: (StringConstructor | NumberConstructor)[];
default: undefined;
};
placeholder: StringConstructor;
type: {
type: PropType<"number" | "text" | "password">;
default: string;
};
}>>;
type VOtpInput = InstanceType<typeof VOtpInput>;
export { VOtpInput };

View File

@ -0,0 +1,2 @@
export { VOtpInput } from "./VOtpInput.mjs";
//# sourceMappingURL=index.mjs.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.mjs","names":["VOtpInput"],"sources":["../../../src/components/VOtpInput/index.ts"],"sourcesContent":["export { VOtpInput } from './VOtpInput'\n"],"mappings":"SAASA,SAAS"}