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.
Files changed (109) hide show
  1. package/.claude/skills/component-compliance-checklist.md +28 -1
  2. package/.claude/skills/component-inline-action-button.md +1 -1
  3. package/.claude/skills/component-local-style-override.md +1 -1
  4. package/.claude/skills/component-naming.md +17 -12
  5. package/.claude/skills/components/container-glow.md +2 -2
  6. package/.claude/skills/components/form-field.md +2 -2
  7. package/.claude/skills/components/form-wrapper.md +1 -1
  8. package/.claude/skills/components/glowing-border.md +1 -1
  9. package/.claude/skills/components/input-description.md +7 -7
  10. package/.claude/skills/components/input-error.md +3 -3
  11. package/.claude/skills/components/input-label.md +7 -8
  12. package/.claude/skills/components/input-number.md +17 -17
  13. package/.claude/skills/components/input-select.md +153 -0
  14. package/.claude/skills/components/input-text.md +220 -0
  15. package/.claude/skills/components/input-textarea.md +147 -0
  16. package/.claude/skills/components/select-menu.md +4 -4
  17. package/.claude/skills/components/toggle-switch.md +143 -0
  18. package/.claude/skills/composable-zod-validation.md +1 -1
  19. package/.claude/skills/css-nesting-conventions.md +22 -1
  20. package/.claude/skills/icon-sets.md +1 -1
  21. package/.claude/skills/icon-sizing.md +1 -1
  22. package/.claude/skills/index.md +5 -5
  23. package/.claude/skills/storybook-add-story.md +1 -1
  24. package/.claude/skills/theming-component-token-pattern.md +8 -8
  25. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  26. package/.vscode/srcdev-component-input-select.code-snippets +20 -14
  27. package/.vscode/srcdev-component-input-text.code-snippets +46 -40
  28. package/.vscode/srcdev-component-input-textarea.code-snippets +19 -14
  29. package/.vscode/srcdev-component-toggle-switch.code-snippets +19 -29
  30. package/app/assets/styles/setup/04.elements/forms/00.element-defaults.css +2 -2
  31. package/app/assets/styles/setup/04.elements/forms/06.button-geometry.css +1 -1
  32. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +2 -2
  33. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +4 -4
  34. package/app/components/02.molecules/progress-tracker/stories/ProgressTracker.stories.ts +4 -4
  35. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +1 -1
  36. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +2 -2
  37. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +3 -3
  38. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +6 -6
  39. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +7 -7
  40. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +9 -5
  41. package/app/components/05.forms/input-number/InputNumber.vue +2 -2
  42. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +80 -10
  43. package/app/components/05.forms/input-select/InputSelect.vue +198 -0
  44. package/app/components/05.forms/input-select/InputSelectField.vue +130 -0
  45. package/app/components/05.forms/input-select/stories/InputSelect.stories.ts +184 -0
  46. package/app/components/05.forms/input-select/stories/InputSelectField.stories.ts +187 -0
  47. package/app/components/05.forms/input-select/tests/{InputSelectCore.spec.ts → InputSelect.spec.ts} +75 -12
  48. package/app/components/05.forms/input-select/tests/InputSelectField.spec.ts +121 -0
  49. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +85 -15
  50. package/app/components/05.forms/input-text/{InputPasswordWithLabel.vue → InputPasswordField.vue} +16 -19
  51. package/app/components/05.forms/input-text/{InputTextCore.vue → InputText.vue} +42 -78
  52. package/app/components/05.forms/input-text/{InputTextAsNumberWithLabel.vue → InputTextAsNumberField.vue} +66 -62
  53. package/app/components/05.forms/input-text/InputTextField.vue +168 -0
  54. package/app/components/05.forms/input-text/stories/InputPasswordField.stories.ts +163 -0
  55. package/app/components/05.forms/input-text/stories/InputText.stories.ts +194 -0
  56. package/app/components/05.forms/input-text/stories/InputTextAsNumberField.stories.ts +182 -0
  57. package/app/components/05.forms/input-text/stories/InputTextField.stories.ts +206 -0
  58. package/app/components/05.forms/input-text/tests/{InputPasswordWithLabel.spec.ts → InputPasswordField.spec.ts} +42 -3
  59. package/app/components/05.forms/input-text/tests/InputText.spec.ts +166 -0
  60. package/app/components/05.forms/input-text/tests/InputTextAsNumberField.spec.ts +135 -0
  61. package/app/components/05.forms/input-text/tests/InputTextField.spec.ts +130 -0
  62. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +94 -12
  63. package/app/components/05.forms/input-textarea/InputTextarea.vue +165 -0
  64. package/app/components/05.forms/input-textarea/InputTextareaField.vue +159 -0
  65. package/app/components/05.forms/input-textarea/stories/InputTextarea.stories.ts +198 -0
  66. package/app/components/05.forms/input-textarea/stories/InputTextareaField.stories.ts +161 -0
  67. package/app/components/05.forms/input-textarea/tests/InputTextarea.spec.ts +149 -0
  68. package/app/components/05.forms/input-textarea/tests/InputTextareaField.spec.ts +122 -0
  69. package/app/components/05.forms/patterns/stories/ContactForm.stories.ts +13 -13
  70. package/app/components/05.forms/patterns/stories/LoginForm.stories.ts +6 -6
  71. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +30 -32
  72. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +71 -50
  73. package/app/components/05.forms/toggle-switch/{ToggleSwitchCore.vue → ToggleSwitch.vue} +58 -73
  74. package/app/components/05.forms/toggle-switch/{ToggleSwitchWithLabel.vue → ToggleSwitchField.vue} +65 -19
  75. package/app/components/05.forms/toggle-switch/stories/ToggleSwitch.stories.ts +148 -0
  76. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchField.stories.ts +176 -0
  77. package/app/components/05.forms/toggle-switch/tests/ToggleSwitch.spec.ts +145 -0
  78. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchField.spec.ts +155 -0
  79. package/package.json +1 -1
  80. package/.claude/skills/components/input-select-core.md +0 -151
  81. package/.claude/skills/components/input-text-core.md +0 -204
  82. package/.claude/skills/components/input-textarea-core.md +0 -154
  83. package/.claude/skills/components/toggle-switch-core.md +0 -169
  84. package/app/components/05.forms/input-select/InputSelectCore.vue +0 -188
  85. package/app/components/05.forms/input-select/InputSelectWithLabel.vue +0 -118
  86. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +0 -69
  87. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +0 -115
  88. package/app/components/05.forms/input-select/tests/InputSelectWithLabel.spec.ts +0 -87
  89. package/app/components/05.forms/input-text/InputTextWithLabel.vue +0 -154
  90. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +0 -368
  91. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +0 -480
  92. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +0 -267
  93. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +0 -319
  94. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +0 -75
  95. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +0 -60
  96. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +0 -71
  97. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +0 -159
  98. package/app/components/05.forms/input-textarea/InputTextareaWithLabel.vue +0 -136
  99. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +0 -247
  100. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +0 -458
  101. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +0 -119
  102. package/app/components/05.forms/input-textarea/tests/InputTextareaWithLabel.spec.ts +0 -93
  103. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabelInline.vue +0 -81
  104. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +0 -251
  105. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +0 -251
  106. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +0 -283
  107. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +0 -101
  108. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchWithLabel.spec.ts +0 -133
  109. 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
- };