srcdev-nuxt-components 9.4.6 → 9.4.8

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/commands/migrate-component.md +34 -3
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/entry-animation.md +88 -0
  9. package/.claude/skills/components/header-block.md +92 -0
  10. package/.claude/skills/components/input-copy.md +2 -0
  11. package/.claude/skills/components/input-text-core.md +186 -0
  12. package/.claude/skills/components/masonry-grid.md +153 -0
  13. package/.claude/skills/components/pop-over.md +100 -0
  14. package/.claude/skills/components/rotating-carousel-image.md +101 -0
  15. package/.claude/skills/components/skip-links.md +82 -0
  16. package/.claude/skills/components/tabs-core.md +187 -0
  17. package/.claude/skills/index.md +10 -1
  18. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  19. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  20. package/.vscode/srcdev-component-header-block.code-snippets +41 -0
  21. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  22. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  23. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  24. package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
  25. package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
  26. package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
  27. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
  28. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  29. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  30. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  31. package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
  33. package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
  34. package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
  35. package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
  36. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  37. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  38. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  39. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  40. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  41. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  42. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  43. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  44. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  45. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  46. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
  47. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
  48. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
  49. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
  50. package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
  51. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
  52. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
  53. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
  54. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
  55. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  56. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  57. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  58. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  59. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  60. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
  61. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
  62. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
  63. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
  64. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
  65. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  66. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  67. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  68. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  69. package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
  70. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
  71. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
  72. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  73. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  74. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  75. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  76. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  77. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  78. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  79. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  80. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  81. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  82. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  83. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  84. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  85. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  86. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  87. package/app/composables/useTabs.ts +225 -207
  88. package/app/types/components/index.ts +2 -0
  89. package/app/types/components/rotating-carousel-image.d.ts +4 -0
  90. package/app/types/components/skip-links.d.ts +4 -0
  91. package/app/types/forms/types.forms.d.ts +1 -1
  92. package/package.json +1 -1
  93. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
  94. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  95. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  96. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  97. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  98. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  99. package/app/components/pop-over/PopOver.vue +0 -90
  100. package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
  101. package/app/components/skip-links/SkipLinks.vue +0 -60
  102. package/app/components/tabs/TabsCore.vue +0 -306
  103. package/app/components/typography/HeaderBlock.vue +0 -35
  104. package/app/layouts/default.vue +0 -308
  105. package/app/layouts/site-navigation-demo.vue +0 -188
  106. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
  107. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
  108. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
  109. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
