@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,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MIcon from './MIcon.vue'
|
|
3
|
+
|
|
4
|
+
export interface BreadcrumbItem {
|
|
5
|
+
label: string
|
|
6
|
+
icon?: string
|
|
7
|
+
to?: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
withDefaults(defineProps<{
|
|
12
|
+
items: BreadcrumbItem[]
|
|
13
|
+
separator?: string
|
|
14
|
+
}>(), { separator: 'chevron_right' })
|
|
15
|
+
|
|
16
|
+
defineEmits<{ select: [BreadcrumbItem, number] }>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<nav aria-label="Breadcrumb" class="flex items-center gap-1 overflow-x-auto text-label-large">
|
|
21
|
+
<template v-for="(item, i) in items" :key="i">
|
|
22
|
+
<!-- Separator -->
|
|
23
|
+
<MIcon
|
|
24
|
+
v-if="i > 0"
|
|
25
|
+
:name="separator"
|
|
26
|
+
:size="18"
|
|
27
|
+
class="shrink-0 text-on-surface-variant"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Item -->
|
|
31
|
+
<button
|
|
32
|
+
v-if="i < items.length - 1 && !item.disabled"
|
|
33
|
+
type="button"
|
|
34
|
+
class="flex shrink-0 cursor-pointer items-center gap-1.5 rounded-sm px-1.5 py-1 text-primary transition-colors hover:bg-primary/8 focus-visible:outline-none"
|
|
35
|
+
@click="$emit('select', item, i)"
|
|
36
|
+
>
|
|
37
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="18" />
|
|
38
|
+
<span>{{ item.label }}</span>
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<!-- Last item (current page) or disabled -->
|
|
42
|
+
<span
|
|
43
|
+
v-else
|
|
44
|
+
class="flex shrink-0 items-center gap-1.5 px-1.5 py-1"
|
|
45
|
+
:class="item.disabled ? 'text-on-surface/38' : 'font-medium text-on-surface'"
|
|
46
|
+
>
|
|
47
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="18" />
|
|
48
|
+
<span>{{ item.label }}</span>
|
|
49
|
+
</span>
|
|
50
|
+
</template>
|
|
51
|
+
</nav>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import MSpinner from './MSpinner.vue'
|
|
4
|
+
import MIcon from './MIcon.vue'
|
|
5
|
+
|
|
6
|
+
const NAMED_COLORS = ['primary', 'error'] as const
|
|
7
|
+
type NamedColor = (typeof NAMED_COLORS)[number]
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
variant?: 'filled' | 'tonal' | 'outlined' | 'text' | 'elevated'
|
|
12
|
+
/**
|
|
13
|
+
* Named semantic color ('primary' | 'error') OR any CSS color string
|
|
14
|
+
* ('red', '#e91e63', 'oklch(0.6 0.2 0)', …).
|
|
15
|
+
* When a CSS color is passed, --color-primary is overridden for this button.
|
|
16
|
+
*/
|
|
17
|
+
color?: string
|
|
18
|
+
type?: 'button' | 'submit' | 'reset'
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
loading?: boolean
|
|
21
|
+
icon?: string
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
variant: 'filled',
|
|
25
|
+
color: 'primary',
|
|
26
|
+
type: 'button',
|
|
27
|
+
disabled: false,
|
|
28
|
+
loading: false,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const isCustomColor = computed(
|
|
33
|
+
() => !!props.color && !(NAMED_COLORS as readonly string[]).includes(props.color),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const customStyle = computed(() => {
|
|
37
|
+
if (!isCustomColor.value) return undefined
|
|
38
|
+
return {
|
|
39
|
+
'--color-primary': props.color,
|
|
40
|
+
'--color-on-primary': '#ffffff',
|
|
41
|
+
'--color-primary-container': props.color + '33',
|
|
42
|
+
'--color-on-primary-container': props.color,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const isError = computed(() => props.color === 'error')
|
|
47
|
+
|
|
48
|
+
// State-layer overlay: before: pseudo-element uses currentColor (the button's text color)
|
|
49
|
+
// so it's always the correct M3 state-layer color for every variant automatically.
|
|
50
|
+
const base =
|
|
51
|
+
'relative inline-flex items-center justify-center gap-2 h-10 rounded-full text-label-large font-medium ' +
|
|
52
|
+
'whitespace-nowrap overflow-hidden transition-[box-shadow,background-color,color] duration-150 select-none cursor-pointer ' +
|
|
53
|
+
'disabled:cursor-not-allowed disabled:opacity-[0.38] disabled:shadow-none ' +
|
|
54
|
+
"before:content-[''] before:pointer-events-none before:absolute before:inset-0 " +
|
|
55
|
+
'before:bg-current before:opacity-0 before:transition-opacity before:duration-150 ' +
|
|
56
|
+
'enabled:hover:before:opacity-[0.08] enabled:active:before:opacity-[0.12]'
|
|
57
|
+
|
|
58
|
+
const variantClasses = computed(() => {
|
|
59
|
+
const err = isError.value
|
|
60
|
+
switch (props.variant) {
|
|
61
|
+
case 'filled':
|
|
62
|
+
return err
|
|
63
|
+
? 'px-6 bg-error text-on-error enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
|
|
64
|
+
: 'px-6 bg-primary text-on-primary enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
|
|
65
|
+
case 'tonal':
|
|
66
|
+
return err
|
|
67
|
+
? 'px-6 bg-error-container text-on-error-container enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
|
|
68
|
+
: 'px-6 bg-secondary-container text-on-secondary-container enabled:hover:shadow-elevation-1 enabled:active:shadow-none'
|
|
69
|
+
case 'elevated':
|
|
70
|
+
return err
|
|
71
|
+
? 'px-6 bg-surface-container-low text-error shadow-elevation-1 enabled:hover:shadow-elevation-2'
|
|
72
|
+
: 'px-6 bg-surface-container-low text-primary shadow-elevation-1 enabled:hover:shadow-elevation-2'
|
|
73
|
+
case 'outlined':
|
|
74
|
+
return err
|
|
75
|
+
? 'px-6 border border-error text-error'
|
|
76
|
+
: 'px-6 border border-outline text-primary'
|
|
77
|
+
case 'text':
|
|
78
|
+
return err
|
|
79
|
+
? 'px-3 text-error'
|
|
80
|
+
: 'px-3 text-primary'
|
|
81
|
+
default:
|
|
82
|
+
return ''
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
function createRipple(event: PointerEvent) {
|
|
87
|
+
if (props.disabled || props.loading) return
|
|
88
|
+
const button = event.currentTarget as HTMLElement
|
|
89
|
+
const rect = button.getBoundingClientRect()
|
|
90
|
+
const d = Math.max(rect.width, rect.height) * 2
|
|
91
|
+
const el = document.createElement('span')
|
|
92
|
+
el.className = 'm3-ripple'
|
|
93
|
+
el.style.cssText = `width:${d}px;height:${d}px;top:${event.clientY - rect.top - d / 2}px;left:${event.clientX - rect.left - d / 2}px`
|
|
94
|
+
button.appendChild(el)
|
|
95
|
+
el.addEventListener('animationend', () => el.remove(), { once: true })
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<button
|
|
101
|
+
:type="type"
|
|
102
|
+
:disabled="disabled || loading"
|
|
103
|
+
:class="[base, variantClasses]"
|
|
104
|
+
:style="customStyle"
|
|
105
|
+
@pointerdown="createRipple"
|
|
106
|
+
>
|
|
107
|
+
<MSpinner v-if="loading" :size="18" />
|
|
108
|
+
<MIcon v-else-if="icon" :name="icon" :size="20" />
|
|
109
|
+
<slot />
|
|
110
|
+
</button>
|
|
111
|
+
</template>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import MIconButton from './MIconButton.vue'
|
|
5
|
+
|
|
6
|
+
export interface CalendarEvent {
|
|
7
|
+
id: string | number
|
|
8
|
+
title: string
|
|
9
|
+
date: string
|
|
10
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'success'
|
|
11
|
+
icon?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
15
|
+
events?: CalendarEvent[]
|
|
16
|
+
locale?: string
|
|
17
|
+
}>(), { events: () => [], locale: 'es-ES' })
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
dateClick: [string]
|
|
21
|
+
eventClick: [CalendarEvent]
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const viewDate = ref(new Date())
|
|
25
|
+
|
|
26
|
+
const WEEKDAYS = (() => {
|
|
27
|
+
const f = new Intl.DateTimeFormat(props.locale, { weekday: 'short' })
|
|
28
|
+
return Array.from({ length: 7 }, (_, i) => f.format(new Date(2024, 0, i + 1)))
|
|
29
|
+
})()
|
|
30
|
+
|
|
31
|
+
const monthLabel = computed(() =>
|
|
32
|
+
new Intl.DateTimeFormat(props.locale, { month: 'long', year: 'numeric' }).format(viewDate.value)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
function fmt(y: number, m: number, d: number) {
|
|
36
|
+
const dt = new Date(y, m, d)
|
|
37
|
+
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const todayIso = fmt(new Date().getFullYear(), new Date().getMonth(), new Date().getDate())
|
|
41
|
+
|
|
42
|
+
interface CalendarDay {
|
|
43
|
+
date: number
|
|
44
|
+
iso: string
|
|
45
|
+
current: boolean
|
|
46
|
+
events: CalendarEvent[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const calendarDays = computed<CalendarDay[]>(() => {
|
|
50
|
+
const y = viewDate.value.getFullYear()
|
|
51
|
+
const m = viewDate.value.getMonth()
|
|
52
|
+
const first = new Date(y, m, 1)
|
|
53
|
+
const startDay = (first.getDay() + 6) % 7
|
|
54
|
+
const daysInMonth = new Date(y, m + 1, 0).getDate()
|
|
55
|
+
const days: CalendarDay[] = []
|
|
56
|
+
|
|
57
|
+
const eventMap = new Map<string, CalendarEvent[]>()
|
|
58
|
+
for (const ev of props.events) {
|
|
59
|
+
if (!eventMap.has(ev.date)) eventMap.set(ev.date, [])
|
|
60
|
+
eventMap.get(ev.date)!.push(ev)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prevMonth = new Date(y, m, 0).getDate()
|
|
64
|
+
for (let i = startDay - 1; i >= 0; i--) {
|
|
65
|
+
const d = prevMonth - i
|
|
66
|
+
const iso = fmt(y, m - 1, d)
|
|
67
|
+
days.push({ date: d, current: false, iso, events: eventMap.get(iso) ?? [] })
|
|
68
|
+
}
|
|
69
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
70
|
+
const iso = fmt(y, m, d)
|
|
71
|
+
days.push({ date: d, current: true, iso, events: eventMap.get(iso) ?? [] })
|
|
72
|
+
}
|
|
73
|
+
const remaining = 42 - days.length
|
|
74
|
+
for (let d = 1; d <= remaining; d++) {
|
|
75
|
+
const iso = fmt(y, m + 1, d)
|
|
76
|
+
days.push({ date: d, current: false, iso, events: eventMap.get(iso) ?? [] })
|
|
77
|
+
}
|
|
78
|
+
return days
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function prevMonth() { const d = new Date(viewDate.value); d.setMonth(d.getMonth() - 1); viewDate.value = d }
|
|
82
|
+
function nextMonth() { const d = new Date(viewDate.value); d.setMonth(d.getMonth() + 1); viewDate.value = d }
|
|
83
|
+
function goToday() { viewDate.value = new Date() }
|
|
84
|
+
|
|
85
|
+
const eventColor: Record<string, string> = {
|
|
86
|
+
primary: 'bg-primary text-on-primary',
|
|
87
|
+
secondary: 'bg-secondary text-on-secondary',
|
|
88
|
+
tertiary: 'bg-tertiary text-on-tertiary',
|
|
89
|
+
error: 'bg-error text-on-error',
|
|
90
|
+
success: 'bg-success text-on-success',
|
|
91
|
+
}
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div class="flex flex-col overflow-hidden rounded-lg border border-outline-variant">
|
|
96
|
+
<!-- Header -->
|
|
97
|
+
<div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-3">
|
|
98
|
+
<div class="flex items-center gap-1">
|
|
99
|
+
<MIconButton icon="chevron_left" label="Mes anterior" :size="36" @click="prevMonth" />
|
|
100
|
+
<MIconButton icon="chevron_right" label="Mes siguiente" :size="36" @click="nextMonth" />
|
|
101
|
+
</div>
|
|
102
|
+
<h3 class="text-title-medium font-medium capitalize text-on-surface">{{ monthLabel }}</h3>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="cursor-pointer rounded-full border border-outline px-3 py-1 text-label-medium text-on-surface transition-colors hover:bg-on-surface/8"
|
|
106
|
+
@click="goToday"
|
|
107
|
+
>
|
|
108
|
+
Hoy
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Weekday headers -->
|
|
113
|
+
<div class="grid grid-cols-7 border-b border-outline-variant bg-surface-container-high">
|
|
114
|
+
<div
|
|
115
|
+
v-for="wd in WEEKDAYS"
|
|
116
|
+
:key="wd"
|
|
117
|
+
class="py-2 text-center text-label-small font-medium uppercase text-on-surface-variant"
|
|
118
|
+
>
|
|
119
|
+
{{ wd }}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Days grid -->
|
|
124
|
+
<div class="grid grid-cols-7">
|
|
125
|
+
<div
|
|
126
|
+
v-for="(day, i) in calendarDays"
|
|
127
|
+
:key="i"
|
|
128
|
+
class="flex min-h-[80px] cursor-pointer flex-col border-b border-r border-outline-variant/50 p-1.5 transition-colors hover:bg-on-surface/[0.03]"
|
|
129
|
+
:class="[
|
|
130
|
+
!day.current ? 'bg-surface-container-lowest/50' : 'bg-surface',
|
|
131
|
+
(i + 1) % 7 === 0 ? 'border-r-0' : '',
|
|
132
|
+
i >= 35 ? 'border-b-0' : '',
|
|
133
|
+
]"
|
|
134
|
+
@click="emit('dateClick', day.iso)"
|
|
135
|
+
>
|
|
136
|
+
<!-- Day number -->
|
|
137
|
+
<span
|
|
138
|
+
class="mb-0.5 flex h-6 w-6 items-center justify-center self-end rounded-full text-label-medium"
|
|
139
|
+
:class="
|
|
140
|
+
day.iso === todayIso
|
|
141
|
+
? 'bg-primary text-on-primary font-medium'
|
|
142
|
+
: day.current
|
|
143
|
+
? 'text-on-surface'
|
|
144
|
+
: 'text-on-surface-variant/40'
|
|
145
|
+
"
|
|
146
|
+
>
|
|
147
|
+
{{ day.date }}
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
<!-- Events -->
|
|
151
|
+
<div v-if="day.events.length" class="flex flex-col gap-0.5">
|
|
152
|
+
<button
|
|
153
|
+
v-for="ev in day.events.slice(0, 2)"
|
|
154
|
+
:key="ev.id"
|
|
155
|
+
type="button"
|
|
156
|
+
class="flex w-full cursor-pointer items-center gap-1 truncate rounded px-1 py-0.5 text-left text-label-small transition-opacity hover:opacity-80"
|
|
157
|
+
:class="eventColor[ev.color ?? 'primary']"
|
|
158
|
+
@click.stop="emit('eventClick', ev)"
|
|
159
|
+
>
|
|
160
|
+
<MIcon v-if="ev.icon" :name="ev.icon" :size="12" />
|
|
161
|
+
<span class="truncate">{{ ev.title }}</span>
|
|
162
|
+
</button>
|
|
163
|
+
<span
|
|
164
|
+
v-if="day.events.length > 2"
|
|
165
|
+
class="px-1 text-label-small text-on-surface-variant"
|
|
166
|
+
>
|
|
167
|
+
+{{ day.events.length - 2 }} más
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
variant?: 'elevated' | 'filled' | 'outlined'
|
|
7
|
+
clickable?: boolean
|
|
8
|
+
elevated?: boolean
|
|
9
|
+
/** src URL for a full-bleed header image */
|
|
10
|
+
image?: string
|
|
11
|
+
imageAlt?: string
|
|
12
|
+
imageHeight?: string
|
|
13
|
+
}>(),
|
|
14
|
+
{ variant: 'elevated', clickable: false, elevated: false },
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const resolvedVariant = computed(() => (props.elevated ? 'elevated' : props.variant))
|
|
18
|
+
|
|
19
|
+
const variantClasses: Record<string, string> = {
|
|
20
|
+
elevated: 'bg-surface-container-low shadow-elevation-1',
|
|
21
|
+
filled: 'bg-surface-container-highest',
|
|
22
|
+
outlined: 'bg-surface border border-outline-variant',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Expose the card's background as --field-bg so outlined text-field labels
|
|
26
|
+
// inside the card automatically match without needing the fieldBg prop.
|
|
27
|
+
const fieldBgByVariant: Record<string, string> = {
|
|
28
|
+
elevated: 'var(--color-surface-container-low)',
|
|
29
|
+
filled: 'var(--color-surface-container-highest)',
|
|
30
|
+
outlined: 'var(--color-surface)',
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div
|
|
36
|
+
class="overflow-hidden rounded-md transition-shadow duration-150"
|
|
37
|
+
:class="[
|
|
38
|
+
variantClasses[resolvedVariant],
|
|
39
|
+
clickable ? 'cursor-pointer hover:shadow-elevation-2 active:shadow-elevation-1' : '',
|
|
40
|
+
]"
|
|
41
|
+
:style="{ '--field-bg': fieldBgByVariant[resolvedVariant] }"
|
|
42
|
+
>
|
|
43
|
+
<!-- Optional header image -->
|
|
44
|
+
<div v-if="image || $slots.media" :class="['w-full overflow-hidden', imageHeight ?? 'h-48']">
|
|
45
|
+
<img
|
|
46
|
+
v-if="image"
|
|
47
|
+
:src="image"
|
|
48
|
+
:alt="imageAlt ?? ''"
|
|
49
|
+
class="h-full w-full object-cover"
|
|
50
|
+
/>
|
|
51
|
+
<slot v-else name="media" />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<slot />
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch, onMounted } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
Chart as ChartJS,
|
|
5
|
+
CategoryScale,
|
|
6
|
+
LinearScale,
|
|
7
|
+
PointElement,
|
|
8
|
+
LineElement,
|
|
9
|
+
BarElement,
|
|
10
|
+
ArcElement,
|
|
11
|
+
RadialLinearScale,
|
|
12
|
+
Filler,
|
|
13
|
+
Tooltip,
|
|
14
|
+
Legend,
|
|
15
|
+
Title,
|
|
16
|
+
type ChartData,
|
|
17
|
+
type ChartOptions,
|
|
18
|
+
} from 'chart.js'
|
|
19
|
+
import { Line, Bar, Pie, Doughnut, Radar } from 'vue-chartjs'
|
|
20
|
+
|
|
21
|
+
ChartJS.register(
|
|
22
|
+
CategoryScale,
|
|
23
|
+
LinearScale,
|
|
24
|
+
PointElement,
|
|
25
|
+
LineElement,
|
|
26
|
+
BarElement,
|
|
27
|
+
ArcElement,
|
|
28
|
+
RadialLinearScale,
|
|
29
|
+
Filler,
|
|
30
|
+
Tooltip,
|
|
31
|
+
Legend,
|
|
32
|
+
Title,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'radar'
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(
|
|
38
|
+
defineProps<{
|
|
39
|
+
type: ChartType
|
|
40
|
+
data: ChartData<any>
|
|
41
|
+
options?: ChartOptions<any>
|
|
42
|
+
height?: string
|
|
43
|
+
}>(),
|
|
44
|
+
{ height: '300px' },
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
function getM3Colors() {
|
|
48
|
+
const style = getComputedStyle(document.documentElement)
|
|
49
|
+
const get = (v: string) => style.getPropertyValue(v).trim()
|
|
50
|
+
return {
|
|
51
|
+
primary: get('--color-primary'),
|
|
52
|
+
onSurface: get('--color-on-surface'),
|
|
53
|
+
onSurfaceVariant: get('--color-on-surface-variant'),
|
|
54
|
+
outlineVariant: get('--color-outline-variant'),
|
|
55
|
+
surface: get('--color-surface'),
|
|
56
|
+
surfaceContainer: get('--color-surface-container'),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const m3Colors = ref(getM3Colors())
|
|
61
|
+
|
|
62
|
+
onMounted(() => { m3Colors.value = getM3Colors() })
|
|
63
|
+
|
|
64
|
+
const themeObserver = ref<MutationObserver | null>(null)
|
|
65
|
+
onMounted(() => {
|
|
66
|
+
themeObserver.value = new MutationObserver(() => { m3Colors.value = getM3Colors() })
|
|
67
|
+
themeObserver.value.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
watch(() => m3Colors.value, () => {}, { deep: true })
|
|
71
|
+
|
|
72
|
+
const mergedOptions = computed<ChartOptions<any>>(() => {
|
|
73
|
+
const c = m3Colors.value
|
|
74
|
+
const base: ChartOptions<any> = {
|
|
75
|
+
responsive: true,
|
|
76
|
+
maintainAspectRatio: false,
|
|
77
|
+
plugins: {
|
|
78
|
+
legend: {
|
|
79
|
+
labels: {
|
|
80
|
+
color: c.onSurface,
|
|
81
|
+
font: { family: "'Roboto', system-ui, sans-serif", size: 12 },
|
|
82
|
+
usePointStyle: true,
|
|
83
|
+
pointStyle: 'circle',
|
|
84
|
+
padding: 16,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
tooltip: {
|
|
88
|
+
backgroundColor: c.surfaceContainer,
|
|
89
|
+
titleColor: c.onSurface,
|
|
90
|
+
bodyColor: c.onSurfaceVariant,
|
|
91
|
+
borderColor: c.outlineVariant,
|
|
92
|
+
borderWidth: 1,
|
|
93
|
+
cornerRadius: 12,
|
|
94
|
+
padding: 12,
|
|
95
|
+
titleFont: { family: "'Roboto', system-ui, sans-serif", size: 13, weight: '600' as const },
|
|
96
|
+
bodyFont: { family: "'Roboto', system-ui, sans-serif", size: 12 },
|
|
97
|
+
displayColors: true,
|
|
98
|
+
boxPadding: 4,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (props.type !== 'pie' && props.type !== 'doughnut') {
|
|
104
|
+
base.scales = {
|
|
105
|
+
x: {
|
|
106
|
+
grid: { color: c.outlineVariant + '40', drawTicks: false },
|
|
107
|
+
ticks: { color: c.onSurfaceVariant, font: { size: 11 }, padding: 8 },
|
|
108
|
+
border: { color: c.outlineVariant },
|
|
109
|
+
},
|
|
110
|
+
y: {
|
|
111
|
+
grid: { color: c.outlineVariant + '40', drawTicks: false },
|
|
112
|
+
ticks: { color: c.onSurfaceVariant, font: { size: 11 }, padding: 8 },
|
|
113
|
+
border: { color: c.outlineVariant },
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (props.type === 'radar') {
|
|
119
|
+
base.scales = {
|
|
120
|
+
r: {
|
|
121
|
+
grid: { color: c.outlineVariant + '40' },
|
|
122
|
+
angleLines: { color: c.outlineVariant + '40' },
|
|
123
|
+
pointLabels: { color: c.onSurfaceVariant, font: { size: 11 } },
|
|
124
|
+
ticks: { color: c.onSurfaceVariant, backdropColor: 'transparent' },
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return deepMerge(base, props.options ?? {})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
function deepMerge(target: any, source: any): any {
|
|
133
|
+
const output = { ...target }
|
|
134
|
+
for (const key of Object.keys(source)) {
|
|
135
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
136
|
+
output[key] = deepMerge(target[key] ?? {}, source[key])
|
|
137
|
+
} else {
|
|
138
|
+
output[key] = source[key]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return output
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const chartComponent = computed(() => {
|
|
145
|
+
const map = { line: Line, bar: Bar, pie: Pie, doughnut: Doughnut, radar: Radar }
|
|
146
|
+
return map[props.type]
|
|
147
|
+
})
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<div class="rounded-lg border border-outline-variant bg-surface p-4" :style="{ height }">
|
|
152
|
+
<component
|
|
153
|
+
:is="chartComponent"
|
|
154
|
+
:data="data"
|
|
155
|
+
:options="mergedOptions"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MIcon from "./MIcon.vue";
|
|
3
|
+
|
|
4
|
+
withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
modelValue: boolean;
|
|
7
|
+
indeterminate?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
label?: string;
|
|
10
|
+
}>(),
|
|
11
|
+
{ indeterminate: false, disabled: false },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{ "update:modelValue": [boolean] }>();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<label
|
|
19
|
+
class="inline-flex items-center gap-2 select-none"
|
|
20
|
+
:class="disabled ? 'cursor-not-allowed opacity-[0.38]' : 'cursor-pointer'"
|
|
21
|
+
>
|
|
22
|
+
<span
|
|
23
|
+
class="relative inline-flex h-4.5 w-4.5 shrink-0 items-center justify-center rounded-[3px] border-2 transition-colors"
|
|
24
|
+
:class="
|
|
25
|
+
modelValue || indeterminate
|
|
26
|
+
? 'border-primary bg-primary text-on-primary'
|
|
27
|
+
: 'border-on-surface-variant text-transparent'
|
|
28
|
+
"
|
|
29
|
+
>
|
|
30
|
+
<input
|
|
31
|
+
type="checkbox"
|
|
32
|
+
class="sr-only"
|
|
33
|
+
:checked="modelValue"
|
|
34
|
+
:disabled="disabled"
|
|
35
|
+
@change="emit('update:modelValue', !modelValue)"
|
|
36
|
+
/>
|
|
37
|
+
<MIcon
|
|
38
|
+
:name="indeterminate ? 'remove' : 'check'"
|
|
39
|
+
:size="14"
|
|
40
|
+
class="transition-[opacity,transform] duration-150"
|
|
41
|
+
:class="modelValue || indeterminate ? 'scale-100 opacity-100' : 'scale-0 opacity-0'"
|
|
42
|
+
/>
|
|
43
|
+
</span>
|
|
44
|
+
<span v-if="label || $slots.default" class="text-body-large text-on-surface">
|
|
45
|
+
<slot>{{ label }}</slot>
|
|
46
|
+
</span>
|
|
47
|
+
</label>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
const NAMED_TONES = ['neutral', 'primary', 'success', 'error', 'tertiary', 'secondary'] as const
|
|
6
|
+
type NamedTone = (typeof NAMED_TONES)[number]
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
tone?: string // named tone OR CSS color string
|
|
11
|
+
selected?: boolean
|
|
12
|
+
removable?: boolean
|
|
13
|
+
clickable?: boolean
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
icon?: string
|
|
16
|
+
}>(),
|
|
17
|
+
{
|
|
18
|
+
tone: 'neutral',
|
|
19
|
+
selected: false,
|
|
20
|
+
removable: false,
|
|
21
|
+
clickable: false,
|
|
22
|
+
disabled: false,
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{ click: []; remove: [] }>()
|
|
27
|
+
|
|
28
|
+
const isCustomColor = computed(
|
|
29
|
+
() => !!props.tone && !(NAMED_TONES as readonly string[]).includes(props.tone),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// When a CSS color is passed, apply it via CSS variables
|
|
33
|
+
const customStyle = computed(() => {
|
|
34
|
+
if (!isCustomColor.value) return undefined
|
|
35
|
+
return {
|
|
36
|
+
'--chip-bg': props.tone + '22',
|
|
37
|
+
'--chip-color': props.tone,
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const toneClasses = computed(() => {
|
|
42
|
+
if (isCustomColor.value) {
|
|
43
|
+
return 'border border-transparent bg-[var(--chip-bg)] text-[var(--chip-color)]'
|
|
44
|
+
}
|
|
45
|
+
if (props.tone === 'neutral' && !props.selected) {
|
|
46
|
+
return 'border border-outline bg-transparent text-on-surface-variant'
|
|
47
|
+
}
|
|
48
|
+
const map: Record<string, string> = {
|
|
49
|
+
neutral: 'border border-transparent bg-secondary-container text-on-secondary-container',
|
|
50
|
+
primary: 'border border-transparent bg-primary-container text-on-primary-container',
|
|
51
|
+
secondary: 'border border-transparent bg-secondary-container text-on-secondary-container',
|
|
52
|
+
success: 'border border-transparent bg-success-container text-on-success-container',
|
|
53
|
+
error: 'border border-transparent bg-error-container text-on-error-container',
|
|
54
|
+
tertiary: 'border border-transparent bg-tertiary-container text-on-tertiary-container',
|
|
55
|
+
}
|
|
56
|
+
return map[props.tone ?? 'neutral'] ?? map.neutral
|
|
57
|
+
})
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<component
|
|
62
|
+
:is="clickable ? 'button' : 'span'"
|
|
63
|
+
:type="clickable ? 'button' : undefined"
|
|
64
|
+
:disabled="clickable && disabled ? true : undefined"
|
|
65
|
+
class="inline-flex h-8 items-center gap-1.5 rounded-sm px-3 text-label-large transition-colors"
|
|
66
|
+
:class="[
|
|
67
|
+
toneClasses,
|
|
68
|
+
clickable && !disabled ? 'cursor-pointer hover:bg-on-surface/8' : '',
|
|
69
|
+
disabled ? 'cursor-not-allowed opacity-[0.38]' : '',
|
|
70
|
+
]"
|
|
71
|
+
:style="customStyle"
|
|
72
|
+
@click="clickable && !disabled && emit('click')"
|
|
73
|
+
>
|
|
74
|
+
<MIcon v-if="icon" :name="icon" :size="18" />
|
|
75
|
+
<slot />
|
|
76
|
+
<button
|
|
77
|
+
v-if="removable"
|
|
78
|
+
type="button"
|
|
79
|
+
class="-mr-1 ml-0.5 inline-flex items-center justify-center rounded-full hover:bg-on-surface/12"
|
|
80
|
+
aria-label="Quitar"
|
|
81
|
+
:disabled="disabled"
|
|
82
|
+
@click.stop="emit('remove')"
|
|
83
|
+
>
|
|
84
|
+
<MIcon name="close" :size="16" />
|
|
85
|
+
</button>
|
|
86
|
+
</component>
|
|
87
|
+
</template>
|