spoko-design-system 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,124 +1,124 @@
1
- {
2
- "name": "spoko-design-system",
3
- "version": "0.5.7",
4
- "private": false,
5
- "main": "./index.ts",
6
- "module": "./index.ts",
7
- "types": "./index.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./index.ts",
11
- "require": "./index.ts"
12
- },
13
- "./styles/*": "./src/styles/*",
14
- "./icons": "./icon.config.ts",
15
- "./icon-collections": "./icon.collections.ts",
16
- "./uno-config": "./uno-config/index.ts"
17
- },
18
- "scripts": {
19
- "dev": "astro dev",
20
- "start": "astro dev",
21
- "build": "astro build",
22
- "preview": "astro preview"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/polo-blue/sds"
27
- },
28
- "author": {
29
- "name": "spokospace",
30
- "email": "szymon@spoko.space",
31
- "url": "https://spoko.space"
32
- },
33
- "homepage": "https://sds.spoko.space/",
34
- "license": "MIT",
35
- "keywords": [
36
- "astro-starter",
37
- "seo",
38
- "astro",
39
- "sds design system",
40
- "spoko design system"
41
- ],
42
- "dependencies": {
43
- "@algolia/client-search": "^5.20.3",
44
- "@astrojs/mdx": "^4.0.8",
45
- "@astrojs/node": "^9.1.1",
46
- "@astrojs/sitemap": "^3.2.1",
47
- "@astrojs/ts-plugin": "^1.10.4",
48
- "@astrojs/vue": "^5.0.7",
49
- "@docsearch/css": "^3.9.0",
50
- "@iconify-json/ant-design": "^1.2.5",
51
- "@iconify-json/bi": "^1.2.2",
52
- "@iconify-json/bx": "^1.2.2",
53
- "@iconify-json/carbon": "^1.2.7",
54
- "@iconify-json/circle-flags": "^1.2.6",
55
- "@iconify-json/ei": "^1.2.2",
56
- "@iconify-json/el": "^1.2.1",
57
- "@iconify-json/eos-icons": "^1.2.2",
58
- "@iconify-json/et": "^1.2.1",
59
- "@iconify-json/flowbite": "^1.2.4",
60
- "@iconify-json/fluent": "^1.2.14",
61
- "@iconify-json/fluent-emoji": "1.2.3",
62
- "@iconify-json/ic": "^1.2.2",
63
- "@iconify-json/icon-park-outline": "^1.2.2",
64
- "@iconify-json/la": "^1.2.1",
65
- "@iconify-json/material-symbols-light": "^1.2.14",
66
- "@iconify-json/mdi": "^1.2.3",
67
- "@iconify-json/noto-v1": "^1.2.1",
68
- "@iconify-json/octicon": "^1.2.5",
69
- "@iconify-json/ph": "^1.2.2",
70
- "@iconify-json/simple-icons": "^1.2.26",
71
- "@iconify-json/system-uicons": "^1.2.2",
72
- "@iconify-json/uil": "^1.2.3",
73
- "@iconify/json": "^2.2.310",
74
- "@iconify/vue": "^4.3.0",
75
- "@playform/compress": "^0.1.7",
76
- "@playform/inline": "^0.1.1",
77
- "@unocss/astro": "^66.0.0",
78
- "@unocss/preset-attributify": "^66.0.0",
79
- "@unocss/preset-typography": "^66.0.0",
80
- "@unocss/preset-uno": "^66.0.0",
81
- "@unocss/preset-web-fonts": "^66.0.0",
82
- "@unocss/preset-wind": "^66.0.0",
83
- "@unocss/reset": "^66.0.0",
84
- "@vite-pwa/astro": "^0.5.0",
85
- "@vueuse/core": "^12.7.0",
86
- "astro-i18next": "1.0.0-beta.21",
87
- "astro-icon": "^1.1.5",
88
- "astro-meta-tags": "^0.3.1",
89
- "astro-navbar": "^2.3.9",
90
- "astro-pagefind": "^1.8.1",
91
- "astro-remote": "^0.3.3",
92
- "dotenv": "^16.4.7",
93
- "i18next": "^24.2.2",
94
- "i18next-browser-languagedetector": "^8.0.4",
95
- "i18next-fs-backend": "^2.6.0",
96
- "i18next-http-backend": "^3.0.2",
97
- "i18next-vue": "^5.2.0",
98
- "swiper": "^11.2.4",
99
- "unocss": "^66.0.0",
100
- "vue": "^3.5.13"
101
- },
102
- "devDependencies": {
103
- "@types/gtag.js": "^0.0.20",
104
- "@types/node": "^22.13.5",
105
- "@unocss/transformer-variant-group": "^66.0.0",
106
- "@vitejs/plugin-vue": "^5.2.1",
107
- "@vue/compiler-sfc": "^3.5.13",
108
- "astro": "^5.3.1",
109
- "unocss": "^0.65.0",
110
- "vite": "^6.2.0"
111
- },
112
- "packageManager": "pnpm@9.15.3",
113
- "pnpm": {
114
- "default": "9.15.3",
115
- "overrides": {
116
- "file-type@>=17.0.0 <17.1.3": ">=17.1.3",
117
- "sharp@<0.30.5": ">=0.30.5"
118
- }
119
- },
120
- "engines": {
121
- "node": ">=18.14.1",
122
- "pnpm": ">=9.15.3"
123
- }
124
- }
1
+ {
2
+ "name": "spoko-design-system",
3
+ "version": "0.5.9",
4
+ "private": false,
5
+ "main": "./index.ts",
6
+ "module": "./index.ts",
7
+ "types": "./index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.ts",
11
+ "require": "./index.ts"
12
+ },
13
+ "./styles/*": "./src/styles/*",
14
+ "./icons": "./icon.config.ts",
15
+ "./icon-collections": "./icon.collections.ts",
16
+ "./uno-config": "./uno-config/index.ts"
17
+ },
18
+ "scripts": {
19
+ "dev": "astro dev",
20
+ "start": "astro dev",
21
+ "build": "astro build",
22
+ "preview": "astro preview"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/polo-blue/sds"
27
+ },
28
+ "author": {
29
+ "name": "spokospace",
30
+ "email": "szymon@spoko.space",
31
+ "url": "https://spoko.space"
32
+ },
33
+ "homepage": "https://sds.spoko.space/",
34
+ "license": "MIT",
35
+ "keywords": [
36
+ "astro-starter",
37
+ "seo",
38
+ "astro",
39
+ "sds design system",
40
+ "spoko design system"
41
+ ],
42
+ "dependencies": {
43
+ "@algolia/client-search": "^5.20.3",
44
+ "@astrojs/mdx": "^4.0.8",
45
+ "@astrojs/node": "^9.1.1",
46
+ "@astrojs/sitemap": "^3.2.1",
47
+ "@astrojs/ts-plugin": "^1.10.4",
48
+ "@astrojs/vue": "^5.0.7",
49
+ "@docsearch/css": "^3.9.0",
50
+ "@iconify-json/ant-design": "^1.2.5",
51
+ "@iconify-json/bi": "^1.2.2",
52
+ "@iconify-json/bx": "^1.2.2",
53
+ "@iconify-json/carbon": "^1.2.7",
54
+ "@iconify-json/circle-flags": "^1.2.6",
55
+ "@iconify-json/ei": "^1.2.2",
56
+ "@iconify-json/el": "^1.2.1",
57
+ "@iconify-json/eos-icons": "^1.2.2",
58
+ "@iconify-json/et": "^1.2.1",
59
+ "@iconify-json/flowbite": "^1.2.4",
60
+ "@iconify-json/fluent": "^1.2.14",
61
+ "@iconify-json/fluent-emoji": "1.2.3",
62
+ "@iconify-json/ic": "^1.2.2",
63
+ "@iconify-json/icon-park-outline": "^1.2.2",
64
+ "@iconify-json/la": "^1.2.1",
65
+ "@iconify-json/material-symbols-light": "^1.2.14",
66
+ "@iconify-json/mdi": "^1.2.3",
67
+ "@iconify-json/noto-v1": "^1.2.1",
68
+ "@iconify-json/octicon": "^1.2.5",
69
+ "@iconify-json/ph": "^1.2.2",
70
+ "@iconify-json/simple-icons": "^1.2.26",
71
+ "@iconify-json/system-uicons": "^1.2.2",
72
+ "@iconify-json/uil": "^1.2.3",
73
+ "@iconify/json": "^2.2.310",
74
+ "@iconify/vue": "^4.3.0",
75
+ "@playform/compress": "^0.1.7",
76
+ "@playform/inline": "^0.1.1",
77
+ "@unocss/astro": "66.1.0-beta.2",
78
+ "@unocss/preset-attributify": "66.1.0-beta.2",
79
+ "@unocss/preset-typography": "66.1.0-beta.2",
80
+ "@unocss/preset-uno": "66.1.0-beta.2",
81
+ "@unocss/preset-web-fonts": "66.1.0-beta.2",
82
+ "@unocss/preset-wind": "66.1.0-beta.2",
83
+ "@unocss/reset": "66.1.0-beta.2",
84
+ "@vite-pwa/astro": "^0.5.0",
85
+ "@vueuse/core": "^12.7.0",
86
+ "astro-i18next": "1.0.0-beta.21",
87
+ "astro-icon": "^1.1.5",
88
+ "astro-meta-tags": "^0.3.1",
89
+ "astro-navbar": "^2.3.9",
90
+ "astro-pagefind": "^1.8.1",
91
+ "astro-remote": "^0.3.3",
92
+ "dotenv": "^16.4.7",
93
+ "i18next": "^24.2.2",
94
+ "i18next-browser-languagedetector": "^8.0.4",
95
+ "i18next-fs-backend": "^2.6.0",
96
+ "i18next-http-backend": "^3.0.2",
97
+ "i18next-vue": "^5.2.0",
98
+ "swiper": "^11.2.4",
99
+ "unocss": "66.1.0-beta.2",
100
+ "vue": "^3.5.13"
101
+ },
102
+ "devDependencies": {
103
+ "@types/gtag.js": "^0.0.20",
104
+ "@types/node": "^22.13.5",
105
+ "@unocss/transformer-variant-group": "66.1.0-beta.2",
106
+ "@vitejs/plugin-vue": "^5.2.1",
107
+ "@vue/compiler-sfc": "^3.5.13",
108
+ "astro": "^5.3.1",
109
+ "unocss": "^0.65.0",
110
+ "vite": "^6.2.0"
111
+ },
112
+ "packageManager": "pnpm@9.15.3",
113
+ "pnpm": {
114
+ "default": "9.15.3",
115
+ "overrides": {
116
+ "file-type@>=17.0.0 <17.1.3": ">=17.1.3",
117
+ "sharp@<0.30.5": ">=0.30.5"
118
+ }
119
+ },
120
+ "engines": {
121
+ "node": ">=18.14.1",
122
+ "pnpm": ">=9.15.3"
123
+ }
124
+ }
@@ -0,0 +1,153 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs } from 'vue';
3
+
4
+ interface InputProps {
5
+ id?: string;
6
+ name?: string;
7
+ label: string;
8
+ variant?: 'filled' | 'standard';
9
+ type?: string;
10
+ modelValue?: string | number;
11
+ required?: boolean;
12
+ rows?: number;
13
+ placeholder?: string;
14
+ error?: string | boolean;
15
+ success?: string | boolean;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ class?: string;
18
+ [key: string]: any;
19
+ }
20
+
21
+ const props = withDefaults(defineProps<InputProps>(), {
22
+ id: () => `input-${Math.random().toString(36).substring(2, 9)}`,
23
+ name: undefined,
24
+ variant: 'standard',
25
+ type: 'text',
26
+ modelValue: '',
27
+ required: false,
28
+ rows: 3,
29
+ placeholder: ' ', // space for "floating label"
30
+ error: false,
31
+ success: false,
32
+ size: 'md',
33
+ class: ''
34
+ });
35
+
36
+ const emit = defineEmits(['update:modelValue', 'input', 'focus', 'blur']);
37
+
38
+ // Handle external attrs
39
+ const attrs = useAttrs();
40
+
41
+ // Compute wrapper class - uses existing shortcut
42
+ const wrapperClass = computed(() => `input-wrapper-${props.variant}`);
43
+
44
+ // Compute input classes - uses shortcuts
45
+ const inputClass = computed(() => {
46
+ const classes = ['input-base', 'input-placeholder', `input-${props.variant}`];
47
+
48
+ // Add size class
49
+ if (props.size) classes.push(`input-${props.size}`);
50
+
51
+ // Add textarea class if needed
52
+ if (props.type === 'textarea') classes.push('input-textarea');
53
+
54
+ // Add status classes
55
+ if (props.error) classes.push('input-error');
56
+ else if (props.success) classes.push('input-success');
57
+
58
+ // Add custom classes
59
+ if (props.class) classes.push(props.class);
60
+
61
+ return classes.join(' ');
62
+ });
63
+
64
+ // Compute label classes - using optimized shortcuts
65
+ const labelClass = computed(() => {
66
+ const classes = [
67
+ // Base label style
68
+ 'input-label-base',
69
+
70
+ // Position styling
71
+ `input-label-${props.variant}`,
72
+
73
+ // State styling - contains all transformations for the specific variant
74
+ `input-label-${props.variant}-state`
75
+ ];
76
+
77
+ // Add size class
78
+ if (props.size) classes.push(`input-label-${props.size}`);
79
+
80
+ // Add status classes
81
+ if (props.error) classes.push('input-label-error');
82
+ else if (props.success) classes.push('input-label-success');
83
+
84
+ return classes.join(' ');
85
+ });
86
+
87
+ // Event handlers
88
+ const handleInput = (event: Event) => {
89
+ const target = event.target as HTMLInputElement | HTMLTextAreaElement;
90
+ emit('update:modelValue', target.value);
91
+ emit('input', event);
92
+ };
93
+
94
+ const handleFocus = (event: FocusEvent) => emit('focus', event);
95
+ const handleBlur = (event: FocusEvent) => emit('blur', event);
96
+ </script>
97
+
98
+ <template>
99
+ <div :class="wrapperClass">
100
+ <textarea
101
+ v-if="type === 'textarea'"
102
+ :id="id"
103
+ :name="name || id"
104
+ :rows="rows"
105
+ :required="required"
106
+ :class="inputClass + ' peer'"
107
+ :placeholder="placeholder"
108
+ :value="modelValue"
109
+ @input="handleInput"
110
+ @focus="handleFocus"
111
+ @blur="handleBlur"
112
+ v-bind="attrs"
113
+ ></textarea>
114
+
115
+ <input
116
+ v-else
117
+ :type="type"
118
+ :id="id"
119
+ :name="name || id"
120
+ :required="required"
121
+ :class="inputClass + ' peer'"
122
+ :placeholder="placeholder"
123
+ :value="modelValue"
124
+ @input="handleInput"
125
+ @focus="handleFocus"
126
+ @blur="handleBlur"
127
+ v-bind="attrs"
128
+ />
129
+
130
+ <label
131
+ :for="id"
132
+ :class="labelClass"
133
+ style="transform-origin: top left;"
134
+ >
135
+ {{ label }}
136
+ <span v-if="required" class="text-red-500 ml-1">*</span>
137
+ </label>
138
+
139
+ <div
140
+ v-if="error && typeof error === 'string'"
141
+ class="input-error-message"
142
+ >
143
+ {{ error }}
144
+ </div>
145
+
146
+ <div
147
+ v-if="success && typeof success === 'string'"
148
+ class="input-success-message"
149
+ >
150
+ {{ success }}
151
+ </div>
152
+ </div>
153
+ </template>
@@ -2,18 +2,7 @@
2
2
  title: Input