@@ -0,0 +1,61 @@
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 "../variants/InputTextAsNumberWithLabel.vue";
5
+
6
+ const initialPropsData = {
7
+ name: "quantity",
8
+ label: "Quantity",
9
+ errorMessage: "",
10
+ min: 0,
11
+ max: 10,
12
+ };
13
+
14
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
15
+ const wrapperFactory = (propsData = {}) => {
16
+ const mockPropsData = { ...initialPropsData, ...propsData };
17
+
18
+ return mountSuspended(ComponentUnderTest, {
19
+ attachTo: document.body,
20
+ props: mockPropsData,
21
+ slots: {
22
+ left: () => "−",
23
+ right: () => "+",
24
+ },
25
+ });
26
+ };
27
+
28
+ describe("InputTextAsNumberWithLabel", () => {
29
+ it("mounts without error", async () => {
30
+ wrapper = await wrapperFactory();
31
+ expect(wrapper.vm).toBeTruthy();
32
+ });
33
+
34
+ it("uses the default step button text", async () => {
35
+ wrapper = await wrapperFactory();
36
+
37
+ const [stepDown, stepUp] = wrapper.findAll("button");
38
+ expect(stepDown?.text()).toContain("Step down");
39
+ expect(stepUp?.text()).toContain("Step up");
40
+ });
41
+
42
+ it("uses custom stepDownText/stepUpText when provided", async () => {
43
+ wrapper = await wrapperFactory({
44
+ stepDownText: "Diminuer",
45
+ stepUpText: "Augmenter",
46
+ });
47
+
48
+ const [stepDown, stepUp] = wrapper.findAll("button");
49
+ expect(stepDown?.text()).toContain("Diminuer");
50
+ expect(stepUp?.text()).toContain("Augmenter");
51
+ });
52
+
53
+ it("increments the value when the step-up button is clicked", async () => {
54
+ wrapper = await wrapperFactory({ modelValue: 5 });
55
+
56
+ const [, stepUp] = wrapper.findAll("button");
57
+ await stepUp?.trigger("click");
58
+
59
+ expect(wrapper.emitted("update:modelValue")?.[0]).toEqual([6]);
60
+ });
61
+ });
@@ -0,0 +1,60 @@
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
+ });
@@ -0,0 +1,46 @@
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 "../variants/InputTextWithLabel.vue";
5
+
6
+ const initialPropsData = {
7
+ type: "text" as const,
8
+ name: "testName",
9
+ label: "Test label",
10
+ errorMessage: "",
11
+ };
12
+
13
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
14
+ const wrapperFactory = (propsData = {}) => {
15
+ const mockPropsData = { ...initialPropsData, ...propsData };
16
+
17
+ return mountSuspended(ComponentUnderTest, {
18
+ attachTo: document.body,
19
+ props: mockPropsData,
20
+ });
21
+ };
22
+
23
+ describe("InputTextWithLabel", () => {
24
+ it("mounts without error", async () => {
25
+ wrapper = await wrapperFactory();
26
+ expect(wrapper.vm).toBeTruthy();
27
+ });
28
+
29
+ it("renders the label text", async () => {
30
+ wrapper = await wrapperFactory({ label: "Email address" });
31
+ expect(wrapper.text()).toContain("Email address");
32
+ });
33
+
34
+ it("forwards min/max through to the underlying InputTextCore input (e.g. for type=date)", async () => {
35
+ wrapper = await wrapperFactory({
36
+ type: "date",
37
+ min: "2026-01-01",
38
+ max: "2026-12-31",
39
+ });
40
+ const input = wrapper.find("input");
41
+
42
+ expect(input.attributes("type")).toBe("date");
43
+ expect(input.attributes("min")).toBe("2026-01-01");
44
+ expect(input.attributes("max")).toBe("2026-12-31");
45
+ });
46
+ });
@@ -53,6 +53,10 @@ interface Props {
53
53
  styleClassPassthrough?: string | string[];
54
54
  theme?: FormUiTheme;
55
55
  inputVariant?: InputUiVariant;
56
+ /** Accessible name for the toggle button while the password is hidden. */
57
+ showPasswordText?: string;
58
+ /** Accessible name for the toggle button while the password is visible. */
59
+ hidePasswordText?: string;
56
60
  }
57
61
 
58
62
  const props = withDefaults(defineProps<Props>(), {
@@ -64,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
64
68
  styleClassPassthrough: () => [],
65
69
  theme: "default",
66
70
  inputVariant: "normal",
71
+ showPasswordText: "Show password",
72
+ hidePasswordText: "Hide password",
67
73
  });
68
74
 
69
75
  const modelValue = defineModel<string>();
