@imaginario27/air-ui-ds 1.0.2 → 1.0.4

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.
@@ -17,7 +17,7 @@
17
17
  >
18
18
  {{ groupKey }}
19
19
  </component>
20
- <Accordion :items="groupedAccordeons[groupKey]" />
20
+ <Accordion :items="groupedAccordeons[groupKey] ?? []" />
21
21
  </div>
22
22
  </div>
23
23
 
@@ -78,7 +78,7 @@
78
78
  >
79
79
  <SVGImage
80
80
  :src="svgIcon"
81
- :color="!useSVGIconColor && svgIconColorClass"
81
+ :color="resolvedSvgIconColor"
82
82
  />
83
83
  </span>
84
84
  </template>
@@ -376,7 +376,15 @@ const gapClass = computed(() => {
376
376
 
377
377
  // Computed functions
378
378
  const resolvedSvgIconColor = computed(() => {
379
- return props.useSVGIconColor ? undefined : svgIconColorClass
379
+ if (props.useSVGIconColor) return undefined
380
+
381
+ const val = svgIconColorClass.value
382
+
383
+ if (Array.isArray(val)) {
384
+ return val.filter(Boolean).join(' ')
385
+ }
386
+
387
+ return val
380
388
  })
381
389
 
382
390
  // Props for the dynamic component
@@ -264,7 +264,15 @@ const svgIconColorClass = computed(() => {
264
264
 
265
265
  // Computed functions
266
266
  const resolvedSvgIconColor = computed(() => {
267
- return props.useSVGIconColor ? undefined : svgIconColorClass
267
+ if (props.useSVGIconColor) return undefined
268
+
269
+ const val = svgIconColorClass.value
270
+
271
+ if (Array.isArray(val)) {
272
+ return val.filter(Boolean).join(' ')
273
+ }
274
+
275
+ return val
268
276
  })
269
277
 
270
278
  // Props for the dynamic component
@@ -37,7 +37,7 @@
37
37
  v-if="hasCopyToClipboardButton"
38
38
  icon="mdiContentCopy"
39
39
  :size="ButtonSize.XS"
40
- @click="copyToClipboard(text.toString(), copyToClipboardMessage)"
40
+ @click="handleCopyToClipboard"
41
41
  />
42
42
  </div>
43
43
  </template>
@@ -90,10 +90,28 @@ const props = defineProps({
90
90
  type: String as PropType<string>,
91
91
  default: 'Copied to clipboard'
92
92
  },
93
+ copyToClipboardErrorMessage: {
94
+ type: String as PropType<string>,
95
+ default: 'Failed to copy to clipboard'
96
+ },
93
97
  })
94
98
 
95
99
  // Computed
96
100
  const isEmpty = computed(() => {
97
101
  return props.text === '' || props.text === null || props.text === undefined || props.text === 0
98
102
  })
103
+
104
+ // Toast
105
+ const { $toast } = useNuxtApp()
106
+
107
+ // Methods
108
+ const handleCopyToClipboard = async () => {
109
+ const success = await copyToClipboard(props.text?.toString() ?? '')
110
+
111
+ if (success) {
112
+ $toast.success(props.copyToClipboardMessage, { toastId: 'clipboard-success' })
113
+ } else {
114
+ $toast.error(props.copyToClipboardErrorMessage, { toastId: 'clipboard-error' })
115
+ }
116
+ }
99
117
  </script>
@@ -74,36 +74,6 @@
74
74
  {{ acceptedFileTypes }} {{ upToText }} {{ maxFileSize }}MB
75
75
  </p>
76
76
  </template>
77
- <!-- <template v-if="!isImageFile" #preview>
78
- <div class="w-full flex flex-col gap-2">
79
- <div
80
- v-for="(file, index) in selectedFiles"
81
- :key="index"
82
- class="flex flex-row gap-3 p-3 border border-border-default rounded-md bg-background-neutral w-full"
83
- >
84
- <div class="flex items-start justify-center pt-1">
85
- <MdiIcon
86
- icon="mdiFileDocumentOutline"
87
- size="24"
88
- class="text-icon-default min-w-[24px]"
89
- />
90
- </div>
91
-
92
- <div class="flex flex-col flex-1 overflow-hidden">
93
- <p class="text-sm font-medium break-words whitespace-normal leading-snug mt-1.5 select-none">
94
- {{ file.name || (file as any)?.file?.name || 'Unnamed file' }}
95
- </p>
96
- </div>
97
-
98
- <ActionIconButton
99
- icon="mdiClose"
100
- :styleType="ButtonStyleType.DELETE_FILLED"
101
- :size="ButtonSize.SM"
102
- @click="removeFile(index)"
103
- />
104
- </div>
105
- </div>
106
- </template> -->
107
77
  </Vue3Dropzone>
108
78
 
109
79
  <!-- Help Text -->
@@ -123,7 +93,7 @@
123
93
 
124
94
  <script setup lang="ts">
125
95
  // Imports
126
- //@ts-ignore
96
+ //@ts-expect-error: vue3-dropzone has no types; using JS import
127
97
  import Vue3Dropzone from '@jaxtheprime/vue3-dropzone'
128
98
  import '@jaxtheprime/vue3-dropzone/dist/style.css'
129
99
 
@@ -214,23 +184,6 @@ const { $toast } = useNuxtApp()
214
184
  // Computed
215
185
  const hasError = computed(() => props.error !== '')
216
186
 
217
- // Used to check if the file is an image or not
218
- const isImageFile = computed(() => {
219
- const accepted = Array.isArray(props.accept) ? props.accept : [props.accept]
220
-
221
- const acceptsImages = accepted.some(type =>
222
- typeof type === 'string' && type.startsWith('image/')
223
- )
224
-
225
- if (!acceptsImages) return false
226
-
227
- return selectedFiles.value.length > 0 &&
228
- selectedFiles.value.every(file => {
229
- const realFile = (file as any)?.file || file
230
- return realFile?.type?.startsWith('image/')
231
- })
232
- })
233
-
234
187
  // Converts Array of strings into a string
235
188
  const normalizedAccept = computed(() => {
236
189
  return Array.isArray(props.accept) ? props.accept.join(',') : props.accept
@@ -240,9 +193,10 @@ const normalizedAccept = computed(() => {
240
193
  const acceptedFileTypes = computed(() => {
241
194
  return (Array.isArray(props.accept) ? props.accept : [props.accept])
242
195
  .map(ext => {
243
- // Extract file extension from MIME type
244
196
  const match = ext.match(/\/([a-zA-Z0-9]+)/)
245
- return match ? match[1].toUpperCase() : ext.toUpperCase()
197
+
198
+ // If matched, return the extracted file extension, otherwise use full type
199
+ return match?.[1]?.toUpperCase() || ext.toUpperCase()
246
200
  })
247
201
  .join(', ')
248
202
  })
@@ -114,7 +114,7 @@
114
114
  'whitespace-nowrap',
115
115
  ]"
116
116
  >
117
- {{ $t('slider.clickToEdit') }}
117
+ Click to edit
118
118
  </div>
119
119
  </div>
120
120
  </div>
@@ -283,7 +283,7 @@
283
283
  'whitespace-nowrap',
284
284
  ]"
285
285
  >
286
- {{ $t('slider.clickToEdit') || 'Click para editar' }}
286
+ Click to edit
287
287
  </div>
288
288
  </div>
289
289
  </div>
@@ -514,15 +514,16 @@ const getThumbPosition = (value: number) => {
514
514
  const trackStyle = computed(() => {
515
515
  if (props.type === SliderType.RANGE) {
516
516
  const [start, end] = thumbValues.value
517
- const startPercent = percentage(start)
518
- const endPercent = percentage(end)
517
+
518
+ const startPercent = percentage(start ?? props.min)
519
+ const endPercent = percentage(end ?? props.max)
519
520
 
520
521
  return {
521
522
  left: `${startPercent}%`,
522
523
  width: `${endPercent - startPercent}%`,
523
524
  }
524
525
  } else {
525
- const percent = percentage(thumbValues.value[0])
526
+ const percent = percentage(thumbValues.value[0] ?? props.min)
526
527
  return {
527
528
  left: '0%',
528
529
  width: `${percent}%`,
@@ -530,6 +531,7 @@ const trackStyle = computed(() => {
530
531
  }
531
532
  })
532
533
 
534
+
533
535
  const startDrag = (index: number) => {
534
536
  if (props.disabled) return
535
537
 
@@ -551,23 +553,20 @@ const onDrag = (event: MouseEvent) => {
551
553
  const step = stepSize.value
552
554
  const stepsFromMin = Math.round((clamped - props.min) / step)
553
555
  clamped = props.min + stepsFromMin * step
554
- clamped = Math.round(clamped)
555
- emit('update:modelValue', clamped)
556
+ emit('update:modelValue', Math.round(clamped))
557
+ return
558
+ }
559
+
560
+ const range = [...thumbValues.value] as [number, number]
561
+ const roundedValue = Math.round(clamped)
562
+
563
+ if (draggingIndex.value === 0) {
564
+ range[0] = Math.min(roundedValue, range[1] - 1)
556
565
  } else {
557
- // MODIFICADO: Validación para rangos durante el drag
558
- const range = [...(thumbValues.value as [number, number])]
559
- const roundedValue = Math.round(clamped)
560
-
561
- if (draggingIndex.value === 0) {
562
- // Arrastrando el thumb mínimo
563
- range[0] = Math.min(roundedValue, range[1] - 1) // Asegurar que sea menor que el máximo
564
- } else if (draggingIndex.value === 1) {
565
- // Arrastrando el thumb máximo
566
- range[1] = Math.max(roundedValue, range[0] + 1) // Asegurar que sea mayor que el mínimo
567
- }
568
-
569
- emit('update:modelValue', range)
566
+ range[1] = Math.max(roundedValue, range[0] + 1)
570
567
  }
568
+
569
+ emit('update:modelValue', range)
571
570
  }
572
571
 
573
572
  const stopDrag = () => {
@@ -657,25 +656,34 @@ const stopEditing = () => {
657
656
 
658
657
  // AÑADIDO: Validación específica para rangos
659
658
  if (props.type === SliderType.RANGE) {
660
- const currentRange = [...(props.modelValue as [number, number])]
661
-
659
+ const mv = props.modelValue
660
+
661
+ if (!Array.isArray(mv)) return
662
+
663
+ const currentRange = [...mv] as [number, number]
664
+
662
665
  if (editingIndex.value === 0) {
663
- // Editando valor mínimo - debe ser menor que el máximo
664
666
  if (adjustedValue >= currentRange[1]) {
665
667
  showEditingError('El valor mínimo debe ser menor que el máximo')
666
668
  return
667
669
  }
668
- } else if (editingIndex.value === 1) {
669
- // Editando valor máximo - debe ser mayor que el mínimo
670
+ }
671
+ else if (editingIndex.value === 1) {
670
672
  if (adjustedValue <= currentRange[0]) {
671
673
  showEditingError('El valor máximo debe ser mayor que el mínimo')
672
674
  return
673
675
  }
676
+ }
677
+ else {
678
+ // If editingIndex is null or invalid, stop.
679
+ return
674
680
  }
675
-
676
- // Si llegamos aquí, el valor es válido
681
+
682
+ // Now it's safe:
677
683
  currentRange[editingIndex.value] = adjustedValue
678
684
  emit('update:modelValue', currentRange)
685
+
686
+ return
679
687
  } else {
680
688
  // Para slider simple, solo actualizar el valor
681
689
  emit('update:modelValue', adjustedValue)
@@ -50,7 +50,7 @@
50
50
  >
51
51
  <MdiIcon
52
52
  v-if="item.icon"
53
- :icon="item.icon"
53
+ :icon="item.icon as any"
54
54
  size="20"
55
55
  preserveAspectRatio="xMidYMid meet"
56
56
  />
@@ -62,8 +62,7 @@
62
62
  </template>
63
63
 
64
64
  <script setup lang="ts">
65
- import { computed } from 'vue'
66
- import { NuxtLink } from '#components'
65
+ // Props
67
66
 
68
67
  type NavItem = {
69
68
  path: string
@@ -7,7 +7,7 @@
7
7
  :modelValue
8
8
  :options="rowsOptions"
9
9
  class="w-full min-w-[80px]"
10
- :dropdownPosition="DropdownPosition.TOP"
10
+ :dropdownPosition="Position.TOP"
11
11
  @update:modelValue="(value) => emit('update:modelValue', value)"
12
12
  />
13
13
  </div>
@@ -9,25 +9,25 @@
9
9
  >
10
10
  <SecurePasswordCondition
11
11
  v-if="enabledConditions.includes('length')"
12
- :label="$t('Password should be at least 12 characters long.')"
12
+ :label="'Password should be at least 12 characters long.'"
13
13
  :isValid="conditions.isLongEnough"
14
14
  />
15
15
 
16
16
  <SecurePasswordCondition
17
17
  v-if="enabledConditions.includes('combination')"
18
- :label="$t('Use a mix of uppercase and lowercase letters.')"
18
+ :label="'Use a mix of uppercase and lowercase letters.'"
19
19
  :isValid="conditions.hasMixedCase"
20
20
  />
21
21
 
22
22
  <SecurePasswordCondition
23
23
  v-if="enabledConditions.includes('specialCharacters')"
24
- :label="$t('Include numbers and special characters for extra security.')"
24
+ :label="'Include numbers and special characters for extra security.'"
25
25
  :isValid="conditions.hasNumbersAndSpecialChars"
26
26
  />
27
27
 
28
28
  <SecurePasswordCondition
29
29
  v-if="enabledConditions.includes('passwordMatch') && repeatPassword !== undefined"
30
- :label="$t('Both passwords must match.')"
30
+ :label="'Both passwords must match.'"
31
31
  :isValid="password === repeatPassword && password !== ''"
32
32
  />
33
33
  </div>
@@ -25,7 +25,7 @@
25
25
  :class="[
26
26
  'min-w-[20px] sm:min-w-[40px]',
27
27
  'border-2',
28
- stepItems[index].status === StepStatus.COMPLETED && '!border-border-primary-brand-active',
28
+ item.status === StepStatus.COMPLETED && '!border-border-primary-brand-active',
29
29
  dividerClass
30
30
  ]"
31
31
  />
@@ -62,11 +62,13 @@ const isHovered = ref(false)
62
62
 
63
63
  // Computed classes
64
64
  const titleClass = computed(() => {
65
- const variant = {
65
+ const variant: Record<StepStatus, string> = {
66
66
  [StepStatus.INACTIVE]: 'text-text-neutral-inactive group-hover:text-text-primary-brand-hover',
67
67
  [StepStatus.CURRENT]: 'text-text-primary-brand-active group-hover:text-text-primary-brand-hover',
68
68
  [StepStatus.COMPLETED]: 'text-text-default group-hover:text-text-primary-brand-hover',
69
+ [StepStatus.NONE]: 'text-text-neutral-inactive',
69
70
  }
70
- return variant[props.status as StepStatus] || 'text-text-neutral-inactive'
71
+
72
+ return variant[props.status ?? StepStatus.INACTIVE]
71
73
  })
72
74
  </script>
@@ -28,7 +28,8 @@ export const useTableOfContents = () => {
28
28
 
29
29
  // First heading is active by default when the page loads
30
30
  if (headings.length > 0) {
31
- activeId.value = headings[0].id
31
+ const firstHeading = headings[0] as HTMLElement
32
+ activeId.value = firstHeading.id ?? null
32
33
  }
33
34
  }
34
35
 
@@ -1,6 +1,11 @@
1
1
  export interface MenuItem {
2
- text: string;
3
- to: string;
2
+ text: string
3
+ to: string
4
+ }
5
+
6
+ export interface FooterMenuItem {
7
+ text: string
8
+ to: string
4
9
  }
5
10
 
6
11
  export interface SidebarMenuItem {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",
@@ -18,7 +18,8 @@
18
18
  "preview": "nuxt preview",
19
19
  "postinstall": "nuxt prepare",
20
20
  "test": "vitest",
21
- "generate-theme": "ts-node scripts/generate-theme.ts"
21
+ "generate-theme": "ts-node scripts/generate-theme.ts",
22
+ "typecheck": "vue-tsc --noEmit -p tsconfig.typecheck.json"
22
23
  },
23
24
  "dependencies": {
24
25
  "@jaxtheprime/vue3-dropzone": "^3.4.0",
@@ -0,0 +1,104 @@
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ unlinkSync,
7
+ } from "fs"
8
+ import { resolve, dirname } from "path"
9
+
10
+ const inputPath = resolve("assets/css/theme/ui-theme.css")
11
+ const outputPath = resolve("assets/css/main.css")
12
+
13
+ ensureOutputDir(outputPath)
14
+ deleteIfExists(outputPath)
15
+
16
+ const content = readFileSync(inputPath, "utf-8")
17
+ const { colorVars, otherVars } = extractThemeVars(content)
18
+
19
+ const finalOutput = generateThemeFile(colorVars, otherVars)
20
+ writeFileSync(outputPath, finalOutput, "utf-8")
21
+
22
+ console.log("✅ Tailwind theme file generated in assets/css/")
23
+
24
+ function ensureOutputDir(filePath: string) {
25
+ const dir = dirname(filePath)
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true })
28
+ }
29
+ }
30
+
31
+ function deleteIfExists(filePath: string) {
32
+ if (existsSync(filePath)) {
33
+ unlinkSync(filePath)
34
+ }
35
+ }
36
+
37
+ function extractThemeVars(content: string) {
38
+ const colorVars: string[] = []
39
+ const otherVars: string[] = []
40
+
41
+ const lines = content.split("\n")
42
+ let inRoot = false
43
+ let inDark = false
44
+
45
+ for (const rawLine of lines) {
46
+ const line = rawLine.trim()
47
+
48
+ if (line.startsWith(":root {")) {
49
+ inRoot = true
50
+ continue
51
+ }
52
+ if (line.startsWith(".dark {")) {
53
+ inDark = true
54
+ continue
55
+ }
56
+ if (line.startsWith("}")) {
57
+ inRoot = false
58
+ inDark = false
59
+ continue
60
+ }
61
+
62
+ if (!inRoot || inDark) continue
63
+
64
+ const key = extractVarKey(line)
65
+ if (!key || key.startsWith("--ds-")) continue
66
+
67
+ const declaration = ` ${key}: var(${key});`
68
+ if (key.startsWith("--color-")) {
69
+ colorVars.push(declaration)
70
+ } else {
71
+ otherVars.push(declaration)
72
+ }
73
+ }
74
+
75
+ return { colorVars, otherVars }
76
+ }
77
+
78
+ function extractVarKey(line: string): string | null {
79
+ const match = line.match(/^--[\w-]+:\s*[^;]+;/)
80
+ if (!match) return null
81
+ const [key] = line.split(":").map(s => s.trim().replace(/;$/, ""))
82
+ return key || null
83
+ }
84
+
85
+ function generateThemeFile(colorVars: string[], otherVars: string[]): string {
86
+ return [
87
+ `@import "tailwindcss";`,
88
+ `@source "../../node_modules/@imaginario27/air-ui-ds";`,
89
+ `@source "../../node_modules/@imaginario27/air-ui-utils";`,
90
+ ``,
91
+ `@import "./theme/primitives.css";`,
92
+ `@import "./theme/colors.css";`,
93
+ `@import "./theme/ui-theme.css";`,
94
+ ``,
95
+ `@theme {`,
96
+ ` /* Disables Tailwind default colors */`,
97
+ ` --color-*: initial;`,
98
+ ``,
99
+ ...colorVars,
100
+ ``,
101
+ ...otherVars,
102
+ `}`,
103
+ ].join("\n")
104
+ }
package/tsconfig.json CHANGED
@@ -3,5 +3,5 @@
3
3
  "extends": "./.nuxt/tsconfig.json",
4
4
  "compilerOptions": {
5
5
  "types": ["vitest/globals"],
6
- }
6
+ },
7
7
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "skipLibCheck": true
5
+ },
6
+ "exclude": [
7
+ "node_modules",
8
+ "../../node_modules",
9
+ "tests",
10
+ "**/*.spec.ts",
11
+ "**/*.test.ts",
12
+ ]
13
+ }