srcdev-nuxt-components 9.6.8 → 9.6.10
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/.claude/skills/component-compliance-checklist.md +28 -1
- package/.claude/skills/component-inline-action-button.md +1 -1
- package/.claude/skills/component-local-style-override.md +1 -1
- package/.claude/skills/component-naming.md +17 -12
- package/.claude/skills/components/container-glow.md +2 -2
- package/.claude/skills/components/form-field.md +2 -2
- package/.claude/skills/components/form-wrapper.md +1 -1
- package/.claude/skills/components/glowing-border.md +1 -1
- package/.claude/skills/components/input-description.md +7 -7
- package/.claude/skills/components/input-error.md +3 -3
- package/.claude/skills/components/input-label.md +7 -8
- package/.claude/skills/components/input-number.md +17 -17
- package/.claude/skills/components/input-select.md +153 -0
- package/.claude/skills/components/input-text.md +220 -0
- package/.claude/skills/components/input-textarea.md +147 -0
- package/.claude/skills/components/select-menu.md +4 -4
- package/.claude/skills/components/toggle-switch.md +143 -0
- package/.claude/skills/composable-zod-validation.md +1 -1
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/icon-sizing.md +1 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +1 -1
- package/.claude/skills/theming-component-token-pattern.md +8 -8
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.vscode/srcdev-component-input-select.code-snippets +20 -14
- package/.vscode/srcdev-component-input-text.code-snippets +46 -40
- package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
- package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
- package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
- package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
- package/app/components/05.forms/input-select/InputSelect.vue +198 -0
- package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
- package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
- package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
- package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
- package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
- package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
- package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
- package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
- package/app/components/05.forms/input-text/InputTextField.vue +168 -0
- package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
- package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
- package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
- package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
- package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
- package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
- package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
- package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
- package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
- package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
- package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
- package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
- package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
- package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
- package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
- package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
- package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
- package/package.json +1 -1
- package/.claude/skills/components/input-select-core.md +0 -151
- package/.claude/skills/components/input-text-core.md +0 -204
- package/.claude/skills/components/input-textarea-core.md +0 -154
- package/.claude/skills/components/toggle-switch-core.md +0 -169
- package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
- package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
- package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
- package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
- package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
- package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
- package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
- package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
- package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
- package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
- package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
- package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
- package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
- package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabelInline.spec.ts +0 -66
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
-
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
-
import ComponentUnderTest from "../InputTextCore.vue";
|
|
5
|
-
|
|
6
|
-
const initialPropsData = {
|
|
7
|
-
id: "test-input",
|
|
8
|
-
name: "testName",
|
|
9
|
-
modelValue: "",
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
13
|
-
const wrapperFactory = (propsData = {}) => {
|
|
14
|
-
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
15
|
-
|
|
16
|
-
return mountSuspended(ComponentUnderTest, {
|
|
17
|
-
attachTo: document.body,
|
|
18
|
-
props: mockPropsData,
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
describe("InputTextCore", () => {
|
|
23
|
-
it("mounts without error", async () => {
|
|
24
|
-
wrapper = await wrapperFactory();
|
|
25
|
-
expect(wrapper.vm).toBeTruthy();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("does not set min/max attributes when not provided", async () => {
|
|
29
|
-
wrapper = await wrapperFactory();
|
|
30
|
-
const input = wrapper.find("input");
|
|
31
|
-
|
|
32
|
-
expect(input.attributes("min")).toBeUndefined();
|
|
33
|
-
expect(input.attributes("max")).toBeUndefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("passes through a string min/max (e.g. for type=date)", async () => {
|
|
37
|
-
wrapper = await wrapperFactory({
|
|
38
|
-
type: "date",
|
|
39
|
-
min: "2026-01-01",
|
|
40
|
-
max: "2026-12-31",
|
|
41
|
-
});
|
|
42
|
-
const input = wrapper.find("input");
|
|
43
|
-
|
|
44
|
-
expect(input.attributes("type")).toBe("date");
|
|
45
|
-
expect(input.attributes("min")).toBe("2026-01-01");
|
|
46
|
-
expect(input.attributes("max")).toBe("2026-12-31");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("passes through a numeric min/max (e.g. for type=number)", async () => {
|
|
50
|
-
wrapper = await wrapperFactory({
|
|
51
|
-
type: "number",
|
|
52
|
-
min: 0,
|
|
53
|
-
max: 100,
|
|
54
|
-
});
|
|
55
|
-
const input = wrapper.find("input");
|
|
56
|
-
|
|
57
|
-
expect(input.attributes("min")).toBe("0");
|
|
58
|
-
expect(input.attributes("max")).toBe("100");
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// https://nuxt.com/docs/getting-started/testing#unit-testing
|
|
2
|
-
import type { VueWrapper } from "@vue/test-utils";
|
|
3
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
4
|
-
import ComponentUnderTest from "../InputTextWithLabel.vue";
|
|
5
|
-
import { defineComponent, nextTick, ref } from "vue";
|
|
6
|
-
|
|
7
|
-
const initialPropsData = {
|
|
8
|
-
type: "text" as const,
|
|
9
|
-
name: "testName",
|
|
10
|
-
label: "Test label",
|
|
11
|
-
errorMessage: "",
|
|
12
|
-
modelValue: "",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
|
|
16
|
-
const wrapperFactory = (propsData = {}) => {
|
|
17
|
-
const mockPropsData = { ...initialPropsData, ...propsData };
|
|
18
|
-
|
|
19
|
-
return mountSuspended(ComponentUnderTest, {
|
|
20
|
-
attachTo: document.body,
|
|
21
|
-
props: mockPropsData,
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
describe("InputTextWithLabel", () => {
|
|
26
|
-
it("mounts without error", async () => {
|
|
27
|
-
wrapper = await wrapperFactory();
|
|
28
|
-
expect(wrapper.vm).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("renders the label text", async () => {
|
|
32
|
-
wrapper = await wrapperFactory({ label: "Email address" });
|
|
33
|
-
expect(wrapper.text()).toContain("Email address");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("forwards min/max through to the underlying InputTextCore input (e.g. for type=date)", async () => {
|
|
37
|
-
wrapper = await wrapperFactory({
|
|
38
|
-
type: "date",
|
|
39
|
-
min: "2026-01-01",
|
|
40
|
-
max: "2026-12-31",
|
|
41
|
-
});
|
|
42
|
-
const input = wrapper.find("input");
|
|
43
|
-
|
|
44
|
-
expect(input.attributes("type")).toBe("date");
|
|
45
|
-
expect(input.attributes("min")).toBe("2026-01-01");
|
|
46
|
-
expect(input.attributes("max")).toBe("2026-12-31");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("points aria-describedby at the description once it appears after mount", async () => {
|
|
50
|
-
const Host = defineComponent({
|
|
51
|
-
components: { ComponentUnderTest },
|
|
52
|
-
setup() {
|
|
53
|
-
return { modelValue: ref(""), showDescription: ref(false) };
|
|
54
|
-
},
|
|
55
|
-
template: `
|
|
56
|
-
<ComponentUnderTest v-model="modelValue" type="text" name="email" label="Email" error-message="">
|
|
57
|
-
<template v-if="showDescription" #descriptionText>We never share it.</template>
|
|
58
|
-
</ComponentUnderTest>
|
|
59
|
-
`,
|
|
60
|
-
});
|
|
61
|
-
const host = await mountSuspended(Host);
|
|
62
|
-
const input = () => host.find("input");
|
|
63
|
-
expect(input().attributes("aria-describedby")).toBeFalsy();
|
|
64
|
-
(host.vm as unknown as { showDescription: boolean }).showDescription = true;
|
|
65
|
-
await nextTick();
|
|
66
|
-
expect(input().attributes("aria-describedby")).toMatch(/-description$/);
|
|
67
|
-
(host.vm as unknown as { showDescription: boolean }).showDescription = false;
|
|
68
|
-
await nextTick();
|
|
69
|
-
expect(input().attributes("aria-describedby")).toBeFalsy();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="input-textarea-wrapper"
|
|
4
|
-
:data-theme="theme"
|
|
5
|
-
:data-invalid="fieldHasError ? '' : null"
|
|
6
|
-
:class="[
|
|
7
|
-
inputVariant,
|
|
8
|
-
{ dirty: isDirty },
|
|
9
|
-
{ active: isActive },
|
|
10
|
-
{ error: fieldHasError },
|
|
11
|
-
{ 'has-left-slot': slots.left },
|
|
12
|
-
{ 'has-right-slot': slots.right },
|
|
13
|
-
]"
|
|
14
|
-
>
|
|
15
|
-
<span v-if="slots.left" class="slot left-slot">
|
|
16
|
-
<slot name="left"></slot>
|
|
17
|
-
</span>
|
|
18
|
-
|
|
19
|
-
<textarea
|
|
20
|
-
:id
|
|
21
|
-
ref="inputField"
|
|
22
|
-
v-model="modelValue"
|
|
23
|
-
:maxlength
|
|
24
|
-
:placeholder
|
|
25
|
-
:name
|
|
26
|
-
:required
|
|
27
|
-
:class="['input-textarea-core', elementClasses, { dirty: isDirty }, { active: isActive }]"
|
|
28
|
-
:aria-invalid="fieldHasError"
|
|
29
|
-
:aria-describedby
|
|
30
|
-
@focusin="updateFocus(true)"
|
|
31
|
-
@focusout="updateFocus(false)"
|
|
32
|
-
></textarea>
|
|
33
|
-
|
|
34
|
-
<span v-if="slots.right" class="slot right-slot">
|
|
35
|
-
<slot name="right"></slot>
|
|
36
|
-
</span>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<script setup lang="ts">
|
|
41
|
-
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms";
|
|
42
|
-
|
|
43
|
-
interface Props {
|
|
44
|
-
maxlength?: number;
|
|
45
|
-
id: string;
|
|
46
|
-
name: string;
|
|
47
|
-
placeholder?: string;
|
|
48
|
-
ariaDescribedby?: string;
|
|
49
|
-
fieldHasError?: boolean;
|
|
50
|
-
required?: boolean;
|
|
51
|
-
styleClassPassthrough?: string | string[];
|
|
52
|
-
theme?: FormUiTheme;
|
|
53
|
-
inputVariant?: InputUiVariant;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
57
|
-
maxlength: 255,
|
|
58
|
-
placeholder: "",
|
|
59
|
-
ariaDescribedby: "",
|
|
60
|
-
fieldHasError: false,
|
|
61
|
-
required: false,
|
|
62
|
-
styleClassPassthrough: () => [],
|
|
63
|
-
theme: "default",
|
|
64
|
-
inputVariant: "normal",
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const slots = useSlots();
|
|
68
|
-
|
|
69
|
-
const modelValue = defineModel<string | number | readonly string[] | null | undefined>({
|
|
70
|
-
required: true,
|
|
71
|
-
});
|
|
72
|
-
const isDirty = defineModel<boolean>("isDirty");
|
|
73
|
-
const isActive = defineModel<boolean>("isActive");
|
|
74
|
-
|
|
75
|
-
const updateFocus = (isFocused: boolean) => {
|
|
76
|
-
isActive.value = isFocused;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const inputField = ref<HTMLInputElement | null>(null);
|
|
80
|
-
|
|
81
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
82
|
-
</script>
|
|
83
|
-
|
|
84
|
-
<style lang="css">
|
|
85
|
-
@layer components {
|
|
86
|
-
.input-textarea-wrapper {
|
|
87
|
-
/* Public --input-textarea-* tokens, inline-fallback to the shared --theme-* tokens (see
|
|
88
|
-
theming-component-token-pattern.md) — mirrors InputTextCore's token shape. --_border is
|
|
89
|
-
genuinely reused (both the .normal border and the .underlined border-bottom read it); the
|
|
90
|
-
hover/focus border colours are each used only once, so they're inlined directly rather than
|
|
91
|
-
wrapped in a private var (see the Public token pattern rule in CLAUDE.md's Styling
|
|
92
|
-
Methodology). */
|
|
93
|
-
--_border: var(--input-textarea-border, var(--theme-border));
|
|
94
|
-
|
|
95
|
-
background-color: var(--input-textarea-surface, var(--theme-input-surface));
|
|
96
|
-
padding-inline: var(--input-padding-inline);
|
|
97
|
-
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
98
|
-
|
|
99
|
-
z-index: 2;
|
|
100
|
-
position: relative;
|
|
101
|
-
|
|
102
|
-
&.normal {
|
|
103
|
-
border: var(--form-element-border-width) solid var(--_border);
|
|
104
|
-
border-radius: var(--form-input-border-radius);
|
|
105
|
-
outline: var(--form-element-outline-width) solid transparent;
|
|
106
|
-
|
|
107
|
-
&:has(textarea:focus-visible) {
|
|
108
|
-
outline: var(--form-element-outline-width-focus) solid
|
|
109
|
-
var(--input-textarea-border-focus, var(--theme-border-focus));
|
|
110
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
&:has(textarea:is(:hover)) {
|
|
114
|
-
outline: var(--form-element-outline-width-focus) solid
|
|
115
|
-
var(--input-textarea-border-hover, var(--theme-border-focus));
|
|
116
|
-
outline-offset: var(--form-element-outline-offset-focus);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
&.underlined {
|
|
121
|
-
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
display: flex;
|
|
125
|
-
align-items: center;
|
|
126
|
-
gap: 12px;
|
|
127
|
-
|
|
128
|
-
.slot {
|
|
129
|
-
display: flex;
|
|
130
|
-
place-items: center;
|
|
131
|
-
background-clip: padding-box;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.input-textarea-core {
|
|
135
|
-
all: unset;
|
|
136
|
-
touch-action: manipulation;
|
|
137
|
-
flex-grow: 1;
|
|
138
|
-
field-sizing: content;
|
|
139
|
-
|
|
140
|
-
color: var(--input-textarea-text-color, var(--theme-input-text-color-normal));
|
|
141
|
-
font-family: var(--font-family);
|
|
142
|
-
font-size: var(--input-font-size);
|
|
143
|
-
line-height: var(--input-textarea-line-height);
|
|
144
|
-
padding-block: var(--form-textarea-padding-block);
|
|
145
|
-
padding-inline: var(--form-textarea-padding-inline);
|
|
146
|
-
|
|
147
|
-
min-height: 6lh;
|
|
148
|
-
|
|
149
|
-
&::placeholder {
|
|
150
|
-
color: var(--input-textarea-placeholder-color, var(--theme-input-placeholder));
|
|
151
|
-
font-size: var(--theme-input-placeholder-font-size);
|
|
152
|
-
font-style: italic;
|
|
153
|
-
line-height: 1;
|
|
154
|
-
font-weight: normal;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
</style>
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div
|
|
4
|
-
class="input-textarea-with-label"
|
|
5
|
-
:data-theme="theme"
|
|
6
|
-
:data-invalid="fieldHasError ? '' : null"
|
|
7
|
-
:class="[elementClasses, inputVariant, { dirty: isDirty }, { active: isActive }]"
|
|
8
|
-
>
|
|
9
|
-
<InputLabel
|
|
10
|
-
:id
|
|
11
|
-
:required
|
|
12
|
-
:input-variant
|
|
13
|
-
:field-has-error
|
|
14
|
-
:style-class-passthrough="['input-textarea-label']"
|
|
15
|
-
>
|
|
16
|
-
<template #textLabel>{{ label }}</template>
|
|
17
|
-
</InputLabel>
|
|
18
|
-
|
|
19
|
-
<InputDescription
|
|
20
|
-
v-if="inputVariant !== 'outlined'"
|
|
21
|
-
:description-id
|
|
22
|
-
:input-variant
|
|
23
|
-
:field-has-error="fieldHasError"
|
|
24
|
-
:style-class-passthrough="['input-text-description']"
|
|
25
|
-
>
|
|
26
|
-
<template v-if="slots.descriptionHtml" #descriptionHtml>
|
|
27
|
-
<slot name="descriptionHtml"></slot>
|
|
28
|
-
</template>
|
|
29
|
-
<template v-if="slots.descriptionText" #descriptionText>
|
|
30
|
-
<slot name="descriptionText"></slot>
|
|
31
|
-
</template>
|
|
32
|
-
</InputDescription>
|
|
33
|
-
|
|
34
|
-
<InputTextareaCore
|
|
35
|
-
:id
|
|
36
|
-
v-model="modelValue"
|
|
37
|
-
v-model:is-dirty="isDirty"
|
|
38
|
-
v-model:is-active="isActive"
|
|
39
|
-
:maxlength
|
|
40
|
-
:name
|
|
41
|
-
:placeholder
|
|
42
|
-
:field-has-error
|
|
43
|
-
:required
|
|
44
|
-
:style-class-passthrough
|
|
45
|
-
:theme
|
|
46
|
-
:input-variant
|
|
47
|
-
:aria-describedby="ariaDescribedby()"
|
|
48
|
-
>
|
|
49
|
-
<template v-if="slots.left" #left>
|
|
50
|
-
<slot name="left"></slot>
|
|
51
|
-
</template>
|
|
52
|
-
<template v-if="slots.right" #right>
|
|
53
|
-
<slot name="right"></slot>
|
|
54
|
-
</template>
|
|
55
|
-
</InputTextareaCore>
|
|
56
|
-
<InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="false" :input-variant />
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<InputDescription
|
|
60
|
-
v-if="inputVariant === 'outlined'"
|
|
61
|
-
:description-id
|
|
62
|
-
:input-variant
|
|
63
|
-
:field-has-error="fieldHasError"
|
|
64
|
-
:style-class-passthrough="['input-text-description']"
|
|
65
|
-
>
|
|
66
|
-
<template v-if="slots.descriptionHtml" #descriptionHtml>
|
|
67
|
-
<slot name="descriptionHtml"></slot>
|
|
68
|
-
</template>
|
|
69
|
-
<template v-if="slots.descriptionText" #descriptionText>
|
|
70
|
-
<slot name="descriptionText"></slot>
|
|
71
|
-
</template>
|
|
72
|
-
</InputDescription>
|
|
73
|
-
</div>
|
|
74
|
-
</template>
|
|
75
|
-
|
|
76
|
-
<script setup lang="ts">
|
|
77
|
-
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms";
|
|
78
|
-
|
|
79
|
-
interface Props {
|
|
80
|
-
maxlength?: number;
|
|
81
|
-
name: string;
|
|
82
|
-
placeholder?: string;
|
|
83
|
-
label: string;
|
|
84
|
-
errorMessage: object | string;
|
|
85
|
-
fieldHasError?: boolean;
|
|
86
|
-
required?: boolean;
|
|
87
|
-
styleClassPassthrough?: string | string[];
|
|
88
|
-
theme?: FormUiTheme;
|
|
89
|
-
inputVariant?: InputUiVariant;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
93
|
-
maxlength: 255,
|
|
94
|
-
placeholder: "",
|
|
95
|
-
fieldHasError: false,
|
|
96
|
-
required: false,
|
|
97
|
-
styleClassPassthrough: () => [],
|
|
98
|
-
theme: "default",
|
|
99
|
-
inputVariant: "normal",
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const slots = useSlots();
|
|
103
|
-
|
|
104
|
-
const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
|
|
105
|
-
props.name,
|
|
106
|
-
toRef(props, "fieldHasError"),
|
|
107
|
-
slots
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const modelValue = defineModel<string | number | readonly string[] | null | undefined>({
|
|
111
|
-
required: true,
|
|
112
|
-
});
|
|
113
|
-
const isActive = ref<boolean>(false);
|
|
114
|
-
const isDirty = ref<boolean>(false);
|
|
115
|
-
|
|
116
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
117
|
-
|
|
118
|
-
const testDirty = () => {
|
|
119
|
-
const watchValue = modelValue.value ?? "";
|
|
120
|
-
|
|
121
|
-
if (!isDirty.value && typeof watchValue === "string" && watchValue.length > 0) {
|
|
122
|
-
isDirty.value = true;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
onMounted(() => {
|
|
127
|
-
testDirty();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
watch(
|
|
131
|
-
() => modelValue.value,
|
|
132
|
-
() => {
|
|
133
|
-
testDirty();
|
|
134
|
-
}
|
|
135
|
-
);
|
|
136
|
-
</script>
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import { computed } from "vue";
|
|
3
|
-
import StorybookComponent from "../InputTextareaCore.vue";
|
|
4
|
-
import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
|
|
5
|
-
|
|
6
|
-
interface InputTextareaCoreStoryArgs {
|
|
7
|
-
modelValue: string;
|
|
8
|
-
maxlength: number;
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
placeholder: string;
|
|
12
|
-
ariaDescribedby: string;
|
|
13
|
-
fieldHasError: boolean;
|
|
14
|
-
required: boolean;
|
|
15
|
-
theme: FormUiTheme;
|
|
16
|
-
inputVariant: InputUiVariant;
|
|
17
|
-
styleClassPassthrough: string[];
|
|
18
|
-
leftSlotContent: string;
|
|
19
|
-
rightSlotContent: string;
|
|
20
|
-
useLeftSlot: boolean;
|
|
21
|
-
useRightSlot: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default {
|
|
25
|
-
title: "Components/Forms/Input Textarea/InputTextareaCore",
|
|
26
|
-
component: StorybookComponent,
|
|
27
|
-
argTypes: {
|
|
28
|
-
// Model
|
|
29
|
-
modelValue: {
|
|
30
|
-
control: "text",
|
|
31
|
-
description: "The textarea value",
|
|
32
|
-
table: {
|
|
33
|
-
category: "Model",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
// Basic Configuration
|
|
38
|
-
maxlength: {
|
|
39
|
-
control: { type: "number", min: 1, max: 10000 },
|
|
40
|
-
description: "Maximum length of textarea",
|
|
41
|
-
table: {
|
|
42
|
-
category: "Basic",
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
id: {
|
|
46
|
-
control: "text",
|
|
47
|
-
description: "Element ID",
|
|
48
|
-
table: {
|
|
49
|
-
category: "Basic",
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
name: {
|
|
53
|
-
control: "text",
|
|
54
|
-
description: "Textarea name attribute",
|
|
55
|
-
table: {
|
|
56
|
-
category: "Basic",
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
placeholder: {
|
|
60
|
-
control: "text",
|
|
61
|
-
description: "Placeholder text",
|
|
62
|
-
table: {
|
|
63
|
-
category: "Basic",
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
ariaDescribedby: {
|
|
67
|
-
control: "text",
|
|
68
|
-
description: "ARIA describedby attribute",
|
|
69
|
-
table: {
|
|
70
|
-
category: "Accessibility",
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// Validation
|
|
75
|
-
fieldHasError: {
|
|
76
|
-
control: "boolean",
|
|
77
|
-
description: "Whether field has validation error",
|
|
78
|
-
table: {
|
|
79
|
-
category: "Validation",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
required: {
|
|
83
|
-
control: "boolean",
|
|
84
|
-
description: "Whether field is required",
|
|
85
|
-
table: {
|
|
86
|
-
category: "Validation",
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
// Styling
|
|
91
|
-
theme: {
|
|
92
|
-
control: { type: "select" },
|
|
93
|
-
options: ["primary", "secondary", "tertiary", "ghost", "error", "success", "warning"],
|
|
94
|
-
description: "Theme for styling",
|
|
95
|
-
table: {
|
|
96
|
-
category: "Styling",
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
inputVariant: {
|
|
100
|
-
control: { type: "select" },
|
|
101
|
-
options: ["normal", "outlined", "underlined"],
|
|
102
|
-
description: "Textarea variant style",
|
|
103
|
-
table: {
|
|
104
|
-
category: "Styling",
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
styleClassPassthrough: {
|
|
108
|
-
control: "object",
|
|
109
|
-
description: "Additional CSS classes",
|
|
110
|
-
table: {
|
|
111
|
-
category: "Styling",
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
// Slots
|
|
116
|
-
useLeftSlot: {
|
|
117
|
-
control: "boolean",
|
|
118
|
-
description: "Use left slot",
|
|
119
|
-
table: {
|
|
120
|
-
category: "Slots",
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
useRightSlot: {
|
|
124
|
-
control: "boolean",
|
|
125
|
-
description: "Use right slot",
|
|
126
|
-
table: {
|
|
127
|
-
category: "Slots",
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
leftSlotContent: {
|
|
131
|
-
control: "text",
|
|
132
|
-
description: "Content for left slot",
|
|
133
|
-
table: {
|
|
134
|
-
category: "Slots",
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
rightSlotContent: {
|
|
138
|
-
control: "text",
|
|
139
|
-
description: "Content for right slot",
|
|
140
|
-
table: {
|
|
141
|
-
category: "Slots",
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
args: {
|
|
146
|
-
modelValue: "",
|
|
147
|
-
maxlength: 1000,
|
|
148
|
-
id: "example-textarea",
|
|
149
|
-
name: "example-textarea",
|
|
150
|
-
placeholder: "Enter your message...",
|
|
151
|
-
ariaDescribedby: "",
|
|
152
|
-
fieldHasError: false,
|
|
153
|
-
required: false,
|
|
154
|
-
theme: "default",
|
|
155
|
-
inputVariant: "normal",
|
|
156
|
-
styleClassPassthrough: [],
|
|
157
|
-
useLeftSlot: false,
|
|
158
|
-
useRightSlot: false,
|
|
159
|
-
leftSlotContent: "📝",
|
|
160
|
-
rightSlotContent: "✨",
|
|
161
|
-
},
|
|
162
|
-
} as Meta<typeof StorybookComponent>;
|
|
163
|
-
|
|
164
|
-
// `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
|
|
165
|
-
// destructuring it into local variables/refs, which would snapshot the values once at setup()
|
|
166
|
-
// and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
|
|
167
|
-
// slot-toggle args so they don't leak onto the component as unknown attributes.
|
|
168
|
-
const Template: StoryFn<InputTextareaCoreStoryArgs> = (args) => ({
|
|
169
|
-
components: { StorybookComponent },
|
|
170
|
-
setup() {
|
|
171
|
-
const componentArgs = computed(() => {
|
|
172
|
-
const { useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } = args;
|
|
173
|
-
return rest;
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return { args, componentArgs };
|
|
177
|
-
},
|
|
178
|
-
template: `
|
|
179
|
-
<StorybookComponent
|
|
180
|
-
v-model="args.modelValue"
|
|
181
|
-
v-bind="componentArgs"
|
|
182
|
-
>
|
|
183
|
-
<template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
|
|
184
|
-
<template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
|
|
185
|
-
</StorybookComponent>
|
|
186
|
-
<div class="mt-4 text-sm text-gray-600">
|
|
187
|
-
Character count: {{ args.modelValue.length }}
|
|
188
|
-
</div>
|
|
189
|
-
`,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
export const Default = Template.bind({});
|
|
193
|
-
Default.args = {};
|
|
194
|
-
|
|
195
|
-
export const WithPlaceholder = Template.bind({});
|
|
196
|
-
WithPlaceholder.args = {
|
|
197
|
-
placeholder: "Type your thoughts here...",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export const LongText = Template.bind({});
|
|
201
|
-
LongText.args = {
|
|
202
|
-
modelValue:
|
|
203
|
-
"This is a longer piece of text that demonstrates how the textarea handles multiple lines and wrapping. You can continue typing and see how it behaves with more content.",
|
|
204
|
-
placeholder: "Enter a long message...",
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export const Required = Template.bind({});
|
|
208
|
-
Required.args = {
|
|
209
|
-
required: true,
|
|
210
|
-
placeholder: "This field is required",
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
export const WithError = Template.bind({});
|
|
214
|
-
WithError.args = {
|
|
215
|
-
fieldHasError: true,
|
|
216
|
-
theme: "error",
|
|
217
|
-
modelValue: "Invalid content",
|
|
218
|
-
placeholder: "Enter valid content",
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
export const WithSlots = Template.bind({});
|
|
222
|
-
WithSlots.args = {
|
|
223
|
-
useLeftSlot: true,
|
|
224
|
-
useRightSlot: true,
|
|
225
|
-
leftSlotContent: "📝",
|
|
226
|
-
rightSlotContent: "✨",
|
|
227
|
-
placeholder: "Textarea with decorative slots",
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
export const Outlined = Template.bind({});
|
|
231
|
-
Outlined.args = {
|
|
232
|
-
inputVariant: "outlined",
|
|
233
|
-
placeholder: "Outlined textarea",
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
export const Underlined = Template.bind({});
|
|
237
|
-
Underlined.args = {
|
|
238
|
-
inputVariant: "underlined",
|
|
239
|
-
placeholder: "Underlined textarea",
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
export const CharacterLimit = Template.bind({});
|
|
243
|
-
CharacterLimit.args = {
|
|
244
|
-
maxlength: 140,
|
|
245
|
-
placeholder: "Tweet-length message (140 chars max)",
|
|
246
|
-
modelValue: "This is a sample message to show character counting.",
|
|
247
|
-
};
|