3
3
  layout: ../../layouts/MainLayout.astro
4
4
  ---
5
- import MainInput from '../../components/MainInput.vue'
6
- import Input from '../../components/Input.astro';
7
-
8
- # Basic Input text
9
-
10
- <div class="component-preview">
11
- <MainInput label="Name"></MainInput>
12
- </div>
13
-
14
- ```js
15
- <MainInput type="text" value="Hello world!"></MainInput>
16
- ```
5
+ import Input from '../../components/Input.vue';
17
6
 
18
7
  # Floating Label Input
19
8
  <div class="component-preview">
@@ -55,6 +44,7 @@ import Input from '../../components/Input.astro';
55
44
  id="name-filled"
56
45
  label="Floating filled"
57
46
  variant="filled"
47
+
58
48
  type="textarea"
59
49
  />
60
50
  <Input
@@ -82,4 +72,297 @@ import Input from '../../components/Input.astro';
82
72
  variant="standard"
83
73
  type="textarea"
84
74
  />
85
- ```
75
+ ```
76
+
77
+ # Input with Error State
78
+ The Input component supports error states to provide validation feedback to users. You can pass a string as the error prop to display a specific error message.
79
+
80
+ <div class="component-preview">
81
+ <div class="bg-white grid items-end w-full gap-6 md:grid-cols-2 px-4 py-6">
82
+ <Input
83
+ id="error-filled"
84
+ label="With error"
85
+ variant="filled"
86
+ error="This field is required"
87
+ />
88
+ <Input
89
+ id="error-standard"
90
+ label="With error"
91
+ variant="standard"
92
+ error="Please enter a valid email"
93
+ />
94
+ </div>
95
+ </div>
96
+
97
+ ```js
98
+ // Error with filled variant
99
+ <Input
100
+ id="error-filled"
101
+ label="With error"
102
+ variant="filled"
103
+ error="This field is required"
104
+ />
105
+
106
+ // Error with standard variant
107
+ <Input
108
+ id="error-standard"
109
+ label="With error"
110
+ variant="standard"
111
+ error="Please enter a valid email"
112
+ />
113
+ ```
114
+
115
+ You can also pass a boolean `true` to indicate an error state without a message:
116
+
117
+ ```js
118
+ <Input
119
+ label="Username"
120
+ error={true}
121
+ />
122
+ ```
123
+
124
+ # Input with Success State
125
+ Similar to errors, you can indicate a successful validation state using the success prop:
126
+
127
+ <div class="component-preview">
128
+ <div class="bg-white grid items-end w-full gap-6 md:grid-cols-2 px-4 py-6">
129
+ <Input
130
+ id="success-filled"
131
+ label="With success"
132
+ variant="filled"
133
+ success="Username is available"
134
+ />
135
+ <Input
136
+ id="success-standard"
137
+ label="With success"
138
+ variant="standard"
139
+ success={true}
140
+ />
141
+ </div>
142
+ </div>
143
+
144
+ ```js
145
+ // Success with message
146
+ <Input
147
+ id="success-filled"
148
+ label="With success"
149
+ variant="filled"
150
+ success="Username is available"
151
+ />
152
+
153
+ // Success without message
154
+ <Input
155
+ id="success-standard"
156
+ label="With success"
157
+ variant="standard"
158
+ success={true}
159
+ />
160
+ ```
161
+
162
+ # Input Sizes
163
+ The Input component supports different sizes: `sm`, `md` (default), and `lg`.
164
+
165
+ <div class="component-preview">
166
+ <div class="bg-white grid items-end w-full gap-6 px-4 py-6">
167
+ <Input
168
+ id="small-input"
169
+ label="Small input"
170
+ variant="filled"
171
+ size="sm"
172
+ />
173
+ <Input
174
+ id="medium-input"
175
+ label="Medium input (default)"
176
+ variant="filled"
177
+ size="md"
178
+ />
179
+ <Input
180
+ id="large-input"
181
+ label="Large input"
182
+ variant="filled"
183
+ size="lg"
184
+ />
185
+ </div>
186
+ </div>
187
+
188
+ ```js
189
+ // Small input
190
+ <Input
191
+ id="small-input"
192
+ label="Small input"
193
+ variant="filled"
194
+ size="sm"
195
+ />
196
+
197
+ // Medium input (default)
198
+ <Input
199
+ id="medium-input"
200
+ label="Medium input (default)"
201
+ variant="filled"
202
+ size="md"
203
+ />
204
+
205
+ // Large input
206
+ <Input
207
+ id="large-input"
208
+ label="Large input"
209
+ variant="filled"
210
+ size="lg"
211
+ />
212
+ ```
213
+
214
+ # Required Input
215
+ You can mark an input as required, which will add a red asterisk to the label:
216
+
217
+ <div class="component-preview">
218
+ <div class="bg-white grid items-end w-full gap-6 md:grid-cols-2 px-4 py-6">
219
+ <Input
220
+ id="required-filled"
221
+ label="Required field"
222
+ variant="filled"
223
+ required
224
+ />
225
+ <Input
226
+ id="required-standard"
227
+ label="Required field"
228
+ variant="standard"
229
+ required
230
+ />
231
+ </div>
232
+ </div>
233
+
234
+ ```js
235
+ <Input
236
+ id="required-filled"
237
+ label="Required field"
238
+ variant="filled"
239
+ required
240
+ />
241
+ ```
242
+
243
+ # Input Types
244
+ The Input component supports all standard HTML input types, plus a special `textarea` type:
245
+
246
+ <div class="component-preview">
247
+ <div class="bg-white grid items-end w-full gap-6 md:grid-cols-2 px-4 py-6">
248
+ <Input
249
+ id="email-input"
250
+ label="Email"
251
+ type="email"
252
+ variant="filled"
253
+ />
254
+ <Input
255
+ id="password-input"
256
+ label="Password"
257
+ type="password"
258
+ variant="standard"
259
+ />
260
+ <Input
261
+ id="number-input"
262
+ label="Age"
263
+ type="number"
264
+ variant="filled"
265
+ />
266
+ <Input
267
+ id="date-input"
268
+ label="Date"
269
+ type="date"
270
+ variant="standard"
271
+ />
272
+ </div>
273
+ </div>
274
+
275
+ ```js
276
+ // Email input
277
+ <Input
278
+ id="email-input"
279
+ label="Email"
280
+ type="email"
281
+ variant="filled"
282
+ />
283
+
284
+ // Password input
285
+ <Input
286
+ id="password-input"
287
+ label="Password"
288
+ type="password"
289
+ variant="standard"
290
+ />
291
+
292
+ // Number input
293
+ <Input
294
+ id="number-input"
295
+ label="Age"
296
+ type="number"
297
+ variant="filled"
298
+ />
299
+
300
+ // Date input
301
+ <Input
302
+ id="date-input"
303
+ label="Date"
304
+ type="date"
305
+ variant="standard"
306
+ />
307
+ ```
308
+
309
+ # Using with v-model
310
+ The Input component fully supports Vue's v-model for two-way binding:
311
+
312
+ ```js
313
+ <script setup>
314
+ import { ref } from 'vue';
315
+ import { Input } from 'spoko-design-system';
316
+
317
+ const username = ref('');
318
+ const email = ref('');
319
+ </script>
320
+
321
+ <template>
322
+ <Input
323
+ v-model="username"
324
+ label="Username"
325
+ variant="filled"
326
+ />
327
+
328
+ <Input
329
+ v-model="email"
330
+ label="Email"
331
+ variant="standard"
332
+ type="email"
333
+ />
334
+
335
+ <div>
336
+ Current values:
337
+ <p>Username: {{ username }}</p>
338
+ <p>Email: {{ email }}</p>
339
+ </div>
340
+ </template>
341
+ ```
342
+
343
+ # Props Reference
344
+
345
+ | Prop | Type | Default | Description |
346
+ |------|------|---------|-------------|
347
+ | `id` | `string` | Random ID | Unique identifier for the input |
348
+ | `name` | `string` | Same as id | Name attribute for the input field |
349
+ | `label` | `string` | Required | Label text for the input |
350
+ | `variant` | `'standard' \| 'filled'` | `'standard'` | Visual style variant |
351
+ | `type` | `string` | `'text'` | Input type (all HTML types + 'textarea') |
352
+ | `modelValue` | `string \| number` | `''` | Value for v-model binding |
353
+ | `required` | `boolean` | `false` | Whether the field is required |
354
+ | `rows` | `number` | `3` | Number of rows (for textarea only) |
355
+ | `placeholder` | `string` | `' '` | Placeholder text (space for floating label) |
356
+ | `error` | `string \| boolean` | `false` | Error state or message |
357
+ | `success` | `string \| boolean` | `false` | Success state or message |
358
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the input |
359
+ | `class` | `string` | `''` | Additional CSS classes |
360
+
361
+ # Events
362
+
363
+ | Event | Parameters | Description |
364
+ |-------|------------|-------------|
365
+ | `update:modelValue` | `(value: string \| number)` | Emitted when input value changes (for v-model) |
366
+ | `input` | `(event: Event)` | Native input event |
367
+ | `focus` | `(event: FocusEvent)` | Native focus event |
368
+ | `blur` | `(event: FocusEvent)` | Native blur event |
@@ -14,6 +14,26 @@ import {
14
14
  import { shortcuts } from './theme/shortcuts';
15
15
  import { theme } from './theme';
16
16
 
17
+ // List of peer selectors we want to preserve during build
18
+ const peerSelectorClasses = [
19
+ // Focus state classes
20
+ 'peer-focus:text-blue-light',
21
+ 'peer-focus:dark:text-blue-lightest',
22
+ 'peer-focus:scale-75',
23
+ 'peer-focus:-translate-y-6',
24
+ 'peer-focus:-translate-y-4',
25
+ 'peer-focus:start-0',
26
+
27
+ // Placeholder shown classes
28
+ 'peer-placeholder-shown:scale-100',
29
+ 'peer-placeholder-shown:translate-y-0',
30
+
31
+ // Not placeholder shown classes
32
+ 'peer-not-placeholder-shown:scale-75',
33
+ 'peer-not-placeholder-shown:-translate-y-6',
34
+ 'peer-not-placeholder-shown:-translate-y-4',
35
+ ];
36
+
17
37
  interface CustomConfig extends Partial<UserConfig> {
18
38
  shortcuts?: UserShortcuts;
19
39
  theme?: Partial<typeof theme>;
@@ -33,8 +53,159 @@ export function createSdsConfig(customConfig: CustomConfig = {}) {
33
53
  ...theme,
34
54
  ...(customConfig.theme || {})
35
55
  },
56
+ // Enhanced variants to better handle peer selectors
57
+ variants: [
58
+ // Add specific peer variant support
59
+ (matcher) => {
60
+ if (!matcher.startsWith('peer-'))
61
+ return matcher;
62
+
63
+ const peerVariant = matcher.slice(5);
64
+ const selectorMap = {
65
+ 'focus:': (s) => `.peer:focus ~ ${s}`,
66
+ 'hover:': (s) => `.peer:hover ~ ${s}`,
67
+ 'placeholder-shown:': (s) => `.peer:placeholder-shown ~ ${s}`,
68
+ 'not-placeholder-shown:': (s) => `.peer:not(:placeholder-shown) ~ ${s}`,
69
+ };
70
+
71
+ // Check for nested variants like 'peer-focus:text-blue'
72
+ for (const [key, selectorFn] of Object.entries(selectorMap)) {
73
+ if (peerVariant.startsWith(key)) {
74
+ return {
75
+ matcher: peerVariant.slice(key.length),
76
+ selector: selectorFn,
77
+ };
78
+ }
79
+ }
80
+
81
+ // Default peer handling
82
+ return {
83
+ matcher: peerVariant,
84
+ selector: (s) => `.peer:${peerVariant} ~ ${s}`,
85
+ };
86
+ },
87
+ ],
88
+ // Comprehensive safelist with all needed classes
36
89
  safelist: [
37
- 'md:grid-cols-product'
90
+ // Existing safelist items
91
+ 'md:grid-cols-product',
92
+
93
+ // Base peer class
94
+ 'peer',
95
+
96
+ // All input component classes from shortcuts
97
+ 'input-base',
98
+ 'input-label-base',
99
+ 'input-placeholder',
100
+ 'input-standard',
101
+ 'input-filled',
102
+ 'input-wrapper-standard',
103
+ 'input-wrapper-filled',
104
+ 'input-label-standard',
105
+ 'input-label-filled',
106
+
107
+ // Label state shortcuts
108
+ 'input-label-focus-color',
109
+ 'input-label-focus-scale',
110
+ 'input-label-focus-translate-standard',
111
+ 'input-label-focus-translate-filled',
112
+ 'input-label-placeholder',
113
+ 'input-label-filled-standard',
114
+ 'input-label-filled-filled',
115
+ 'input-label-standard-state',
116
+ 'input-label-filled-state',
117
+
118
+ // Input types
119
+ 'input-textarea',
120
+ 'resize-none',
121
+
122
+ // Size variants
123
+ 'input-sm',
124
+ 'input-md',
125
+ 'input-lg',
126
+ 'input-label-sm',
127
+ 'input-label-md',
128
+ 'input-label-lg',
129
+
130
+ // Status classes
131
+ 'input-error',
132
+ 'input-label-error',
133
+ 'input-error-message',
134
+ 'input-success',
135
+ 'input-label-success',
136
+ 'input-success-message',
137
+
138
+ // Transform related classes
139
+ 'origin-top-left',
140
+ 'transform-gpu',
141
+ 'translate-y-0',
142
+ '-translate-y-4',
143
+ '-translate-y-6',
144
+ 'scale-75',
145
+ 'scale-100',
146
+
147
+ // Every possible arbitrary selector used
148
+ '[&:focus~label]:scale-75',
149
+ '[&:focus~label]:-translate-y-4',
150
+ '[&:focus~label]:-translate-y-6',
151
+ '[&:focus~label]:text-blue-light',
152
+ '[&:focus~label]:dark:text-blue-lightest',
153
+ '[&:focus~label]:start-0',
154
+ '[&:placeholder-shown~label]:scale-100',
155
+ '[&:placeholder-shown~label]:translate-y-0',
156
+ '[&:not(:placeholder-shown)~label]:scale-75',
157
+ '[&:not(:placeholder-shown)~label]:-translate-y-4',
158
+ '[&:not(:placeholder-shown)~label]:-translate-y-6',
159
+
160
+ // Combinations of selectors
161
+ 'peer:focus:text-blue-light',
162
+ 'peer:focus:dark:text-blue-lightest',
163
+ 'peer:focus:scale-75',
164
+ 'peer:focus:-translate-y-4',
165
+ 'peer:focus:-translate-y-6',
166
+ 'peer:focus:start-0',
167
+ 'peer-placeholder-shown:scale-100',
168
+ 'peer-placeholder-shown:translate-y-0',
169
+ 'peer-not-placeholder-shown:scale-75',
170
+ 'peer-not-placeholder-shown:-translate-y-4',
171
+ 'peer-not-placeholder-shown:-translate-y-6',
172
+
173
+ // With !important for good measure
174
+ '[&:focus~label]:!scale-75',
175
+ '[&:focus~label]:!-translate-y-4',
176
+ '[&:focus~label]:!-translate-y-6',
177
+ '[&:not(:placeholder-shown)~label]:!scale-75',
178
+ '[&:not(:placeholder-shown)~label]:!-translate-y-4',
179
+ '[&:not(:placeholder-shown)~label]:!-translate-y-6',
180
+
181
+ // Direct css vars that might be used
182
+ '--un-scale-x',
183
+ '--un-scale-y',
184
+ '--un-translate-y',
185
+
186
+ // All peer selectors from the list
187
+ ...peerSelectorClasses,
188
+ ],
189
+ // Custom extractors to ensure peer classes are preserved
190
+ extractors: [
191
+ {
192
+ name: 'vue-astro',
193
+ extract({ code }) {
194
+ const result = new Set();
195
+
196
+ // Extract all peer selectors in the code
197
+ const peerRegex = /peer-([a-zA-Z0-9-]+:[a-zA-Z0-9-]+)/g;
198
+ const peerMatches = code.match(peerRegex);
199
+ if (peerMatches) {
200
+ peerMatches.forEach(match => result.add(match));
201
+ }
202
+
203
+ // Add all known peer selectors
204
+ peerSelectorClasses.forEach(cls => result.add(cls));
205
+
206
+ return result;
207
+ },
208
+ },
38
209
  ],
39
210
  presets: [
40
211
  presetUno(),
@@ -5,6 +5,7 @@ import { layoutShortcuts } from './layout';
5
5
  import { componentShortcuts } from './components';
6
6
  import { productShortcuts } from './product';
7
7
  import { jumbotronShortcuts } from './jumbotron';
8
+ import { inputShortcuts } from './inputs';
8
9
 
9
10
  const convertToShortcuts = (shortcuts: string[][]): UserShortcuts => {
10
11
  return Object.fromEntries(shortcuts.map(([name, value]) => [name, value]));
@@ -16,4 +17,5 @@ export const shortcuts: UserShortcuts = {
16
17
  ...convertToShortcuts(componentShortcuts),
17
18
  ...convertToShortcuts(productShortcuts),
18
19
  ...convertToShortcuts(jumbotronShortcuts),
20
+ ...convertToShortcuts(inputShortcuts),
19
21
  };
@@ -0,0 +1,64 @@
1
+ // uno-config\theme\shortcuts\inputs.ts
2
+ // Complete shortcuts for Input component with floating labels support
3
+
4
+ export const inputShortcuts = [
5
+ // Base input - core class for the input without peer selectors
6
+ ['input-base', 'block w-full text-4.5 text-blue-medium border-0 border-b-1 border-neutral-light appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-lightest focus:outline-none focus:ring-0 focus:border-blue-medium'],
7
+
8
+ // Base label - basic styles for the label without transformations and states
9
+ ['input-label-base', 'absolute text-sm text-slate-light dark:text-neutral-default origin-top-left transform-gpu transition-all duration-300 ease-in-out'],
10
+
11
+ // Base placeholders
12
+ ['input-placeholder', 'placeholder:text-slate-light dark:placeholder:text-neutral-default placeholder:opacity-60'],
13
+
14
+ // Variant-specific container classes
15
+ ['input-wrapper-standard', 'relative z-0'],
16
+ ['input-wrapper-filled', 'relative'],
17
+
18
+ // Input variants - styling without peer selectors
19
+ ['input-standard', 'py-2.5 px-0 bg-transparent'],
20
+ ['input-filled', 'rounded-t-lg px-2.5 pb-2.5 pt-5 bg-gray-50 dark:bg-gray-700'],
21
+
22
+ // LABELS - positioning without transforms
23
+ ['input-label-standard', 'top-3 -z-10'],
24
+ ['input-label-filled', 'top-4 z-10 start-2.5'],
25
+
26
+ // Focus state transformations - explicitly defined
27
+ ['input-label-focus-color', 'peer-focus:text-blue-light peer-focus:dark:text-blue-lightest'],
28
+ ['input-label-focus-scale', 'peer-focus:scale-75'],
29
+ ['input-label-focus-translate-standard', 'peer-focus:-translate-y-6'],
30
+ ['input-label-focus-translate-filled', 'peer-focus:-translate-y-4'],
31
+
32
+ // Placeholder state transformations
33
+ ['input-label-placeholder', 'peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0'],
34
+
35
+ // Not-placeholder-shown state transformations
36
+ ['input-label-filled-standard', 'peer-not-placeholder-shown:scale-75 peer-not-placeholder-shown:-translate-y-6'],
37
+ ['input-label-filled-filled', 'peer-not-placeholder-shown:scale-75 peer-not-placeholder-shown:-translate-y-4'],
38
+
39
+ // Standard input states - complete shortcuts for specific states
40
+ ['input-label-standard-state', 'input-label-focus-color input-label-focus-scale input-label-focus-translate-standard input-label-placeholder input-label-filled-standard'],
41
+ ['input-label-filled-state', 'input-label-focus-color input-label-focus-scale input-label-focus-translate-filled input-label-placeholder input-label-filled-filled'],
42
+
43
+ // Input types
44
+ ['input-textarea', 'resize-none'],
45
+
46
+ // Input sizes
47
+ ['input-sm', 'text-sm'],
48
+ ['input-md', 'text-base'],
49
+ ['input-lg', 'text-lg '],
50
+
51
+ // Label sizes
52
+ ['input-label-sm', 'text-sm'],
53
+ ['input-label-md', 'text-sm'],
54
+ ['input-label-lg', 'text-sm'],
55
+
56
+ // Status classes
57
+ ['input-error', 'border-red-500 focus:border-red-500 dark:border-red-400 dark:focus:border-red-400'],
58
+ ['input-label-error', 'text-red-500 dark:text-red-400'],
59
+ ['input-error-message', 'mt-1 text-xs text-red-500 dark:text-red-400'],
60
+
61
+ ['input-success', 'border-green-500 focus:border-green-500 dark:border-green-400 dark:focus:border-green-400'],
62
+ ['input-label-success', 'text-green-500 dark:text-green-400'],
63
+ ['input-success-message', 'mt-1 text-xs text-green-500 dark:text-green-400'],
64
+ ];
@@ -1,86 +0,0 @@
1
- ---
2
- // Input.astro
3
- interface Props {
4
- id: string;
5
- name?: string;
6
- label: string;
7
- variant?: 'filled' | 'standard';
8
- type?: HTMLInputElement['type'] | 'textarea'; // support textarea
9
- value?: string;
10
- required?: boolean;
11
- rows?: number; // rows for textarea
12
- placeholder?: string;
13
- class?: string; // additional classes
14
- }
15
-
16
- const {
17
- id,
18
- name,
19
- label,
20
- variant = 'standard',
21
- type = 'text',
22
- value = '',
23
- required = false,
24
- rows = 3,
25
- placeholder = " ", //space for "floating label")
26
- class: additionalClasses = "",
27
- ...restProps
28
- } = Astro.props;
29
-
30
- // Common classes for both variants
31
- const baseInputClasses = "block w-full text-4.5 text-blue-medium border-0 border-b-1 border-neutral-light appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-lightest focus:outline-none focus:ring-0 focus:border-blue-medium peer";
32
-
33
- const baseLabelClasses = "absolute text-sm text-slate-medium dark:text-neutral-default transform scale-75 origin-[0] peer-focus:text-blue-medium peer-focus:dark:text-blue-lightest peer-placeholder-shown:scale-100 peer-focus:scale-75 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto transition-all duration-300 ease-in-out";
34
-
35
- // Variant specific classes
36
- const variantClasses = {
37
- filled: {
38
- wrapper: "relative",
39
- input: `${baseInputClasses} rounded-t-lg px-2.5 pb-2.5 pt-5 bg-gray-50 dark:bg-gray-700 ${additionalClasses}`,
40
- label: `${baseLabelClasses} -translate-y-4 top-4 z-10 start-2.5 peer-placeholder-shown:translate-y-0 peer-focus:-translate-y-4`
41
- },
42
- standard: {
43
- wrapper: "relative z-0",
44
- input: `${baseInputClasses} py-2.5 px-0 bg-transparent ${additionalClasses}`,
45
- label: `${baseLabelClasses} -translate-y-6 top-3 -z-10 peer-focus:start-0 peer-placeholder-shown:translate-y-0 peer-focus:-translate-y-6`
46
- }
47
- };
48
-
49
- const classes = variantClasses[variant];
50
-
51
- // Keep border-b-1 for textarea but add resize-none
52
- const textareaClasses = type === 'textarea'
53
- ? `${classes.input} resize-none`
54
- : classes.input;
55
- ---
56
-
57
- <div class={classes.wrapper}>
58
- {type === 'textarea' ? (
59
- <textarea
60
- name={name}
61
- id={id}
62
- rows={rows}
63
- required={required}
64
- class={textareaClasses}
65
- placeholder={placeholder}
66
- {...restProps}
67
- >{value}</textarea>
68
- ) : (
69
- <input
70
- type={type}
71
- name={name}
72
- id={id}
73
- value={value}
74
- required={required}
75
- class={classes.input}
76
- placeholder={placeholder}
77
- {...restProps}
78
- />
79
- )}
80
- <label
81
- for={id}
82
- class={classes.label}
83
- >
84
- {label}
85
- </label>
86
- </div>