@m11g/library 0.0.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/.editorconfig +8 -0
- package/.gitattributes +1 -0
- package/.oxfmtrc.json +5 -0
- package/.oxlintrc.json +10 -0
- package/.stylelintrc +33 -0
- package/.vscode/extensions.json +8 -0
- package/README.md +48 -0
- package/env.d.ts +1 -0
- package/eslint.config.ts +35 -0
- package/index.html +13 -0
- package/package.json +61 -0
- package/postcss.config.ts +23 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +28 -0
- package/src/assets/css/base.css +2 -0
- package/src/assets/css/elements/_base.css +5 -0
- package/src/assets/css/elements/buttons.css +23 -0
- package/src/assets/css/elements/form.css +75 -0
- package/src/assets/css/elements/scaffolding.css +62 -0
- package/src/assets/css/elements/transition.css +4 -0
- package/src/assets/css/elements/typography.css +80 -0
- package/src/assets/css/media-queries/media-queries.css +3 -0
- package/src/assets/css/mixins/list-reset.css +5 -0
- package/src/assets/css/variables/_base.css +7 -0
- package/src/assets/css/variables/chakra.css +160 -0
- package/src/assets/css/variables/colors.css +205 -0
- package/src/assets/css/variables/grid.css +6 -0
- package/src/assets/css/variables/spacings.css +10 -0
- package/src/assets/css/variables/transitions.css +10 -0
- package/src/assets/css/variables/typography.css +17 -0
- package/src/components/AppBreadcrumb.vue +63 -0
- package/src/components/AppButton.vue +105 -0
- package/src/components/AppCarousel.vue +145 -0
- package/src/components/AppDialog.vue +126 -0
- package/src/components/AppNotification.vue +68 -0
- package/src/components/AppPagination.vue +127 -0
- package/src/components/AppPopover.vue +57 -0
- package/src/components/AppTabs.vue +176 -0
- package/src/components/CenterWrapper.vue +50 -0
- package/src/components/ClickableWrapper.vue +48 -0
- package/src/components/Forms/AppForm.vue +61 -0
- package/src/components/Forms/FormErrorMessage.vue +17 -0
- package/src/components/Forms/FormField.vue +30 -0
- package/src/components/Forms/FormFieldset.vue +28 -0
- package/src/components/Forms/RadioGroup.vue +85 -0
- package/src/components/Forms/SearchField.vue +38 -0
- package/src/components/Forms/SelectField.vue +125 -0
- package/src/components/Forms/TextField.vue +41 -0
- package/src/components/Forms/TextareaField.vue +32 -0
- package/src/components/ScrollSpy.vue +111 -0
- package/src/components/SummaryDetails.vue +83 -0
- package/src/composables/useTheme.ts +20 -0
- package/src/index.ts +48 -0
- package/src/main.ts +10 -0
- package/src/router/index.ts +26 -0
- package/src/types/Theme.ts +11 -0
- package/src/views/CarouselView.vue +27 -0
- package/src/views/FormView.vue +111 -0
- package/src/views/HomeView.vue +100 -0
- package/src/views/ScrollSpyView.vue +58 -0
- package/theme-dark +117 -0
- package/theme-light +191 -0
- package/tsconfig.app.json +18 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +28 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
size?: "full" | "md" | "lg" | "sm";
|
|
5
|
+
top?: boolean;
|
|
6
|
+
}>(),
|
|
7
|
+
{
|
|
8
|
+
top: false,
|
|
9
|
+
size: "lg",
|
|
10
|
+
},
|
|
11
|
+
);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="notch" :class="{ top }">
|
|
16
|
+
<div class="wrapper" :class="`wrapper-${size}`">
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<style lang="css" scoped>
|
|
23
|
+
.notch {
|
|
24
|
+
padding: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
|
|
25
|
+
|
|
26
|
+
&.top {
|
|
27
|
+
padding-block-start: env(safe-area-inset-top);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wrapper-sm {
|
|
32
|
+
--wrapper-size: var(--container-size-sm);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.wrapper-md {
|
|
36
|
+
--wrapper-size: var(--container-size-md);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.wrapper-lg {
|
|
40
|
+
--wrapper-size: var(--container-size-lg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.wrapper {
|
|
44
|
+
/* stylelint-disable-next-line length-zero-no-unit */
|
|
45
|
+
--zero: 0px;
|
|
46
|
+
|
|
47
|
+
padding-inline: var(--gutter);
|
|
48
|
+
margin-inline: max(var(--zero), ((100% - var(--wrapper-size)) / 2));
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, type Ref } from "vue";
|
|
3
|
+
import { useRouter, type RouteLocationRaw } from "vue-router";
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
to: RouteLocationRaw;
|
|
8
|
+
tag?: "div" | "li";
|
|
9
|
+
}>(),
|
|
10
|
+
{
|
|
11
|
+
tag: "li",
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const down: Ref<number | null> = ref(null);
|
|
16
|
+
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
|
|
19
|
+
const mouseDown = (event: MouseEvent) => {
|
|
20
|
+
const target = event.target as Element;
|
|
21
|
+
if (target.nodeName.toUpperCase() === "A" || target.parentNode?.nodeName.toUpperCase() === "A") {
|
|
22
|
+
down.value = null;
|
|
23
|
+
} else {
|
|
24
|
+
down.value = +new Date();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const mouseUp = async () => {
|
|
28
|
+
if (!down.value) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const up = +new Date();
|
|
32
|
+
if (up - down.value < 200) {
|
|
33
|
+
await router.push(props.to);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<component :is="tag" class="item" @mousedown.left="mouseDown" @mouseup.left="mouseUp">
|
|
40
|
+
<slot />
|
|
41
|
+
</component>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style lang="css" scoped>
|
|
45
|
+
.item {
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { FormErrors } from "vee-validate";
|
|
3
|
+
import AppButton from "@/components/AppButton.vue";
|
|
4
|
+
import AppNotification from "@/components/AppNotification.vue";
|
|
5
|
+
import { computed, ref } from "vue";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
buttonTitle: string;
|
|
9
|
+
successText: string;
|
|
10
|
+
status: "idle" | "pending" | "success" | "error";
|
|
11
|
+
errors: FormErrors<Record<string, string>>;
|
|
12
|
+
}>();
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
(event: "submit-form"): void;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const errorList = computed(() => {
|
|
19
|
+
return Object.values(props.errors);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const touched = ref(false);
|
|
23
|
+
|
|
24
|
+
const submit = () => {
|
|
25
|
+
touched.value = true;
|
|
26
|
+
emit("submit-form");
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<app-notification v-if="status === 'success'" :title="successText" variant="success" />
|
|
32
|
+
<form v-else method="post" novalidate @submit.prevent="submit">
|
|
33
|
+
<slot />
|
|
34
|
+
<div aria-live="assertive">
|
|
35
|
+
<app-notification
|
|
36
|
+
v-if="errorList.length && touched"
|
|
37
|
+
variant="warning"
|
|
38
|
+
title="Het formulier is niet correct ingevuld"
|
|
39
|
+
>
|
|
40
|
+
<ul>
|
|
41
|
+
<li v-for="error in errors" :key="error">
|
|
42
|
+
{{ error }}
|
|
43
|
+
</li>
|
|
44
|
+
</ul>
|
|
45
|
+
</app-notification>
|
|
46
|
+
</div>
|
|
47
|
+
<app-button :title="buttonTitle" type="submit" :disabled="status === 'pending'" />
|
|
48
|
+
</form>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style lang="css" scoped>
|
|
52
|
+
ul {
|
|
53
|
+
padding-inline-start: 0;
|
|
54
|
+
margin: 0;
|
|
55
|
+
list-style: none outside;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
[aria-live="assertive"]:not(:empty) {
|
|
59
|
+
margin-block-start: var(--spacing-4);
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
defineProps<{
|
|
3
|
+
errorMessage?: string;
|
|
4
|
+
}>();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div aria-live="assertive">
|
|
9
|
+
{{ errorMessage }}
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style lang="css" scoped>
|
|
14
|
+
span {
|
|
15
|
+
display: block;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import FormErrorMessage from "./FormErrorMessage.vue";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div>
|
|
13
|
+
<label :for="id" class="label">
|
|
14
|
+
{{ title }}
|
|
15
|
+
</label>
|
|
16
|
+
<slot />
|
|
17
|
+
<form-error-message :error-message="errorMessage" class="error-message" />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<style lang="css" scoped>
|
|
22
|
+
label {
|
|
23
|
+
margin-block-end: var(--spacing-xxs);
|
|
24
|
+
font-weight: var(--font-weight-bold);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.error-message {
|
|
28
|
+
margin-block-start: var(--spacing-xxs);
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
title: string;
|
|
5
|
+
columns?: number;
|
|
6
|
+
}>(),
|
|
7
|
+
{
|
|
8
|
+
columns: 1,
|
|
9
|
+
},
|
|
10
|
+
);
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<fieldset>
|
|
15
|
+
<legend>
|
|
16
|
+
{{ title }}
|
|
17
|
+
</legend>
|
|
18
|
+
<slot />
|
|
19
|
+
</fieldset>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<style lang="css" scoped>
|
|
23
|
+
fieldset {
|
|
24
|
+
display: grid;
|
|
25
|
+
grid-template-columns: repeat(v-bind(columns), 1fr);
|
|
26
|
+
gap: var(--gutter);
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useField } from "vee-validate";
|
|
3
|
+
import { toRefs, useId } from "vue";
|
|
4
|
+
import FormErrorMessage from "./FormErrorMessage.vue";
|
|
5
|
+
import useTheme from "@/composables/useTheme.ts";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
// type: 'directors' | 'authors'
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
options: {
|
|
12
|
+
value: string | number;
|
|
13
|
+
title: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const id = useId();
|
|
18
|
+
|
|
19
|
+
const { name } = toRefs(props);
|
|
20
|
+
|
|
21
|
+
// we don't provide any rules here because we are using form-level validation
|
|
22
|
+
// https://vee-validate.logaretm.com/v4/guide/validation#form-level-validation
|
|
23
|
+
const { value, handleChange, errorMessage } = useField<string>(name);
|
|
24
|
+
|
|
25
|
+
const validationListeners = {
|
|
26
|
+
change: handleChange,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const { colors } = useTheme()
|
|
30
|
+
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<fieldset>
|
|
35
|
+
<legend>{{ title }}</legend>
|
|
36
|
+
<div class="fields">
|
|
37
|
+
<div v-for="(option, index) in options" :key="option.value" class="field">
|
|
38
|
+
<input
|
|
39
|
+
:id="`${id}-${index}`"
|
|
40
|
+
type="radio"
|
|
41
|
+
:checked="value === option.value"
|
|
42
|
+
:name
|
|
43
|
+
:value="option.value"
|
|
44
|
+
v-on="validationListeners"
|
|
45
|
+
/>
|
|
46
|
+
<label :for="`${id}-${index}`">
|
|
47
|
+
{{ option.title }}
|
|
48
|
+
</label>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<form-error-message :error-message="errorMessage" />
|
|
52
|
+
</fieldset>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style lang="css" scoped>
|
|
56
|
+
.field {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: var(--spacing-2);
|
|
59
|
+
align-items: start;
|
|
60
|
+
|
|
61
|
+
&:not(:last-child) {
|
|
62
|
+
margin-block-end: var(--spacing-1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
input {
|
|
67
|
+
flex: 0 0 auto;
|
|
68
|
+
inline-size: 1.25em;
|
|
69
|
+
aspect-ratio: 1;
|
|
70
|
+
padding: 0;
|
|
71
|
+
margin: 0;
|
|
72
|
+
margin-block-start: 0.1em;
|
|
73
|
+
appearance: none;
|
|
74
|
+
background: var(--color-panel-bg);
|
|
75
|
+
border: 3px solid var(--color-white);
|
|
76
|
+
border-radius: 50%;
|
|
77
|
+
transition:
|
|
78
|
+
border var(--transition),
|
|
79
|
+
background var(--transition);
|
|
80
|
+
|
|
81
|
+
&:checked {
|
|
82
|
+
background-color: v-bind(colors.solid);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { Icon } from "@iconify/vue";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
}>();
|
|
7
|
+
|
|
8
|
+
const model = defineModel<string>({
|
|
9
|
+
required: true,
|
|
10
|
+
});
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="field">
|
|
15
|
+
<input v-model="model" :placeholder name="search" type="search" />
|
|
16
|
+
<icon icon="solar:minimalistic-magnifer-outline" />
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<style lang="css" scoped>
|
|
21
|
+
.field {
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input {
|
|
26
|
+
padding-inline-end: 3rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
svg {
|
|
30
|
+
position: absolute;
|
|
31
|
+
inset-block-start: 50%;
|
|
32
|
+
inset-inline-end: var(--spacing-4);
|
|
33
|
+
inline-size: 1.5rem;
|
|
34
|
+
block-size: auto;
|
|
35
|
+
aspect-ratio: 1;
|
|
36
|
+
translate: 0 -0.75rem;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useField } from "vee-validate";
|
|
3
|
+
import { toRefs, useId } from "vue";
|
|
4
|
+
import FormField from "./FormField.vue";
|
|
5
|
+
import { Icon } from "@iconify/vue";
|
|
6
|
+
import useTheme from "@/composables/useTheme.ts";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
// type: 'directors' | 'authors'
|
|
10
|
+
name: string;
|
|
11
|
+
title: string;
|
|
12
|
+
options: {
|
|
13
|
+
value: string | number;
|
|
14
|
+
title: string;
|
|
15
|
+
}[];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const id = useId();
|
|
19
|
+
|
|
20
|
+
const { name } = toRefs(props);
|
|
21
|
+
|
|
22
|
+
// we don't provide any rules here because we are using form-level validation
|
|
23
|
+
// https://vee-validate.logaretm.com/v4/guide/validation#form-level-validation
|
|
24
|
+
const { value, handleChange, errorMessage } = useField<string>(name, undefined, {
|
|
25
|
+
validateOnValueUpdate: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const validationListeners = {
|
|
29
|
+
change: handleChange,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const { colors } = useTheme()
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<form-field :id :title :error-message>
|
|
37
|
+
<select :id :value :name v-on="validationListeners">
|
|
38
|
+
<button>
|
|
39
|
+
<selectedcontent />
|
|
40
|
+
<icon icon="solar:alt-arrow-down-outline" class="icon" />
|
|
41
|
+
</button>
|
|
42
|
+
<option v-for="option in options" :key="option.value" :value="option.value">
|
|
43
|
+
{{ option.title }}
|
|
44
|
+
</option>
|
|
45
|
+
</select>
|
|
46
|
+
</form-field>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<style lang="css" scoped>
|
|
50
|
+
.icon {
|
|
51
|
+
transition: rotate var(--transition);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
select {
|
|
55
|
+
appearance: base-select;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
|
|
58
|
+
&:open {
|
|
59
|
+
.icon {
|
|
60
|
+
rotate: -180deg;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
button {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: var(--spacing-2);
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
inline-size: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
::checkmark {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
::picker-icon {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
::picker(select) {
|
|
82
|
+
margin-block-start: var(--spacing-1);
|
|
83
|
+
appearance: base-select;
|
|
84
|
+
border: 2px solid var(--color-white);
|
|
85
|
+
opacity: 0;
|
|
86
|
+
translate: 0 calc(-1 * var(--spacing-4));
|
|
87
|
+
transition:
|
|
88
|
+
translate var(--transition),
|
|
89
|
+
opacity var(--transition),
|
|
90
|
+
overlay var(--transition) allow-discrete,
|
|
91
|
+
display var(--transition) allow-discrete;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:open::picker(select) {
|
|
95
|
+
opacity: 1;
|
|
96
|
+
translate: 0 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@starting-style {
|
|
100
|
+
:open::picker(select) {
|
|
101
|
+
opacity: 0;
|
|
102
|
+
translate: 0 calc(-1 * var(--spacing-4));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
option {
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: var(--spacing-2);
|
|
109
|
+
padding: var(--spacing-2);
|
|
110
|
+
color: var(--color-fg);
|
|
111
|
+
background-color: var(--color-body-background);
|
|
112
|
+
border-inline-start: 3px solid transparent;
|
|
113
|
+
|
|
114
|
+
&:hover {
|
|
115
|
+
color: v-bind(colors.fg);
|
|
116
|
+
background-color: var(--color-primary-hover);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&:checked {
|
|
120
|
+
font-weight: var(--font-weight-bold);
|
|
121
|
+
color: v-bind(colors.fg);
|
|
122
|
+
background-color: var(--color-primary);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useField } from "vee-validate";
|
|
3
|
+
import { toRefs, useId, type InputTypeHTMLAttribute } from "vue";
|
|
4
|
+
import FormField from "./FormField.vue";
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
type?: InputTypeHTMLAttribute;
|
|
9
|
+
autocomplete?: AutoFillField;
|
|
10
|
+
title: string;
|
|
11
|
+
name: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
autocomplete: undefined,
|
|
16
|
+
type: "text",
|
|
17
|
+
placeholder: undefined,
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const id = useId();
|
|
22
|
+
const { name } = toRefs(props);
|
|
23
|
+
|
|
24
|
+
// we don't provide any rules here because we are using form-level validation
|
|
25
|
+
// https://vee-validate.logaretm.com/v4/guide/validation#form-level-validation
|
|
26
|
+
const { value, handleBlur, handleChange, errorMessage } = useField<string>(name, undefined, {
|
|
27
|
+
validateOnValueUpdate: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const validationListeners = {
|
|
31
|
+
blur: (event: InputEvent) => handleBlur(event, true),
|
|
32
|
+
change: handleChange,
|
|
33
|
+
input: (event: InputEvent) => handleChange(event, !!errorMessage.value),
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<form-field :id :title :error-message>
|
|
39
|
+
<input :id :name :type :autocomplete :value :placeholder v-on="validationListeners" />
|
|
40
|
+
</form-field>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useField } from "vee-validate";
|
|
3
|
+
import { toRefs, useId } from "vue";
|
|
4
|
+
import FormField from "./FormField.vue";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
title: string;
|
|
8
|
+
name: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const id = useId();
|
|
13
|
+
const { name } = toRefs(props);
|
|
14
|
+
|
|
15
|
+
// we don't provide any rules here because we are using form-level validation
|
|
16
|
+
// https://vee-validate.logaretm.com/v4/guide/validation#form-level-validation
|
|
17
|
+
const { value, handleBlur, handleChange, errorMessage } = useField<string>(name, undefined, {
|
|
18
|
+
validateOnValueUpdate: false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const validationListeners = {
|
|
22
|
+
blur: (event: InputEvent) => handleBlur(event, true),
|
|
23
|
+
change: handleChange,
|
|
24
|
+
input: (event: InputEvent) => handleChange(event, !!errorMessage.value),
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<form-field :id :title :name :error-message>
|
|
30
|
+
<textarea :id :value :name :placeholder v-on="validationListeners" />
|
|
31
|
+
</form-field>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import useTheme from "@/composables/useTheme";
|
|
3
|
+
import { computed, useId } from "vue";
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
pages: {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const id = useId();
|
|
13
|
+
|
|
14
|
+
const anchor = computed(() => {
|
|
15
|
+
return `--${id}`;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const { colors} = useTheme()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="content">
|
|
23
|
+
<div>
|
|
24
|
+
<section v-for="page in pages" :key="page.title">
|
|
25
|
+
<span :id="page.id" />
|
|
26
|
+
<h2>{{ page.title }}</h2>
|
|
27
|
+
<slot :name="page.id" />
|
|
28
|
+
</section>
|
|
29
|
+
</div>
|
|
30
|
+
<nav>
|
|
31
|
+
<ol>
|
|
32
|
+
<li v-for="item in pages" :id="`link-${item.title}`" :key="item.title">
|
|
33
|
+
<a :href="`#${item.id}`">
|
|
34
|
+
{{ item.title }}
|
|
35
|
+
</a>
|
|
36
|
+
</li>
|
|
37
|
+
</ol>
|
|
38
|
+
</nav>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<style lang="css" scoped>
|
|
43
|
+
.content {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: grid;
|
|
46
|
+
gap: var(--gutter);
|
|
47
|
+
align-items: start;
|
|
48
|
+
|
|
49
|
+
@media (--md) {
|
|
50
|
+
grid-template-columns: auto 12rem;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
span {
|
|
55
|
+
display: block;
|
|
56
|
+
translate: 0 calc(var(--spacing-4) * -1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
nav {
|
|
60
|
+
position: sticky;
|
|
61
|
+
inset-block-start: var(--spacing-4);
|
|
62
|
+
display: none;
|
|
63
|
+
order: -1;
|
|
64
|
+
background: var(--color-bg-panel);
|
|
65
|
+
scroll-target-group: auto;
|
|
66
|
+
|
|
67
|
+
@media (--md) {
|
|
68
|
+
display: block;
|
|
69
|
+
order: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ol {
|
|
74
|
+
padding: 0;
|
|
75
|
+
margin: 0;
|
|
76
|
+
list-style: none outside;
|
|
77
|
+
|
|
78
|
+
&::before {
|
|
79
|
+
position: absolute;
|
|
80
|
+
inset-block-start: anchor(top);
|
|
81
|
+
inset-inline-start: 0;
|
|
82
|
+
display: block;
|
|
83
|
+
inline-size: 0.25em;
|
|
84
|
+
/* stylelint-disable-next-line declaration-property-value-no-unknown */
|
|
85
|
+
block-size: anchor-size(block);
|
|
86
|
+
position-anchor: v-bind(anchor);
|
|
87
|
+
content: "";
|
|
88
|
+
background: v-bind(colors.solid);
|
|
89
|
+
transition:
|
|
90
|
+
top var(--transition),
|
|
91
|
+
height var(--transition);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
a {
|
|
96
|
+
display: block;
|
|
97
|
+
padding-block: var(--spacing-1);
|
|
98
|
+
padding-inline-start: var(--spacing-3);
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
border-inline-start: 0.25em solid v-bind(colors.subtle);
|
|
101
|
+
|
|
102
|
+
&:target-current {
|
|
103
|
+
anchor-name: v-bind(anchor);
|
|
104
|
+
color: v-bind(colors.solid);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&:hover {
|
|
108
|
+
border-color: v-bind(colors.solid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</style>
|