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
@@ -33,7 +33,7 @@
33
33
  <script setup lang="ts">
34
34
  import type { ResponsiveHeaderProp } from "../../../types/components";
35
35
  import PageRow from "../../01.atoms/page-row/PageRow.vue";
36
- import SkipLinks from "../../skip-links/SkipLinks.vue";
36
+ import SkipLinks from "../../01.atoms/navigation/skip-links/SkipLinks.vue";
37
37
  import ResponsiveHeader from "../responsive-header/ResponsiveHeader.vue";
38
38
 
39
39
  interface Props {
@@ -34,7 +34,7 @@ describe("SiteHeader", () => {
34
34
  slots: { branding: "<a href='/' class='brand-link'>Brand</a>" },
35
35
  global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
36
36
  });
37
- expect(wrapper.find(".home-link .brand-link").exists()).toBe(true);
37
+ expect(wrapper.find(".skip-links__home .brand-link").exists()).toBe(true);
38
38
  });
39
39
 
40
40
  it("does not render the secondaryNavigation slot when not provided", async () => {
@@ -4,9 +4,9 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
4
4
  "<div class="page-row content">
5
5
  <header class="site-header">
6
6
  <nav class="home-navigation" aria-label="Home Navigation">
7
- <div class="home-link-navigation">
8
- <div class="home-link"></div>
9
- <nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
7
+ <div class="skip-links">
8
+ <div class="skip-links__home"></div>
9
+ <nav class="skip-links__nav" aria-label="Skip navigation"><a href="#main-content" class="skip-links__link">Skip to main content</a><a href="#footer-content" class="skip-links__link">Skip to footer</a></nav>
10
10
  </div>
11
11
  </nav>
12
12
  <div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
@@ -21,7 +21,7 @@
21
21
  v-for="item in fieldData.data"
22
22
  :key="item.id"
23
23
  :value="item.value"
24
- :selected="Number(item.value) === modelValue"
24
+ :selected="String(item.value) === String(modelValue)"
25
25
  class="input-select-core-option"
26
26
  >
27
27
  <Icon
@@ -27,6 +27,8 @@
27
27
  :name
28
28
  :required
29
29
  :maxlength
30
+ :min
31
+ :max
30
32
  :class="['input-text-core', elementClasses, { dirty: isDirty }, { active: isActive }]"
31
33
  :aria-invalid="fieldHasError ? 'true' : undefined"
32
34
  :aria-describedby
@@ -49,6 +51,10 @@ interface Props {
49
51
  type?: InputTypesText;
50
52
  inputmode?: InputMode;
51
53
  maxlength?: number;
54
+ /** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
55
+ min?: string | number;
56
+ /** Passed straight through to the native input — meaningful for type="date"/"number" etc. */
57
+ max?: string | number;
52
58
  id: string;
53
59
  name: string;
54
60
  required?: boolean;
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../variants/InputPasswordWithLabel.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
@@ -12,12 +13,14 @@ interface InputPasswordWithLabelStoryArgs {
12
13
  fieldHasError: boolean;
13
14
  required: boolean;
14
15
  theme: FormUiTheme;
15
- InputUiVariant: InputUiVariant;
16
+ inputVariant: InputUiVariant;
16
17
  styleClassPassthrough: string[];
17
18
  useDescriptionSlot: boolean;
18
19
  useDescriptionHtmlSlot: boolean;
19
20
  descriptionContent: string;
20
21
  descriptionHtmlContent: string;
22
+ showPasswordText: string;
23
+ hidePasswordText: string;
21
24
  }
22
25
 
23
26
  export default {
@@ -95,7 +98,7 @@ export default {
95
98
  category: "Styling",
96
99
  },
97
100
  },
98
- InputUiVariant: {
101
+ inputVariant: {
99
102
  control: { type: "select" },
100
103
  options: ["normal", "outlined", "underlined"],
101
104
  description: "Input variant style",
@@ -140,6 +143,22 @@ export default {
140
143
  category: "Slots",
141
144
  },
142
145
  },
146
+
147
+ // Localisation
148
+ showPasswordText: {
149
+ control: "text",
150
+ description: "Accessible name for the toggle button while the password is hidden",
151
+ table: {
152
+ category: "Localisation",
153
+ },
154
+ },
155
+ hidePasswordText: {
156
+ control: "text",
157
+ description: "Accessible name for the toggle button while the password is visible",
158
+ table: {
159
+ category: "Localisation",
160
+ },
161
+ },
143
162
  },
144
163
  args: {
145
164
  modelValue: "",
@@ -158,34 +177,35 @@ export default {
158
177
  descriptionContent: "Password should be at least 8 characters long",
159
178
  descriptionHtmlContent:
160
179
  "Password must contain: <strong>8+ characters</strong>, <em>uppercase</em>, <em>lowercase</em>, <em>numbers</em>",
180
+ showPasswordText: "Show password",
181
+ hidePasswordText: "Hide password",
161
182
  },
162
183
  } as Meta<typeof StorybookComponent>;
163
184
 
185
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
186
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
187
+ // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
188
+ // slot-toggle args so they don't leak onto the component as unknown attributes.
164
189
  const Template: StoryFn<InputPasswordWithLabelStoryArgs> = (args) => ({
165
190
  components: { StorybookComponent },
166
191
  setup() {
167
- const { modelValue, ...otherArgs } = args;
168
- const passwordValue = ref(modelValue);
192
+ const componentArgs = computed(() => {
193
+ const { useDescriptionSlot, useDescriptionHtmlSlot, descriptionContent, descriptionHtmlContent, ...rest } = args;
194
+ return rest;
195
+ });
169
196
 
170
- return {
171
- passwordValue,
172
- args: otherArgs,
173
- descriptionContent: args.descriptionContent,
174
- descriptionHtmlContent: args.descriptionHtmlContent,
175
- useDescriptionSlot: args.useDescriptionSlot,
176
- useDescriptionHtmlSlot: args.useDescriptionHtmlSlot,
177
- };
197
+ return { args, componentArgs };
178
198
  },
179
199
  template: `
180
200
  <StorybookComponent
181
- v-model="passwordValue"
182
- v-bind="args"
201
+ v-model="args.modelValue"
202
+ v-bind="componentArgs"
183
203
  >
184
- <template v-if="useDescriptionSlot" #description>{{ descriptionContent }}</template>
185
- <template v-if="useDescriptionHtmlSlot" #descriptionHtml v-html="descriptionHtmlContent"></template>
204
+ <template v-if="args.useDescriptionSlot" #description>{{ args.descriptionContent }}</template>
205
+ <template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
186
206
  </StorybookComponent>
187
207
  <div class="mt-4 text-sm text-gray-600">
188
- Current value: {{ passwordValue ? '•'.repeat(passwordValue.length) : 'Empty' }}
208
+ Current value: {{ args.modelValue ? '•'.repeat(args.modelValue.length) : 'Empty' }}
189
209
  </div>
190
210
  `,
191
211
  });
@@ -239,18 +259,27 @@ PasswordTooShort.args = {
239
259
 
240
260
  export const Outlined = Template.bind({});
241
261
  Outlined.args = {
242
- InputUiVariant: "outlined",
262
+ inputVariant: "outlined",
243
263
  label: "Confirm Password",
244
264
  placeholder: "Confirm your password",
245
265
  };
246
266
 
247
267
  export const Underlined = Template.bind({});
248
268
  Underlined.args = {
249
- InputUiVariant: "underlined",
269
+ inputVariant: "underlined",
250
270
  label: "Master Password",
251
271
  placeholder: "Enter master password",
252
272
  };
253
273
 
274
+ export const LocalisedToggleText = Template.bind({});
275
+ LocalisedToggleText.storyName = "Localised Toggle Text";
276
+ LocalisedToggleText.args = {
277
+ label: "Mot de passe",
278
+ placeholder: "Saisissez votre mot de passe",
279
+ showPasswordText: "Afficher le mot de passe",
280
+ hidePasswordText: "Masquer le mot de passe",
281
+ };
282
+
254
283
  export const AllVariants = Template.bind({});
255
284
  AllVariants.storyName = "All Input Variants";
256
285
  AllVariants.render = (args) => ({
@@ -279,7 +308,7 @@ AllVariants.render = (args) => ({
279
308
  <h3 class="text-lg font-semibold capitalize">{{ variant }} Variant</h3>
280
309
  <StorybookComponent
281
310
  v-model="passwordValues[variant]"
282
- v-bind="{ ...args, InputUiVariant: variant, label: variant + ' Password', name: variant + '-password' }"
311
+ v-bind="{ ...args, inputVariant: variant, label: variant + ' Password', name: variant + '-password' }"
283
312
  />
284
313
  </div>
285
314
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../variants/InputTextAsNumberWithLabel.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
@@ -12,7 +13,7 @@ interface InputTextAsNumberWithLabelStoryArgs {
12
13
  fieldHasError: boolean;
13
14
  required: boolean;
14
15
  theme: FormUiTheme;
15
- InputUiVariant: InputUiVariant;
16
+ inputVariant: InputUiVariant;
16
17
  min: number;
17
18
  max: number;
18
19
  step: number;
@@ -23,6 +24,8 @@ interface InputTextAsNumberWithLabelStoryArgs {
23
24
  useRightSlot: boolean;
24
25
  leftSlotContent: string;
25
26
  rightSlotContent: string;
27
+ stepDownText: string;
28
+ stepUpText: string;
26
29
  }
27
30
 
28
31
  export default {
@@ -123,7 +126,7 @@ export default {
123
126
  category: "Styling",
124
127
  },
125
128
  },
126
- InputUiVariant: {
129
+ inputVariant: {
127
130
  control: { type: "select" },
128
131
  options: ["normal", "outlined", "underlined"],
129
132
  description: "Input variant style",
@@ -182,6 +185,22 @@ export default {
182
185
  category: "Slots",
183
186
  },
184
187
  },
188
+
189
+ // Localisation
190
+ stepDownText: {
191
+ control: "text",
192
+ description: "Accessible name for the decrement button",
193
+ table: {
194
+ category: "Localisation",
195
+ },
196
+ },
197
+ stepUpText: {
198
+ control: "text",
199
+ description: "Accessible name for the increment button",
200
+ table: {
201
+ category: "Localisation",
202
+ },
203
+ },
185
204
  },
186
205
  args: {
187
206
  modelValue: undefined,
@@ -204,37 +223,37 @@ export default {
204
223
  useRightSlot: true,
205
224
  leftSlotContent: "−",
206
225
  rightSlotContent: "+",
226
+ stepDownText: "Step down",
227
+ stepUpText: "Step up",
207
228
  },
208
229
  } as Meta<typeof StorybookComponent>;
209
230
 
231
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
232
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
233
+ // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
234
+ // slot-toggle args so they don't leak onto the component as unknown attributes.
210
235
  const Template: StoryFn<InputTextAsNumberWithLabelStoryArgs> = (args) => ({
211
236
  components: { StorybookComponent },
212
237
  setup() {
213
- const { modelValue, ...otherArgs } = args;
214
- const numberValue = ref(modelValue);
238
+ const componentArgs = computed(() => {
239
+ const { useDescriptionSlot, descriptionContent, useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } =
240
+ args;
241
+ return rest;
242
+ });
215
243
 
216
- return {
217
- numberValue,
218
- args: otherArgs,
219
- descriptionContent: args.descriptionContent,
220
- useDescriptionSlot: args.useDescriptionSlot,
221
- leftSlotContent: args.leftSlotContent,
222
- rightSlotContent: args.rightSlotContent,
223
- useLeftSlot: args.useLeftSlot,
224
- useRightSlot: args.useRightSlot,
225
- };
244
+ return { args, componentArgs };
226
245
  },
227
246
  template: `
228
247
  <StorybookComponent
229
- v-model="numberValue"
230
- v-bind="args"
248
+ v-model="args.modelValue"
249
+ v-bind="componentArgs"
231
250
  >
232
- <template v-if="useDescriptionSlot" #description>{{ descriptionContent }}</template>
233
- <template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
234
- <template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
251
+ <template v-if="args.useDescriptionSlot" #description>{{ args.descriptionContent }}</template>
252
+ <template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
253
+ <template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
235
254
  </StorybookComponent>
236
255
  <div class="mt-4 text-sm text-gray-600">
237
- Current value: {{ numberValue ?? 'undefined' }}
256
+ Current value: {{ args.modelValue ?? 'undefined' }}
238
257
  </div>
239
258
  `,
240
259
  });
@@ -331,7 +350,7 @@ Required.args = {
331
350
  export const Outlined = Template.bind({});
332
351
  Outlined.args = {
333
352
  modelValue: 42,
334
- InputUiVariant: "outlined",
353
+ inputVariant: "outlined",
335
354
  label: "Temperature (°C)",
336
355
  placeholder: "Enter temperature",
337
356
  min: -50,
@@ -344,7 +363,7 @@ Outlined.args = {
344
363
  export const Underlined = Template.bind({});
345
364
  Underlined.args = {
346
365
  modelValue: 10,
347
- InputUiVariant: "underlined",
366
+ inputVariant: "underlined",
348
367
  label: "Items Count",
349
368
  placeholder: "Number of items",
350
369
  min: 0,
@@ -1,11 +1,14 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../InputTextCore.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
5
6
  interface InputTextCoreStoryArgs {
6
7
  modelValue: string;
7
- type: "text" | "email" | "password" | "tel" | "url" | "search";
8
+ type: "text" | "email" | "password" | "tel" | "url" | "search" | "date";
8
9
  maxlength: number;
10
+ min: string | number;
11
+ max: string | number;
9
12
  name: string;
10
13
  placeholder: string;
11
14
  label: string;
@@ -13,7 +16,7 @@ interface InputTextCoreStoryArgs {
13
16
  fieldHasError: boolean;
14
17
  required: boolean;
15
18
  theme: FormUiTheme;
16
- InputUiVariant: InputUiVariant;
19
+ inputVariant: InputUiVariant;
17
20
  styleClassPassthrough: string[];
18
21
  leftSlotContent: string;
19
22
  rightSlotContent: string;
@@ -37,7 +40,7 @@ export default {
37
40
  // Basic Configuration
38
41
  type: {
39
42
  control: { type: "select" },
40
- options: ["text", "email", "password", "tel", "url", "search"],
43
+ options: ["text", "email", "password", "tel", "url", "search", "date"],
41
44
  description: "Input type",
42
45
  table: {
43
46
  category: "Basic",
@@ -50,6 +53,20 @@ export default {
50
53
  category: "Basic",
51
54
  },
52
55
  },
56
+ min: {
57
+ control: "text",
58
+ description: 'Passed straight through to the native input — e.g. earliest allowed date for type="date", or lowest number for type="number".',
59
+ table: {
60
+ category: "Basic",
61
+ },
62
+ },
63
+ max: {
64
+ control: "text",
65
+ description: 'Passed straight through to the native input — e.g. latest allowed date for type="date", or highest number for type="number".',
66
+ table: {
67
+ category: "Basic",
68
+ },
69
+ },
53
70
  name: {
54
71
  control: "text",
55
72
  description: "Input name attribute",
@@ -104,7 +121,7 @@ export default {
104
121
  category: "Styling",
105
122
  },
106
123
  },
107
- InputUiVariant: {
124
+ inputVariant: {
108
125
  control: { type: "select" },
109
126
  options: ["normal", "outlined", "underlined"],
110
127
  description: "Input variant style",
@@ -170,31 +187,30 @@ export default {
170
187
  },
171
188
  } as Meta<typeof StorybookComponent>;
172
189
 
190
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
191
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
192
+ // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
193
+ // slot-toggle args so they don't leak onto the component as unknown attributes.
173
194
  const Template: StoryFn<InputTextCoreStoryArgs> = (args) => ({
174
195
  components: { StorybookComponent },
175
196
  setup() {
176
- const { modelValue, ...otherArgs } = args;
177
- const inputValue = ref(modelValue);
178
-
179
- return {
180
- inputValue,
181
- args: otherArgs,
182
- leftSlotContent: args.leftSlotContent,
183
- rightSlotContent: args.rightSlotContent,
184
- useLeftSlot: args.useLeftSlot,
185
- useRightSlot: args.useRightSlot,
186
- };
197
+ const componentArgs = computed(() => {
198
+ const { useLeftSlot, useRightSlot, leftSlotContent, rightSlotContent, ...rest } = args;
199
+ return rest;
200
+ });
201
+
202
+ return { args, componentArgs };
187
203
  },
188
204
  template: `
189
205
  <StorybookComponent
190
- v-model="inputValue"
191
- v-bind="args"
206
+ v-model="args.modelValue"
207
+ v-bind="componentArgs"
192
208
  >
193
- <template v-if="useLeftSlot" #left>{{ leftSlotContent }}</template>
194
- <template v-if="useRightSlot" #right>{{ rightSlotContent }}</template>
209
+ <template v-if="args.useLeftSlot" #left>{{ args.leftSlotContent }}</template>
210
+ <template v-if="args.useRightSlot" #right>{{ args.rightSlotContent }}</template>
195
211
  </StorybookComponent>
196
212
  <div class="mt-4 text-sm text-gray-600">
197
- Current value: {{ inputValue }}
213
+ Current value: {{ args.modelValue }}
198
214
  </div>
199
215
  `,
200
216
  });
@@ -230,12 +246,22 @@ WithSlots.args = {
230
246
 
231
247
  export const Outlined = Template.bind({});
232
248
  Outlined.args = {
233
- InputUiVariant: "outlined",
249
+ inputVariant: "outlined",
234
250
  placeholder: "Outlined input",
235
251
  };
236
252
 
237
253
  export const Underlined = Template.bind({});
238
254
  Underlined.args = {
239
- InputUiVariant: "underlined",
255
+ inputVariant: "underlined",
240
256
  placeholder: "Underlined input",
241
257
  };
258
+
259
+ export const DateWithMinMax = Template.bind({});
260
+ DateWithMinMax.args = {
261
+ type: "date",
262
+ label: "Appointment date",
263
+ name: "appointmentDate",
264
+ // Prevents picking a date in the past in the native browser date picker.
265
+ min: new Date().toISOString().split("T")[0],
266
+ max: "2027-12-31",
267
+ };
@@ -1,11 +1,14 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import StorybookComponent from "../variants/InputTextWithLabel.vue";
3
4
  import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
4
5
 
5
6
  interface InputTextWithLabelStoryArgs {
6
7
  modelValue: string;
7
- type: "text" | "email" | "password" | "tel" | "url" | "search";
8
+ type: "text" | "email" | "password" | "tel" | "url" | "search" | "date";
8
9
  maxlength: number;
10
+ min: string | number;
11
+ max: string | number;
9
12
  name: string;
10
13
  placeholder: string;
11
14
  label: string;
@@ -37,7 +40,7 @@ export default {
37
40
  // Basic Configuration
38
41
  type: {
39
42
  control: { type: "select" },
40
- options: ["text", "email", "password", "tel", "url", "search"],
43
+ options: ["text", "email", "password", "tel", "url", "search", "date"],
41
44
  description: "Input type",
42
45
  table: {
43
46
  category: "Basic",
@@ -50,6 +53,20 @@ export default {
50
53
  category: "Basic",
51
54
  },
52
55
  },
56
+ min: {
57
+ control: "text",
58
+ description: 'Passed straight through to the native input — e.g. earliest allowed date for type="date".',
59
+ table: {
60
+ category: "Basic",
61
+ },
62
+ },
63
+ max: {
64
+ control: "text",
65
+ description: 'Passed straight through to the native input — e.g. latest allowed date for type="date".',
66
+ table: {
67
+ category: "Basic",
68
+ },
69
+ },
53
70
  name: {
54
71
  control: "text",
55
72
  description: "Input name attribute",
@@ -170,31 +187,30 @@ export default {
170
187
  },
171
188
  } as Meta<typeof StorybookComponent>;
172
189
 
190
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
191
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
192
+ // and stop reflecting later Controls-panel changes. `componentArgs` strips the non-prop
193
+ // slot-toggle args so they don't leak onto the component as unknown attributes.
173
194
  const Template: StoryFn<InputTextWithLabelStoryArgs> = (args) => ({
174
195
  components: { StorybookComponent },
175
196
  setup() {
176
- const { modelValue, ...otherArgs } = args;
177
- const inputValue = ref(modelValue);
197
+ const componentArgs = computed(() => {
198
+ const { useDescriptionSlot, useDescriptionHtmlSlot, descriptionContent, descriptionHtmlContent, ...rest } = args;
199
+ return rest;
200
+ });
178
201
 
179
- return {
180
- inputValue,
181
- args: otherArgs,
182
- descriptionContent: args.descriptionContent,
183
- descriptionHtmlContent: args.descriptionHtmlContent,
184
- useDescriptionSlot: args.useDescriptionSlot,
185
- useDescriptionHtmlSlot: args.useDescriptionHtmlSlot,
186
- };
202
+ return { args, componentArgs };
187
203
  },
188
204
  template: `
189
205
  <StorybookComponent
190
- v-model="inputValue"
191
- v-bind="args"
206
+ v-model="args.modelValue"
207
+ v-bind="componentArgs"
192
208
  >
193
- <template v-if="useDescriptionSlot" #description>{{ descriptionContent }}</template>
194
- <template v-if="useDescriptionHtmlSlot" #descriptionHtml v-html="descriptionHtmlContent"></template>
209
+ <template v-if="args.useDescriptionSlot" #description>{{ args.descriptionContent }}</template>
210
+ <template v-if="args.useDescriptionHtmlSlot" #descriptionHtml v-html="args.descriptionHtmlContent"></template>
195
211
  </StorybookComponent>
196
212
  <div class="mt-4 text-sm text-gray-600">
197
- Current value: {{ inputValue }}
213
+ Current value: {{ args.modelValue }}
198
214
  </div>
199
215
  `,
200
216
  });
@@ -256,6 +272,16 @@ Underlined.args = {
256
272
  type: "url",
257
273
  };
258
274
 
275
+ export const DateWithMinMax = Template.bind({});
276
+ DateWithMinMax.args = {
277
+ type: "date",
278
+ label: "Appointment date",
279
+ name: "appointmentDate",
280
+ // Prevents picking a date in the past in the native browser date picker.
281
+ min: new Date().toISOString().split("T")[0],
282
+ max: "2027-12-31",
283
+ };
284
+
259
285
  export const AllThemes = Template.bind({});
260
286
  AllThemes.storyName = "All Themes";
261
287
  AllThemes.args = {};
@@ -0,0 +1,56 @@
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/InputPasswordWithLabel.vue";
5
+
6
+ const initialPropsData = {
7
+ name: "password",
8
+ label: "Password",
9
+ errorMessage: "",
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("InputPasswordWithLabel", () => {
23
+ it("mounts without error", async () => {
24
+ wrapper = await wrapperFactory();
25
+ expect(wrapper.vm).toBeTruthy();
26
+ });
27
+
28
+ it("defaults to type=password and the default toggle button text", async () => {
29
+ wrapper = await wrapperFactory();
30
+
31
+ expect(wrapper.find("input").attributes("type")).toBe("password");
32
+ expect(wrapper.find("button").text()).toContain("Show password");
33
+ });
34
+
35
+ it("toggles the input type and button text when the toggle button is clicked", async () => {
36
+ wrapper = await wrapperFactory();
37
+
38
+ await wrapper.find("button").trigger("click");
39
+
40
+ expect(wrapper.find("input").attributes("type")).toBe("text");
41
+ expect(wrapper.find("button").text()).toContain("Hide password");
42
+ });
43
+
44
+ it("uses custom showPasswordText/hidePasswordText when provided", async () => {
45
+ wrapper = await wrapperFactory({
46
+ showPasswordText: "Afficher le mot de passe",
47
+ hidePasswordText: "Masquer le mot de passe",
48
+ });
49
+
50
+ expect(wrapper.find("button").text()).toContain("Afficher le mot de passe");
51
+
52
+ await wrapper.find("button").trigger("click");
53
+
54
+ expect(wrapper.find("button").text()).toContain("Masquer le mot de passe");
55
+ });
56
+ });