@m3ui-vue/m3ui-vue 0.1.0

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12
7
+ sm?: 1 | 2 | 3 | 4 | 5 | 6 | 12
8
+ md?: 1 | 2 | 3 | 4 | 5 | 6 | 12
9
+ lg?: 1 | 2 | 3 | 4 | 5 | 6 | 12
10
+ xl?: 1 | 2 | 3 | 4 | 5 | 6 | 12
11
+ gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
12
+ alignItems?: 'start' | 'center' | 'end' | 'stretch'
13
+ }>(),
14
+ { cols: 1, gap: 'md', alignItems: 'stretch' },
15
+ )
16
+
17
+ const colClasses: Record<number, string> = {
18
+ 1: 'grid-cols-1',
19
+ 2: 'grid-cols-2',
20
+ 3: 'grid-cols-3',
21
+ 4: 'grid-cols-4',
22
+ 5: 'grid-cols-5',
23
+ 6: 'grid-cols-6',
24
+ 12: 'grid-cols-12',
25
+ }
26
+
27
+ const smColClasses: Record<number, string> = {
28
+ 1: 'sm:grid-cols-1',
29
+ 2: 'sm:grid-cols-2',
30
+ 3: 'sm:grid-cols-3',
31
+ 4: 'sm:grid-cols-4',
32
+ 5: 'sm:grid-cols-5',
33
+ 6: 'sm:grid-cols-6',
34
+ 12: 'sm:grid-cols-12',
35
+ }
36
+
37
+ const mdColClasses: Record<number, string> = {
38
+ 1: 'md:grid-cols-1',
39
+ 2: 'md:grid-cols-2',
40
+ 3: 'md:grid-cols-3',
41
+ 4: 'md:grid-cols-4',
42
+ 5: 'md:grid-cols-5',
43
+ 6: 'md:grid-cols-6',
44
+ 12: 'md:grid-cols-12',
45
+ }
46
+
47
+ const lgColClasses: Record<number, string> = {
48
+ 1: 'lg:grid-cols-1',
49
+ 2: 'lg:grid-cols-2',
50
+ 3: 'lg:grid-cols-3',
51
+ 4: 'lg:grid-cols-4',
52
+ 5: 'lg:grid-cols-5',
53
+ 6: 'lg:grid-cols-6',
54
+ 12: 'lg:grid-cols-12',
55
+ }
56
+
57
+ const xlColClasses: Record<number, string> = {
58
+ 1: 'xl:grid-cols-1',
59
+ 2: 'xl:grid-cols-2',
60
+ 3: 'xl:grid-cols-3',
61
+ 4: 'xl:grid-cols-4',
62
+ 5: 'xl:grid-cols-5',
63
+ 6: 'xl:grid-cols-6',
64
+ 12: 'xl:grid-cols-12',
65
+ }
66
+
67
+ const gapClasses: Record<string, string> = {
68
+ none: 'gap-0',
69
+ xs: 'gap-1',
70
+ sm: 'gap-2',
71
+ md: 'gap-4',
72
+ lg: 'gap-6',
73
+ xl: 'gap-8',
74
+ }
75
+
76
+ const alignClasses: Record<string, string> = {
77
+ start: 'items-start',
78
+ center: 'items-center',
79
+ end: 'items-end',
80
+ stretch: 'items-stretch',
81
+ }
82
+
83
+ const classes = computed(() => [
84
+ 'grid',
85
+ colClasses[props.cols],
86
+ props.sm && smColClasses[props.sm],
87
+ props.md && mdColClasses[props.md],
88
+ props.lg && lgColClasses[props.lg],
89
+ props.xl && xlColClasses[props.xl],
90
+ gapClasses[props.gap],
91
+ alignClasses[props.alignItems],
92
+ ])
93
+ </script>
94
+
95
+ <template>
96
+ <div :class="classes">
97
+ <slot />
98
+ </div>
99
+ </template>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onBeforeUnmount } from 'vue'
3
+
4
+ export interface HotkeyBinding {
5
+ keys: string
6
+ label: string
7
+ handler: () => void
8
+ group?: string
9
+ disabled?: boolean
10
+ }
11
+
12
+ const props = withDefaults(
13
+ defineProps<{
14
+ bindings: HotkeyBinding[]
15
+ showOverlay?: boolean
16
+ }>(),
17
+ { showOverlay: false },
18
+ )
19
+
20
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent)
21
+
22
+ function formatKey(raw: string): string {
23
+ return raw
24
+ .replace(/mod/gi, isMac ? '⌘' : 'Ctrl')
25
+ .replace(/ctrl/gi, isMac ? '⌃' : 'Ctrl')
26
+ .replace(/alt/gi, isMac ? '⌥' : 'Alt')
27
+ .replace(/shift/gi, isMac ? '⇧' : 'Shift')
28
+ .replace(/meta/gi, '⌘')
29
+ .replace(/enter/gi, '↵')
30
+ .replace(/escape/gi, 'Esc')
31
+ .replace(/backspace/gi, '⌫')
32
+ .replace(/delete/gi, '⌦')
33
+ .replace(/arrowup/gi, '↑')
34
+ .replace(/arrowdown/gi, '↓')
35
+ .replace(/arrowleft/gi, '←')
36
+ .replace(/arrowright/gi, '→')
37
+ }
38
+
39
+ function parseCombo(keys: string) {
40
+ return keys.split('+').map(k => k.trim().toLowerCase())
41
+ }
42
+
43
+ function matchesEvent(combo: string[], e: KeyboardEvent): boolean {
44
+ const modifiers = { ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey, mod: isMac ? e.metaKey : e.ctrlKey }
45
+ const key = e.key.toLowerCase()
46
+
47
+ for (const part of combo) {
48
+ if (part in modifiers) {
49
+ if (!modifiers[part as keyof typeof modifiers]) return false
50
+ } else if (key !== part) {
51
+ return false
52
+ }
53
+ }
54
+
55
+ for (const [mod, active] of Object.entries(modifiers)) {
56
+ if (active && !combo.includes(mod)) return false
57
+ }
58
+
59
+ return true
60
+ }
61
+
62
+ function onKeydown(e: KeyboardEvent) {
63
+ const tag = (e.target as HTMLElement).tagName
64
+ const editable = tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable
65
+
66
+ for (const binding of props.bindings) {
67
+ if (binding.disabled) continue
68
+ const combo = parseCombo(binding.keys)
69
+ const hasModifier = combo.some(k => ['ctrl', 'alt', 'shift', 'meta', 'mod'].includes(k))
70
+ if (!hasModifier && editable) continue
71
+ if (matchesEvent(combo, e)) {
72
+ e.preventDefault()
73
+ binding.handler()
74
+ return
75
+ }
76
+ }
77
+ }
78
+
79
+ onMounted(() => document.addEventListener('keydown', onKeydown))
80
+ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
81
+
82
+ const grouped = () => {
83
+ const map = new Map<string, HotkeyBinding[]>()
84
+ for (const b of props.bindings) {
85
+ const g = b.group ?? ''
86
+ if (!map.has(g)) map.set(g, [])
87
+ map.get(g)!.push(b)
88
+ }
89
+ return map
90
+ }
91
+ </script>
92
+
93
+ <template>
94
+ <div v-if="showOverlay" class="flex flex-col gap-4">
95
+ <template v-for="[group, bindings] in grouped()" :key="group">
96
+ <div>
97
+ <p v-if="group" class="mb-2 text-label-small font-medium tracking-wide text-on-surface-variant uppercase">
98
+ {{ group }}
99
+ </p>
100
+ <div class="flex flex-col gap-1">
101
+ <div
102
+ v-for="b in bindings"
103
+ :key="b.keys"
104
+ class="flex items-center justify-between rounded-lg px-3 py-2 transition-colors hover:bg-on-surface/4"
105
+ :class="b.disabled && 'opacity-38'"
106
+ >
107
+ <span class="text-body-medium text-on-surface">{{ b.label }}</span>
108
+ <div class="flex items-center gap-0.5">
109
+ <kbd
110
+ v-for="(k, ki) in b.keys.split('+')"
111
+ :key="ki"
112
+ class="inline-flex min-w-[24px] items-center justify-center rounded bg-surface-container px-1.5 py-0.5 text-center text-label-small font-medium text-on-surface-variant"
113
+ >
114
+ {{ formatKey(k.trim()) }}
115
+ </kbd>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </template>
121
+ </div>
122
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{ name: string; size?: number }>(), { size: 24 })
3
+ </script>
4
+
5
+ <template>
6
+ <span class="material-symbols-outlined leading-none" :style="{ fontSize: `${size}px` }">{{
7
+ name
8
+ }}</span>
9
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ icon: string
8
+ label: string
9
+ variant?: 'standard' | 'filled' | 'tonal' | 'outlined'
10
+ disabled?: boolean
11
+ size?: number
12
+ }>(),
13
+ {
14
+ variant: 'standard',
15
+ disabled: false,
16
+ size: 40,
17
+ },
18
+ )
19
+
20
+ const base =
21
+ 'inline-flex shrink-0 items-center justify-center rounded-full transition-colors duration-150 cursor-pointer ' +
22
+ 'disabled:cursor-not-allowed disabled:opacity-[0.38]'
23
+
24
+ const variantClasses = computed(() => {
25
+ switch (props.variant) {
26
+ case 'filled':
27
+ return 'bg-primary text-on-primary hover:shadow-elevation-1'
28
+ case 'tonal':
29
+ return 'bg-secondary-container text-on-secondary-container hover:shadow-elevation-1'
30
+ case 'outlined':
31
+ return 'border border-outline text-on-surface-variant hover:bg-on-surface/8'
32
+ default:
33
+ return 'text-on-surface-variant hover:bg-on-surface/8 active:bg-on-surface/12'
34
+ }
35
+ })
36
+ </script>
37
+
38
+ <template>
39
+ <button
40
+ type="button"
41
+ :aria-label="label"
42
+ :title="label"
43
+ :disabled="disabled"
44
+ :class="[base, variantClasses]"
45
+ :style="{ width: `${size}px`, height: `${size}px` }"
46
+ >
47
+ <MIcon :name="icon" :size="Math.round(size * 0.55)" />
48
+ </button>
49
+ </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
3
+ import MSpinner from './MSpinner.vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ loading?: boolean
8
+ disabled?: boolean
9
+ threshold?: number
10
+ loadingText?: string
11
+ endText?: string
12
+ ended?: boolean
13
+ }>(),
14
+ {
15
+ loading: false,
16
+ disabled: false,
17
+ threshold: 100,
18
+ loadingText: 'Cargando...',
19
+ endText: 'No hay más elementos',
20
+ ended: false,
21
+ },
22
+ )
23
+
24
+ const emit = defineEmits<{ load: [] }>()
25
+
26
+ const sentinelRef = ref<HTMLElement | null>(null)
27
+ let observer: IntersectionObserver | null = null
28
+
29
+ function createObserver() {
30
+ if (observer) observer.disconnect()
31
+ if (props.disabled || props.ended) return
32
+
33
+ observer = new IntersectionObserver(
34
+ (entries) => {
35
+ const entry = entries[0]
36
+ if (entry?.isIntersecting && !props.loading && !props.ended && !props.disabled) {
37
+ emit('load')
38
+ }
39
+ },
40
+ { rootMargin: `0px 0px ${props.threshold}px 0px` },
41
+ )
42
+
43
+ if (sentinelRef.value) observer.observe(sentinelRef.value)
44
+ }
45
+
46
+ onMounted(createObserver)
47
+
48
+ watch(() => [props.disabled, props.ended], createObserver)
49
+
50
+ onBeforeUnmount(() => observer?.disconnect())
51
+ </script>
52
+
53
+ <template>
54
+ <div>
55
+ <slot />
56
+
57
+ <div ref="sentinelRef" class="flex items-center justify-center py-4">
58
+ <div v-if="loading" class="flex items-center gap-3">
59
+ <MSpinner :size="20" class="text-primary" />
60
+ <span class="text-body-medium text-on-surface-variant">{{ loadingText }}</span>
61
+ </div>
62
+ <p v-else-if="ended" class="text-body-small text-on-surface-variant">
63
+ {{ endText }}
64
+ </p>
65
+ <slot v-else name="idle" />
66
+ </div>
67
+ </div>
68
+ </template>
@@ -0,0 +1,118 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import MCodeEditor from './MCodeEditor.vue'
4
+ import MIcon from './MIcon.vue'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ modelValue: unknown
9
+ readonly?: boolean
10
+ minHeight?: string
11
+ maxHeight?: string
12
+ }>(),
13
+ {
14
+ readonly: false,
15
+ minHeight: '200px',
16
+ maxHeight: '600px',
17
+ },
18
+ )
19
+
20
+ const emit = defineEmits<{ 'update:modelValue': [unknown] }>()
21
+
22
+ const rawText = ref(JSON.stringify(props.modelValue, null, 2))
23
+ const parseError = ref<string | null>(null)
24
+
25
+ const isValid = computed(() => !parseError.value)
26
+
27
+ watch(() => props.modelValue, (val) => {
28
+ const incoming = JSON.stringify(val, null, 2)
29
+ if (incoming !== rawText.value) {
30
+ rawText.value = incoming
31
+ parseError.value = null
32
+ }
33
+ })
34
+
35
+ function onTextUpdate(text: string) {
36
+ rawText.value = text
37
+ try {
38
+ const parsed = JSON.parse(text)
39
+ parseError.value = null
40
+ emit('update:modelValue', parsed)
41
+ } catch (e) {
42
+ parseError.value = (e as Error).message
43
+ }
44
+ }
45
+
46
+ function formatJson() {
47
+ try {
48
+ const parsed = JSON.parse(rawText.value)
49
+ rawText.value = JSON.stringify(parsed, null, 2)
50
+ parseError.value = null
51
+ emit('update:modelValue', parsed)
52
+ } catch {
53
+ // keep error state
54
+ }
55
+ }
56
+
57
+ function minifyJson() {
58
+ try {
59
+ const parsed = JSON.parse(rawText.value)
60
+ rawText.value = JSON.stringify(parsed)
61
+ parseError.value = null
62
+ emit('update:modelValue', parsed)
63
+ } catch {
64
+ // keep error state
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <template>
70
+ <div class="flex flex-col gap-0">
71
+ <MCodeEditor
72
+ :model-value="rawText"
73
+ language="json"
74
+ :readonly="readonly"
75
+ :min-height="minHeight"
76
+ :max-height="maxHeight"
77
+ @update:model-value="onTextUpdate"
78
+ >
79
+ <template #actions>
80
+ <div class="flex items-center gap-2">
81
+ <!-- Validation badge -->
82
+ <span
83
+ class="flex items-center gap-1 rounded-full px-2.5 py-0.5 text-label-small"
84
+ :class="isValid ? 'bg-success-container text-on-success-container' : 'bg-error-container text-on-error-container'"
85
+ >
86
+ <MIcon :name="isValid ? 'check_circle' : 'error'" :size="14" />
87
+ {{ isValid ? 'Válido' : 'Inválido' }}
88
+ </span>
89
+
90
+ <button
91
+ v-if="!readonly"
92
+ type="button"
93
+ title="Formatear"
94
+ class="flex h-7 cursor-pointer items-center gap-1 rounded px-2 text-label-medium text-on-surface-variant transition-colors hover:bg-on-surface/8"
95
+ @click="formatJson"
96
+ >
97
+ <MIcon name="format_indent_increase" :size="16" />
98
+ Formatear
99
+ </button>
100
+ <button
101
+ v-if="!readonly"
102
+ type="button"
103
+ title="Minificar"
104
+ class="flex h-7 cursor-pointer items-center gap-1 rounded px-2 text-label-medium text-on-surface-variant transition-colors hover:bg-on-surface/8"
105
+ @click="minifyJson"
106
+ >
107
+ <MIcon name="compress" :size="16" />
108
+ Minificar
109
+ </button>
110
+ </div>
111
+ </template>
112
+ </MCodeEditor>
113
+
114
+ <p v-if="parseError" class="px-3 py-1.5 text-body-small text-error">
115
+ {{ parseError }}
116
+ </p>
117
+ </div>
118
+ </template>
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import MIcon from './MIcon.vue'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ data: unknown
8
+ rootName?: string
9
+ expandDepth?: number
10
+ /** @internal — used by recursive instances */
11
+ _depth?: number
12
+ }>(),
13
+ { rootName: 'root', expandDepth: 2, _depth: 0 },
14
+ )
15
+
16
+ const expanded = ref(props._depth < props.expandDepth)
17
+
18
+ const dataType = computed(() => {
19
+ if (props.data === null) return 'null'
20
+ if (Array.isArray(props.data)) return 'array'
21
+ return typeof props.data
22
+ })
23
+
24
+ const isExpandable = computed(() => dataType.value === 'object' || dataType.value === 'array')
25
+
26
+ const entries = computed(() => {
27
+ if (dataType.value === 'array') {
28
+ return (props.data as unknown[]).map((v, i) => ({ key: String(i), value: v }))
29
+ }
30
+ if (dataType.value === 'object' && props.data) {
31
+ return Object.entries(props.data as Record<string, unknown>).map(([k, v]) => ({ key: k, value: v }))
32
+ }
33
+ return []
34
+ })
35
+
36
+ const childCount = computed(() => entries.value.length)
37
+
38
+ const bracketOpen = computed(() => (dataType.value === 'array' ? '[' : '{'))
39
+ const bracketClose = computed(() => (dataType.value === 'array' ? ']' : '}'))
40
+
41
+ function valueClass(val: unknown) {
42
+ if (val === null) return 'text-on-surface-variant italic'
43
+ switch (typeof val) {
44
+ case 'string': return 'text-success'
45
+ case 'number': return 'text-primary'
46
+ case 'boolean': return 'text-tertiary'
47
+ default: return 'text-on-surface'
48
+ }
49
+ }
50
+
51
+ function formatValue(val: unknown) {
52
+ if (typeof val === 'string') return `"${val}"`
53
+ if (val === null) return 'null'
54
+ if (val === undefined) return 'undefined'
55
+ return String(val)
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <div class="font-mono text-body-small leading-relaxed" :class="{ 'rounded-lg border border-outline-variant bg-surface-container-lowest p-3': _depth === 0 }">
61
+ <!-- Expandable node -->
62
+ <template v-if="isExpandable">
63
+ <button
64
+ type="button"
65
+ class="group inline-flex cursor-pointer items-center gap-0.5 rounded px-0.5 hover:bg-on-surface/[0.06]"
66
+ @click="expanded = !expanded"
67
+ >
68
+ <MIcon
69
+ :name="expanded ? 'expand_more' : 'chevron_right'"
70
+ :size="16"
71
+ class="text-on-surface-variant transition-transform duration-100"
72
+ />
73
+ <span v-if="_depth === 0 || rootName" class="text-tertiary">{{ _depth === 0 ? rootName : '' }}</span>
74
+ <span class="text-on-surface-variant">{{ bracketOpen }}</span>
75
+ <span v-if="!expanded" class="text-on-surface-variant/60">
76
+ {{ childCount }} {{ dataType === 'array' ? 'elementos' : 'campos' }}
77
+ </span>
78
+ <span v-if="!expanded" class="text-on-surface-variant">{{ bracketClose }}</span>
79
+ </button>
80
+
81
+ <div v-if="expanded" class="ml-5 border-l border-outline-variant/40 pl-2">
82
+ <div v-for="entry in entries" :key="entry.key" class="flex items-start">
83
+ <span class="shrink-0 text-primary">{{ dataType === 'array' ? '' : `"${entry.key}"` }}</span>
84
+ <span v-if="dataType !== 'array'" class="shrink-0 text-on-surface-variant mr-1">:</span>
85
+
86
+ <!-- Recursive child -->
87
+ <MJsonViewer
88
+ v-if="entry.value !== null && (typeof entry.value === 'object')"
89
+ :data="entry.value"
90
+ :root-name="entry.key"
91
+ :expand-depth="expandDepth"
92
+ :_depth="_depth + 1"
93
+ />
94
+ <!-- Primitive value -->
95
+ <span v-else :class="valueClass(entry.value)">{{ formatValue(entry.value) }}</span>
96
+ </div>
97
+ </div>
98
+ <span v-if="expanded" class="ml-5 text-on-surface-variant">{{ bracketClose }}</span>
99
+ </template>
100
+
101
+ <!-- Primitive root -->
102
+ <template v-else>
103
+ <span :class="valueClass(data)">{{ formatValue(data) }}</span>
104
+ </template>
105
+ </div>
106
+ </template>