@@ -76,7 +82,7 @@ const inputType = computed<"text" | "password">(() => {
76
82
 
77
83
  const displayPassword = ref(false);
78
84
  const buttonText = computed(() => {
79
- return displayPassword.value ? "Hide password" : "Show password";
85
+ return displayPassword.value ? props.hidePasswordText : props.showPasswordText;
80
86
  });
81
87
  const toggleDisplayPassword = () => {
82
88
  displayPassword.value = !displayPassword.value;
@@ -56,7 +56,7 @@
56
56
  type="button"
57
57
  :readonly="(modelValue ?? 0) <= min"
58
58
  :is-pending="false"
59
- button-text="Step down"
59
+ :button-text="stepDownText"
60
60
  theme="default"
61
61
  variant="inline"
62
62
  @click.stop.prevent="updateValue(-step, (modelValue ?? 0) > min)"
@@ -71,7 +71,7 @@
71
71
  type="button"
72
72
  :readonly="(modelValue ?? 0) >= max"
73
73
  :is-pending="false"
74
- button-text="Step up"
74
+ :button-text="stepUpText"
75
75
  theme="default"
76
76
  variant="inline"
77
77
  @click.stop.prevent="updateValue(step, (modelValue ?? 0) < max)"
@@ -103,6 +103,10 @@ interface Props {
103
103
  max: number;
104
104
  step?: number;
105
105
  inputVariant?: InputUiVariant;
106
+ /** Accessible name for the decrement button. */
107
+ stepDownText?: string;
108
+ /** Accessible name for the increment button. */
109
+ stepUpText?: string;
106
110
  }
107
111
 
108
112
  const props = withDefaults(defineProps<Props>(), {
@@ -114,6 +118,8 @@ const props = withDefaults(defineProps<Props>(), {
114
118
  theme: "default",
115
119
  step: 1,
116
120
  inputVariant: "normal",
121
+ stepDownText: "Step down",
122
+ stepUpText: "Step up",
117
123
  });
118
124
 
119
125
  const slots = useSlots();
@@ -41,6 +41,8 @@
41
41
  :type
42
42
  :inputmode
43
43
  :maxlength
44
+ :min
45
+ :max
44
46
  :name
45
47
  :placeholder
46
48
  :label
@@ -86,6 +88,10 @@ import type { InputTypesText, FormUiTheme, InputUiVariant, InputMode } from "~/t
86
88
 
87
89
  interface Props {
88
90
  maxlength?: number;
91
+ /** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
92
+ min?: string | number;
93
+ /** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
94
+ max?: string | number;
89
95
  type: InputTypesText;
90
96
  inputmode?: InputMode;
91
97
  name: string;
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../InputTextareaCore.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
@@ -12,7 +13,7 @@ interface InputTextareaCoreStoryArgs {
12
13
  fieldHasError: boolean;
13
14
  required: boolean;
14
15
  theme: FormUiTheme;
15
- InputUiVariant: InputUiVariant;
16
+ inputVariant: InputUiVariant;
16
17
  styleClassPassthrough: string[];
17
18
  leftSlotContent: string;
18
19
  rightSlotContent: string;
@@ -95,7 +96,7 @@ export default {
95
96
  category: "Styling",
96
97
  },
97
98
  },
98
- InputUiVariant: {
99
+ inputVariant: {
99
100
  control: { type: "select" },
100
101
  options: ["normal", "outlined", "underlined"],
101
102
  description: "Textarea variant style",
@@ -160,31 +161,30 @@ export default {
160
161
  },
161
162
  } as Meta<typeof StorybookComponent>;
162
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.
163
168
  const Template: StoryFn<InputTextareaCoreStoryArgs> = (args) => ({
164
169
  components: { StorybookComponent },
165
170
  setup() {
166
- const { modelValue, ...otherArgs } = args;
167
- const textareaValue = ref(modelValue);
168
-
169
- return {
170
- textareaValue,
171
- args: otherArgs,
172
- leftSlotContent: args.leftSlotContent,
173
- rightSlotContent: args.rightSlotContent,
174
- useLeftSlot: args.useLeftSlot,
175
- useRightSlot: args.useRightSlot,
176
- };
171
+ const componentArgs = computed(() => {
172
+ const { useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } = args;
173
+ return rest;
174
+ });
175
+
176
+ return { args, componentArgs };
177
177
  },
178
178
  template: `
179
179
  <StorybookComponent
180
- v-model="textareaValue"
181
- v-bind="args"
180
+ v-model="args.modelValue"
181
+ v-bind="componentArgs"
182
182
  >
183
- <template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
184
- <template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
183
+ <template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
184
+ <template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
185
185
  </StorybookComponent>
186
186
  <div class="mt-4 text-sm text-gray-600">
187
- Character count: {{ textareaValue.length }}
187
+ Character count: {{ args.modelValue.length }}
188
188
  </div>
189
189
  `,
190
190
  });
@@ -229,13 +229,13 @@ WithSlots.args = {
229
229
 
230
230
  export const Outlined = Template.bind({});
231
231
  Outlined.args = {
232
- InputUiVariant: "outlined",
232
+ inputVariant: "outlined",
233
233
  placeholder: "Outlined textarea",
234
234
  };
235
235
 
236
236
  export const Underlined = Template.bind({});
237
237
  Underlined.args = {
238
- InputUiVariant: "underlined",
238
+ inputVariant: "underlined",
239
239
  placeholder: "Underlined textarea",
240
240
  };
241
241
 
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../variants/InputTextareaWithLabel.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
@@ -12,7 +13,7 @@ interface InputTextareaWithLabelStoryArgs {
12
13
  fieldHasError: boolean;
13
14
  required: boolean;
14
15
  theme: FormUiTheme;
15
- InputUiVariant: InputUiVariant;
16
+ inputVariant: InputUiVariant;
16
17
  styleClassPassthrough: string[];
17
18
  useDescriptionTextSlot: boolean;
18
19
  useDescriptionHtmlSlot: boolean;
@@ -99,7 +100,7 @@ export default {
99
100
  category: "Styling",
100
101
  },
101
102
  },
102
- InputUiVariant: {
103
+ inputVariant: {
103
104
  control: { type: "select" },
104
105
  options: ["normal", "outlined", "underlined"],
105
106
  description: "Textarea variant style",
@@ -196,37 +197,42 @@ export default {
196
197
  },
197
198
  } as Meta<typeof StorybookComponent>;
198
199
 
200
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
201
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
202
+ // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
203
+ // slot-toggle args so they don't leak onto the component as unknown attributes.
199
204
  const Template: StoryFn<InputTextareaWithLabelStoryArgs> = (args) => ({
200
205
  components: { StorybookComponent },
201
206
  setup() {
202
- const { modelValue, ...otherArgs } = args;
203
- const textareaValue = ref(modelValue);
207
+ const componentArgs = computed(() => {
208
+ const {
209
+ useDescriptionTextSlot,
210
+ useDescriptionHtmlSlot,
211
+ descriptionTextContent,
212
+ descriptionHtmlContent,
213
+ useLeftSlot,
214
+ useRightSlot,
215
+ leftSlotContent,
216
+ rightSlotContent,
217
+ ...rest
218
+ } = args;
219
+ return rest;
220
+ });
204
221
 
205
- return {
206
- textareaValue,
207
- args: otherArgs,
208
- descriptionTextContent: args.descriptionTextContent,
209
- descriptionHtmlContent: args.descriptionHtmlContent,
210
- useDescriptionTextSlot: args.useDescriptionTextSlot,
211
- useDescriptionHtmlSlot: args.useDescriptionHtmlSlot,
212
- leftSlotContent: args.leftSlotContent,
213
- rightSlotContent: args.rightSlotContent,
214
- useLeftSlot: args.useLeftSlot,
215
- useRightSlot: args.useRightSlot,
216
- };
222
+ return { args, componentArgs };
217
223
  },
218
224
  template: `
219
225
  <StorybookComponent
220
- v-model="textareaValue"
221
- v-bind="args"
226
+ v-model="args.modelValue"
227
+ v-bind="componentArgs"
222
228
  >
223
- <template v-if="useDescriptionTextSlot" #descriptionText>{{ descriptionTextContent }}</template>
224
- <template v-if="useDescriptionHtmlSlot" #descriptionHtml v-html="descriptionHtmlContent"></template>
225
- <template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
226
- <template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
229
+ <template v-if="args.useDescriptionTextSlot" #descriptionText>{{ args.descriptionTextContent }}</template>
230
+ <template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
231
+ <template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
232
+ <template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
227
233
  </StorybookComponent>
228
234
  <div class="mt-4 text-sm text-gray-600">
229
- Character count: {{ textareaValue.length }} / {{ args.maxlength }}
235
+ Character count: {{ args.modelValue.length }} / {{ args.maxlength }}
230
236
  </div>
231
237
  `,
232
238
  });
@@ -305,7 +311,7 @@ Required.args = {
305
311
 
306
312
  export const Outlined = Template.bind({});
307
313
  Outlined.args = {
308
- InputUiVariant: "outlined",
314
+ inputVariant: "outlined",
309
315
  label: "Outlined Textarea",
310
316
  placeholder: "This is an outlined textarea",
311
317
  useDescriptionTextSlot: true,
@@ -314,7 +320,7 @@ Outlined.args = {
314
320
 
315
321
  export const Underlined = Template.bind({});
316
322
  Underlined.args = {
317
- InputUiVariant: "underlined",
323
+ inputVariant: "underlined",
318
324
  label: "Underlined Textarea",
319
325
  placeholder: "This is an underlined textarea",
320
326
  };
@@ -359,7 +365,7 @@ AllVariants.render = (args) => ({
359
365
  <h3 class="text-lg font-semibold capitalize">{{ variant }} Variant</h3>
360
366
  <StorybookComponent
361
367
  v-model="textareaValues[variant]"
362
- v-bind="{ ...args, InputUiVariant: variant, label: variant + ' Textarea', name: variant + '-textarea' }"
368
+ v-bind="{ ...args, inputVariant: variant, label: variant + ' Textarea', name: variant + '-textarea' }"
363
369
  />
364
370
  </div>
365
371
  </div>