@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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/components/MAlert.vue.d.ts +27 -0
- package/dist/components/MAppBar.vue.d.ts +24 -0
- package/dist/components/MAvatar.vue.d.ts +9 -0
- package/dist/components/MBadge.vue.d.ts +22 -0
- package/dist/components/MBottomSheet.vue.d.ts +26 -0
- package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
- package/dist/components/MButton.vue.d.ts +32 -0
- package/dist/components/MCalendar.vue.d.ts +23 -0
- package/dist/components/MCard.vue.d.ts +28 -0
- package/dist/components/MChart.vue.d.ts +13 -0
- package/dist/components/MCheckbox.vue.d.ts +26 -0
- package/dist/components/MChip.vue.d.ts +33 -0
- package/dist/components/MCodeEditor.vue.d.ts +35 -0
- package/dist/components/MColorPicker.vue.d.ts +18 -0
- package/dist/components/MCommandPalette.vue.d.ts +29 -0
- package/dist/components/MConfirmDialog.vue.d.ts +23 -0
- package/dist/components/MContainer.vue.d.ts +24 -0
- package/dist/components/MContextMenu.vue.d.ts +35 -0
- package/dist/components/MDataTable.vue.d.ts +83 -0
- package/dist/components/MDatePicker.vue.d.ts +21 -0
- package/dist/components/MDateRangePicker.vue.d.ts +24 -0
- package/dist/components/MDialog.vue.d.ts +30 -0
- package/dist/components/MDivider.vue.d.ts +11 -0
- package/dist/components/MDragDropList.vue.d.ts +40 -0
- package/dist/components/MEmptyState.vue.d.ts +21 -0
- package/dist/components/MExpansionPanel.vue.d.ts +28 -0
- package/dist/components/MFab.vue.d.ts +28 -0
- package/dist/components/MFileUpload.vue.d.ts +25 -0
- package/dist/components/MGrid.vue.d.ts +26 -0
- package/dist/components/MHotkeys.vue.d.ts +16 -0
- package/dist/components/MIcon.vue.d.ts +9 -0
- package/dist/components/MIconButton.vue.d.ts +14 -0
- package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
- package/dist/components/MJsonEditor.vue.d.ts +17 -0
- package/dist/components/MJsonViewer.vue.d.ts +14 -0
- package/dist/components/MKanban.vue.d.ts +53 -0
- package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
- package/dist/components/MMarkdown.vue.d.ts +11 -0
- package/dist/components/MMasonry.vue.d.ts +23 -0
- package/dist/components/MMenu.vue.d.ts +27 -0
- package/dist/components/MMenuItem.vue.d.ts +16 -0
- package/dist/components/MMultiSelect.vue.d.ts +34 -0
- package/dist/components/MNavigationBar.vue.d.ts +18 -0
- package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
- package/dist/components/MNavigationRail.vue.d.ts +32 -0
- package/dist/components/MPagination.vue.d.ts +12 -0
- package/dist/components/MProgressBar.vue.d.ts +13 -0
- package/dist/components/MRadio.vue.d.ts +17 -0
- package/dist/components/MRadioGroup.vue.d.ts +24 -0
- package/dist/components/MRating.vue.d.ts +23 -0
- package/dist/components/MResult.vue.d.ts +20 -0
- package/dist/components/MRichTextEditor.vue.d.ts +17 -0
- package/dist/components/MScheduler.vue.d.ts +35 -0
- package/dist/components/MSegmentedButton.vue.d.ts +24 -0
- package/dist/components/MSelect.vue.d.ts +29 -0
- package/dist/components/MSideSheet.vue.d.ts +28 -0
- package/dist/components/MSkeleton.vue.d.ts +14 -0
- package/dist/components/MSlider.vue.d.ts +24 -0
- package/dist/components/MSnackbar.vue.d.ts +3 -0
- package/dist/components/MSpinner.vue.d.ts +10 -0
- package/dist/components/MSplitter.vue.d.ts +26 -0
- package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
- package/dist/components/MStack.vue.d.ts +30 -0
- package/dist/components/MStatCard.vue.d.ts +24 -0
- package/dist/components/MStepper.vue.d.ts +33 -0
- package/dist/components/MSwitch.vue.d.ts +14 -0
- package/dist/components/MTable.vue.d.ts +73 -0
- package/dist/components/MTabs.vue.d.ts +20 -0
- package/dist/components/MTerminal.vue.d.ts +25 -0
- package/dist/components/MTextField.vue.d.ts +41 -0
- package/dist/components/MTimePicker.vue.d.ts +20 -0
- package/dist/components/MTimeline.vue.d.ts +31 -0
- package/dist/components/MTooltip.vue.d.ts +21 -0
- package/dist/components/MTopAppBar.vue.d.ts +29 -0
- package/dist/components/MTour.vue.d.ts +19 -0
- package/dist/components/MTransferList.vue.d.ts +23 -0
- package/dist/components/MTree.vue.d.ts +68 -0
- package/dist/components/MTreeTable.vue.d.ts +57 -0
- package/dist/components/MVirtualTable.vue.d.ts +40 -0
- package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
- package/dist/components/_MTreeNode.vue.d.ts +26 -0
- package/dist/composables/useColorPalette.d.ts +11 -0
- package/dist/composables/useFieldBg.d.ts +13 -0
- package/dist/composables/useTheme.d.ts +5 -0
- package/dist/composables/useToast.d.ts +59 -0
- package/dist/index.d.ts +112 -0
- package/dist/m3ui.css +2 -0
- package/dist/m3ui.js +7432 -0
- package/dist/m3ui.js.map +1 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/styles/palettes.css +1253 -0
- package/dist/styles/theme.css +249 -0
- package/package.json +166 -0
- package/src/components/MAlert.vue +69 -0
- package/src/components/MAppBar.vue +40 -0
- package/src/components/MAvatar.vue +21 -0
- package/src/components/MBadge.vue +46 -0
- package/src/components/MBottomSheet.vue +113 -0
- package/src/components/MBreadcrumbs.vue +52 -0
- package/src/components/MButton.vue +111 -0
- package/src/components/MCalendar.vue +173 -0
- package/src/components/MCard.vue +56 -0
- package/src/components/MChart.vue +158 -0
- package/src/components/MCheckbox.vue +48 -0
- package/src/components/MChip.vue +87 -0
- package/src/components/MCodeEditor.vue +179 -0
- package/src/components/MColorPicker.vue +305 -0
- package/src/components/MCommandPalette.vue +213 -0
- package/src/components/MConfirmDialog.vue +43 -0
- package/src/components/MContainer.vue +36 -0
- package/src/components/MContextMenu.vue +66 -0
- package/src/components/MDataTable.vue +376 -0
- package/src/components/MDatePicker.vue +253 -0
- package/src/components/MDateRangePicker.vue +265 -0
- package/src/components/MDialog.vue +90 -0
- package/src/components/MDivider.vue +26 -0
- package/src/components/MDragDropList.vue +111 -0
- package/src/components/MEmptyState.vue +40 -0
- package/src/components/MExpansionPanel.vue +112 -0
- package/src/components/MFab.vue +220 -0
- package/src/components/MFileUpload.vue +206 -0
- package/src/components/MGrid.vue +99 -0
- package/src/components/MHotkeys.vue +122 -0
- package/src/components/MIcon.vue +9 -0
- package/src/components/MIconButton.vue +49 -0
- package/src/components/MInfiniteScroll.vue +68 -0
- package/src/components/MJsonEditor.vue +118 -0
- package/src/components/MJsonViewer.vue +106 -0
- package/src/components/MKanban.vue +147 -0
- package/src/components/MLoadingOverlay.vue +52 -0
- package/src/components/MMarkdown.vue +123 -0
- package/src/components/MMasonry.vue +87 -0
- package/src/components/MMenu.vue +113 -0
- package/src/components/MMenuItem.vue +15 -0
- package/src/components/MMultiSelect.vue +306 -0
- package/src/components/MNavigationBar.vue +62 -0
- package/src/components/MNavigationDrawer.vue +157 -0
- package/src/components/MNavigationRail.vue +80 -0
- package/src/components/MPagination.vue +37 -0
- package/src/components/MProgressBar.vue +200 -0
- package/src/components/MRadio.vue +89 -0
- package/src/components/MRadioGroup.vue +41 -0
- package/src/components/MRating.vue +108 -0
- package/src/components/MResult.vue +62 -0
- package/src/components/MRichTextEditor.vue +199 -0
- package/src/components/MScheduler.vue +225 -0
- package/src/components/MSegmentedButton.vue +75 -0
- package/src/components/MSelect.vue +259 -0
- package/src/components/MSideSheet.vue +112 -0
- package/src/components/MSkeleton.vue +60 -0
- package/src/components/MSlider.vue +188 -0
- package/src/components/MSnackbar.vue +244 -0
- package/src/components/MSpinner.vue +122 -0
- package/src/components/MSplitter.vue +97 -0
- package/src/components/MSpotlightSearch.vue +244 -0
- package/src/components/MStack.vue +67 -0
- package/src/components/MStatCard.vue +56 -0
- package/src/components/MStepper.vue +161 -0
- package/src/components/MSwitch.vue +63 -0
- package/src/components/MTable.vue +404 -0
- package/src/components/MTabs.vue +97 -0
- package/src/components/MTerminal.vue +146 -0
- package/src/components/MTextField.vue +180 -0
- package/src/components/MTimePicker.vue +227 -0
- package/src/components/MTimeline.vue +117 -0
- package/src/components/MTooltip.vue +82 -0
- package/src/components/MTopAppBar.vue +62 -0
- package/src/components/MTour.vue +226 -0
- package/src/components/MTransferList.vue +181 -0
- package/src/components/MTree.vue +164 -0
- package/src/components/MTreeTable.vue +159 -0
- package/src/components/MVirtualTable.vue +155 -0
- package/src/components/_MContextMenuPanel.vue +129 -0
- package/src/components/_MTreeNode.vue +171 -0
- package/src/composables/useColorPalette.ts +60 -0
- package/src/composables/useFieldBg.ts +91 -0
- package/src/composables/useTheme.ts +55 -0
- package/src/composables/useToast.ts +51 -0
- package/src/env.d.ts +1 -0
- package/src/index.ts +119 -0
- package/src/plugin.ts +18 -0
- package/src/styles/palettes.css +1253 -0
- 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,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>
|