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
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <div
3
+ class="input-text-field"
4
+ :class="elementClasses"
5
+ :data-theme="theme"
6
+ :data-input-variant="inputVariant"
7
+ :data-invalid="fieldHasError ? '' : null"
8
+ :data-dirty="isDirty ? '' : null"
9
+ :data-active="isActive ? '' : null"
10
+ >
11
+ <InputLabel
12
+ :id
13
+ :required
14
+ :input-variant
15
+ :field-has-error
16
+ :style-class-passthrough="['input-text-label']"
17
+ >
18
+ <template #textLabel>{{ label }}</template>
19
+ </InputLabel>
20
+
21
+ <InputDescription
22
+ v-if="inputVariant !== 'outlined'"
23
+ :description-id
24
+ :input-variant
25
+ :field-has-error="fieldHasError"
26
+ :style-class-passthrough="['input-text-description']"
27
+ >
28
+ <template v-if="slots.descriptionHtml" #descriptionHtml>
29
+ <slot name="descriptionHtml"></slot>
30
+ </template>
31
+ <template v-if="slots.descriptionText" #descriptionText>
32
+ <slot name="descriptionText"></slot>
33
+ </template>
34
+ </InputDescription>
35
+
36
+ <InputText
37
+ :id
38
+ v-model="modelValue"
39
+ v-model:is-dirty="isDirty"
40
+ v-model:is-active="isActive"
41
+ :type
42
+ :inputmode
43
+ :maxlength
44
+ :min
45
+ :max
46
+ :name
47
+ :placeholder
48
+ :field-has-error
49
+ :required
50
+ :readonly
51
+ :autocomplete
52
+ :theme
53
+ :aria-describedby="ariaDescribedby() ?? undefined"
54
+ :input-variant
55
+ >
56
+ <template v-if="slots.left" #left>
57
+ <slot name="left"></slot>
58
+ </template>
59
+ <template v-if="slots.right" #right>
60
+ <slot name="right"></slot>
61
+ </template>
62
+ </InputText>
63
+
64
+ <InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="false" :input-variant />
65
+
66
+ <InputDescription
67
+ v-if="inputVariant === 'outlined'"
68
+ :description-id
69
+ :input-variant
70
+ :field-has-error="fieldHasError"
71
+ :style-class-passthrough="['input-text-description']"
72
+ >
73
+ <template v-if="slots.descriptionHtml" #descriptionHtml>
74
+ <slot name="descriptionHtml"></slot>
75
+ </template>
76
+ <template v-if="slots.descriptionText" #descriptionText>
77
+ <slot name="descriptionText"></slot>
78
+ </template>
79
+ </InputDescription>
80
+ </div>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import type { InputTypesText, FormUiTheme, InputUiVariant, InputMode } from "~/types/forms/types.forms";
85
+
86
+ interface Props {
87
+ maxlength?: number;
88
+ min?: string | number;
89
+ max?: string | number;
90
+ type: InputTypesText;
91
+ inputmode?: InputMode;
92
+ name: string;
93
+ placeholder?: string;
94
+ label: string;
95
+ errorMessage: object | string;
96
+ fieldHasError?: boolean;
97
+ required?: boolean;
98
+ readonly?: boolean;
99
+ autocomplete?: string;
100
+ styleClassPassthrough?: string | string[];
101
+ theme?: FormUiTheme;
102
+ inputVariant?: InputUiVariant;
103
+ }
104
+
105
+ const props = withDefaults(defineProps<Props>(), {
106
+ maxlength: 255,
107
+ min: undefined,
108
+ max: undefined,
109
+ inputmode: "text",
110
+ placeholder: "",
111
+ fieldHasError: false,
112
+ required: false,
113
+ readonly: false,
114
+ autocomplete: undefined,
115
+ styleClassPassthrough: () => [],
116
+ theme: "default",
117
+ inputVariant: "normal",
118
+ });
119
+
120
+ const slots = useSlots();
121
+
122
+ const { id, errorId, descriptionId, ariaDescribedby } = useAriaDescribedById(
123
+ props.name,
124
+ toRef(props, "fieldHasError"),
125
+ slots
126
+ );
127
+
128
+ const modelValue = defineModel<string>({ required: true });
129
+ const isActive = ref<boolean>(false);
130
+ const isDirty = ref<boolean>(false);
131
+
132
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
133
+
134
+ watch(
135
+ () => props.styleClassPassthrough,
136
+ () => {
137
+ resetElementClasses(props.styleClassPassthrough);
138
+ }
139
+ );
140
+
141
+ const testDirty = () => {
142
+ const watchValue = modelValue.value ?? "";
143
+
144
+ if (!isDirty.value && typeof watchValue === "string" && watchValue.length > 0) {
145
+ isDirty.value = true;
146
+ }
147
+ };
148
+
149
+ onMounted(() => {
150
+ testDirty();
151
+ });
152
+
153
+ watch(
154
+ () => modelValue.value,
155
+ () => {
156
+ testDirty();
157
+ }
158
+ );
159
+ </script>
160
+
161
+ <style lang="css">
162
+ @layer components {
163
+ .input-text-field {
164
+ min-inline-size: 0;
165
+ overflow-wrap: anywhere;
166
+ }
167
+ }
168
+ </style>
@@ -0,0 +1,163 @@
1
+ import { computed, ref } from "vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import InputPasswordField from "../InputPasswordField.vue";
4
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
5
+ import type { FormUiTheme, InputUiVariant } from "~/types/forms/types.forms.d";
6
+ import type { MediaCanvas } from "~/types/components";
7
+
8
+ interface StoryArgs {
9
+ modelValue: string;
10
+ name: string;
11
+ label: string;
12
+ placeholder: string;
13
+ maxlength: number;
14
+ autocomplete: string;
15
+ errorMessage: string;
16
+ fieldHasError: boolean;
17
+ required: boolean;
18
+ theme: FormUiTheme;
19
+ inputVariant: InputUiVariant;
20
+ styleClassPassthrough: string[];
21
+ showPasswordText: string;
22
+ hidePasswordText: string;
23
+ descriptionText: string;
24
+ }
25
+
26
+ const meta: Meta<StoryArgs> = {
27
+ title: "Components/Forms/Input Text/InputPasswordField",
28
+ component: InputPasswordField,
29
+ argTypes: {
30
+ modelValue: { control: "text", table: { category: "Model" } },
31
+ name: { control: "text", table: { category: "Basic" } },
32
+ label: { control: "text", table: { category: "Basic" } },
33
+ placeholder: { control: "text", table: { category: "Basic" } },
34
+ maxlength: { control: "number", table: { category: "Basic" } },
35
+ autocomplete: {
36
+ control: { type: "select" },
37
+ options: ["current-password", "new-password", "off"],
38
+ table: { category: "Basic" },
39
+ },
40
+ errorMessage: { control: "text", table: { category: "Validation" } },
41
+ fieldHasError: { control: "boolean", table: { category: "Validation" } },
42
+ required: { control: "boolean", table: { category: "Validation" } },
43
+ theme: {
44
+ control: { type: "select" },
45
+ options: ["default", "success", "error", "warning"],
46
+ table: { category: "Styling" },
47
+ },
48
+ inputVariant: {
49
+ control: { type: "select" },
50
+ options: ["normal", "outlined", "underlined"],
51
+ table: { category: "Styling" },
52
+ },
53
+ styleClassPassthrough: { control: "object", table: { category: "Styling" } },
54
+ showPasswordText: { control: "text", description: "Toggle's accessible name while hidden", table: { category: "Accessibility" } },
55
+ hidePasswordText: { control: "text", description: "Toggle's accessible name while shown", table: { category: "Accessibility" } },
56
+ descriptionText: { control: "text", description: "Story-only: fills the descriptionText slot", table: { category: "Slots" } },
57
+ },
58
+ args: {
59
+ modelValue: "",
60
+ name: "password",
61
+ label: "Password",
62
+ placeholder: "",
63
+ maxlength: 255,
64
+ autocomplete: "current-password",
65
+ errorMessage: "",
66
+ fieldHasError: false,
67
+ required: false,
68
+ theme: "default",
69
+ inputVariant: "normal",
70
+ styleClassPassthrough: [],
71
+ showPasswordText: "Show password",
72
+ hidePasswordText: "Hide password",
73
+ descriptionText: "",
74
+ },
75
+ decorators: [
76
+ (story, context) => ({
77
+ components: { story, CanvasSwitcher },
78
+ setup() {
79
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
80
+ return { canvasName };
81
+ },
82
+ template: `
83
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
84
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
85
+ </div>
86
+ <div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
87
+ <story />
88
+ </div>
89
+ `,
90
+ }),
91
+ ],
92
+ parameters: {
93
+ layout: "fullscreen",
94
+ docs: {
95
+ description: {
96
+ component:
97
+ "`InputTextField` with a show/hide toggle (an inline `InputButton` in the right slot) that switches the input between `password` and `text`. `showPasswordText`/`hidePasswordText` localise the toggle's accessible name. `autocomplete` defaults to `current-password`; use `new-password` on sign-up and change-password forms.",
98
+ },
99
+ },
100
+ },
101
+ };
102
+
103
+ export default meta;
104
+ type Story = StoryObj<StoryArgs>;
105
+
106
+ const render = (args: StoryArgs) => ({
107
+ components: { InputPasswordField },
108
+ setup() {
109
+ const componentArgs = computed(() => {
110
+ const { modelValue: _modelValue, descriptionText: _descriptionText, ...rest } = args;
111
+ return rest;
112
+ });
113
+ return { args, componentArgs };
114
+ },
115
+ template: `
116
+ <InputPasswordField v-model="args.modelValue" v-bind="componentArgs">
117
+ <template v-if="args.descriptionText" #descriptionText>{{ args.descriptionText }}</template>
118
+ </InputPasswordField>
119
+ `,
120
+ });
121
+
122
+ export const Default: Story = { render };
123
+
124
+ export const NewPassword: Story = {
125
+ args: { label: "Choose a password", autocomplete: "new-password", descriptionText: "At least 12 characters." },
126
+ render,
127
+ parameters: { docs: { description: { story: "Sign-up use: `autocomplete=\"new-password\"` lets password managers offer a generated one." } } },
128
+ };
129
+
130
+ export const WithError: Story = {
131
+ args: { fieldHasError: true, errorMessage: "Password is too short", modelValue: "weak" },
132
+ render,
133
+ };
134
+
135
+ export const LocalisedToggleText: Story = {
136
+ args: { label: "Mot de passe", showPasswordText: "Afficher le mot de passe", hidePasswordText: "Masquer le mot de passe" },
137
+ render,
138
+ };
139
+
140
+ export const Underlined: Story = { args: { inputVariant: "underlined" }, render };
141
+
142
+ export const StressTest: Story = {
143
+ name: "Stress Test (Worst-Case Data)",
144
+ args: {
145
+ modelValue: "correcthorsebatterystaplecorrecthorsebatterystaplecorrecthorsebatterystaple🔑<script>",
146
+ label: "🔐 Persönlicheskennwortfürdenzugangzumkundenkontoundallenverbundenendiensten <b>not bold</b> كلمة المرور",
147
+ descriptionText: "Mindestens zwölf Zeichen, davon eine Ziffer. يجب أن تتكون كلمة المرور من اثني عشر حرفًا على الأقل",
148
+ errorMessage: "DaskennwortentsprichtleidernichtdenSicherheitsanforderungen",
149
+ fieldHasError: true,
150
+ showPasswordText: "Kennwort im Klartext anzeigen",
151
+ hidePasswordText: "Kennwort wieder verbergen",
152
+ },
153
+ render,
154
+ parameters: {
155
+ initialCanvas: "mobileCanvas",
156
+ docs: {
157
+ description: {
158
+ story:
159
+ "A long unbroken value with emoji and HTML-like text, long German/RTL copy everywhere and long toggle labels. Check at every canvas width: the toggle button keeps its size at the right edge, the copy wraps inside the dashed outline, and toggling shows the value as plain text.",
160
+ },
161
+ },
162
+ },
163
+ };
@@ -0,0 +1,194 @@
1
+ import { computed, ref } from "vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import InputText from "../InputText.vue";
4
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
5
+ import type { FormUiTheme, InputMode, InputTypesText, InputUiVariant } from "~/types/forms/types.forms.d";
6
+ import type { MediaCanvas } from "~/types/components";
7
+
8
+ interface StoryArgs {
9
+ modelValue: string;
10
+ id: string;
11
+ name: string;
12
+ type: InputTypesText;
13
+ inputmode: InputMode;
14
+ placeholder: string;
15
+ maxlength: number;
16
+ min: string;
17
+ max: string;
18
+ autocomplete: string;
19
+ required: boolean;
20
+ readonly: boolean;
21
+ fieldHasError: boolean;
22
+ theme: FormUiTheme;
23
+ inputVariant: InputUiVariant;
24
+ styleClassPassthrough: string[];
25
+ leftSlotContent: string;
26
+ rightSlotContent: string;
27
+ }
28
+
29
+ const meta: Meta<StoryArgs> = {
30
+ title: "Components/Forms/Input Text/InputText",
31
+ component: InputText,
32
+ argTypes: {
33
+ modelValue: { control: "text", table: { category: "Model" } },
34
+ id: { control: "text", table: { category: "Basic" } },
35
+ name: { control: "text", table: { category: "Basic" } },
36
+ type: {
37
+ control: { type: "select" },
38
+ options: ["text", "email", "password", "number", "tel", "url", "date"],
39
+ table: { category: "Basic" },
40
+ },
41
+ inputmode: {
42
+ control: { type: "select" },
43
+ options: ["text", "email", "tel", "url", "search", "numeric", "none", "decimal"],
44
+ description: "`numeric` also blocks typing anything but digits",
45
+ table: { category: "Basic" },
46
+ },
47
+ placeholder: { control: "text", table: { category: "Basic" } },
48
+ maxlength: { control: "number", table: { category: "Basic" } },
49
+ min: { control: "text", description: 'Native min, e.g. the earliest date for type="date"', table: { category: "Basic" } },
50
+ max: { control: "text", description: 'Native max, e.g. the latest date for type="date"', table: { category: "Basic" } },
51
+ autocomplete: { control: "text", description: "Native autocomplete token, e.g. `email`", table: { category: "Basic" } },
52
+ required: { control: "boolean", table: { category: "Validation" } },
53
+ readonly: { control: "boolean", table: { category: "States" } },
54
+ fieldHasError: { control: "boolean", table: { category: "Validation" } },
55
+ theme: {
56
+ control: { type: "select" },
57
+ options: ["default", "success", "error", "warning"],
58
+ table: { category: "Styling" },
59
+ },
60
+ inputVariant: {
61
+ control: { type: "select" },
62
+ options: ["normal", "outlined", "underlined"],
63
+ table: { category: "Styling" },
64
+ },
65
+ styleClassPassthrough: { control: "object", table: { category: "Styling" } },
66
+ leftSlotContent: { control: "text", description: "Story-only: fills the left slot", table: { category: "Slots" } },
67
+ rightSlotContent: { control: "text", description: "Story-only: fills the right slot", table: { category: "Slots" } },
68
+ },
69
+ args: {
70
+ modelValue: "",
71
+ id: "example-input",
72
+ name: "example",
73
+ type: "text",
74
+ inputmode: "text",
75
+ placeholder: "Enter text",
76
+ maxlength: 255,
77
+ min: "",
78
+ max: "",
79
+ autocomplete: "",
80
+ required: false,
81
+ readonly: false,
82
+ fieldHasError: false,
83
+ theme: "default",
84
+ inputVariant: "normal",
85
+ styleClassPassthrough: [],
86
+ leftSlotContent: "",
87
+ rightSlotContent: "",
88
+ },
89
+ decorators: [
90
+ (story, context) => ({
91
+ components: { story, CanvasSwitcher },
92
+ setup() {
93
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
94
+ return { canvasName };
95
+ },
96
+ template: `
97
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
98
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
99
+ </div>
100
+ <div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
101
+ <story />
102
+ </div>
103
+ `,
104
+ }),
105
+ ],
106
+ parameters: {
107
+ layout: "fullscreen",
108
+ docs: {
109
+ description: {
110
+ component:
111
+ "Bare native `<input>` for the `05.forms` text family: no label, description or error (use `InputTextField`, `InputPasswordField` or `InputTextAsNumberField` for those, or pair it with your own `<label for>`). `left`/`right` slots hold decorations or inline `InputButton`s. `inputmode=\"numeric\"` blocks non-digit typing. State is exposed as `data-*` attributes on `.input-text-wrapper`.",
112
+ },
113
+ },
114
+ },
115
+ };
116
+
117
+ export default meta;
118
+ type Story = StoryObj<StoryArgs>;
119
+
120
+ const render = (args: StoryArgs) => ({
121
+ components: { InputText },
122
+ setup() {
123
+ const componentArgs = computed(() => {
124
+ const {
125
+ modelValue: _modelValue,
126
+ leftSlotContent: _left,
127
+ rightSlotContent: _right,
128
+ min,
129
+ max,
130
+ autocomplete,
131
+ ...rest
132
+ } = args;
133
+ return { ...rest, min: min || undefined, max: max || undefined, autocomplete: autocomplete || undefined };
134
+ });
135
+ return { args, componentArgs };
136
+ },
137
+ template: `
138
+ <InputText v-model="args.modelValue" v-bind="componentArgs">
139
+ <template v-if="args.leftSlotContent" #left>{{ args.leftSlotContent }}</template>
140
+ <template v-if="args.rightSlotContent" #right>{{ args.rightSlotContent }}</template>
141
+ </InputText>
142
+ <p style="margin-block-start: 1.6rem; font-family: monospace;">modelValue: {{ JSON.stringify(args.modelValue) }}</p>
143
+ `,
144
+ });
145
+
146
+ export const Default: Story = { render };
147
+
148
+ export const WithSlots: Story = { args: { leftSlotContent: "📧", rightSlotContent: "🔍" }, render };
149
+
150
+ export const WithError: Story = { args: { fieldHasError: true, theme: "error" }, render };
151
+
152
+ export const Readonly: Story = {
153
+ args: { readonly: true, modelValue: "en-GB", inputVariant: "underlined" },
154
+ render,
155
+ parameters: { docs: { description: { story: "`readonly` reaches the native input and sets `data-readonly` on the wrapper." } } },
156
+ };
157
+
158
+ export const NumericOnly: Story = {
159
+ args: { inputmode: "numeric", placeholder: "Digits only" },
160
+ render,
161
+ parameters: {
162
+ docs: { description: { story: "`inputmode=\"numeric\"` shows a number keypad on phones and blocks typing anything but digits." } },
163
+ },
164
+ };
165
+
166
+ export const Underlined: Story = { args: { inputVariant: "underlined" }, render };
167
+
168
+ export const DateWithMinMax: Story = {
169
+ args: { type: "date", name: "appointmentDate", min: new Date().toISOString().split("T")[0], max: "2027-12-31" },
170
+ render,
171
+ parameters: { docs: { description: { story: "A date input whose picker can't go before today." } } },
172
+ };
173
+
174
+ export const StressTest: Story = {
175
+ name: "Stress Test (Worst-Case Data)",
176
+ args: {
177
+ modelValue:
178
+ "Donaudampfschifffahrtsgesellschaftskapitänswitwenrentenversicherung https://example.com/a/very/long/unbroken/url <b>not bold</b>",
179
+ placeholder: "Bitte geben Sie hier Ihre vollständige Lieferadresse einschließlich Postleitzahl ein",
180
+ leftSlotContent: "🔎🔎🔎",
181
+ rightSlotContent: "عربى",
182
+ fieldHasError: true,
183
+ },
184
+ render,
185
+ parameters: {
186
+ initialCanvas: "mobileCanvas",
187
+ docs: {
188
+ description: {
189
+ story:
190
+ "A long unbroken German, URL and HTML-like value with a long placeholder and text in both slots. Check at every canvas width: the input stays inside the dashed outline and scrolls its text instead of growing, the slots keep their size, and markup renders as text. Clear it to check the placeholder.",
191
+ },
192
+ },
193
+ },
194
+ };