@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,147 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import MIconButton from './MIconButton.vue'
|
|
5
|
+
|
|
6
|
+
export interface KanbanCard {
|
|
7
|
+
id: string | number
|
|
8
|
+
[key: string]: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface KanbanColumn {
|
|
12
|
+
id: string | number
|
|
13
|
+
title: string
|
|
14
|
+
cards: KanbanCard[]
|
|
15
|
+
color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'success'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
modelValue: KanbanColumn[]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
'update:modelValue': [KanbanColumn[]]
|
|
24
|
+
cardMove: [{ cardId: string | number; fromColumn: string | number; toColumn: string | number; toIndex: number }]
|
|
25
|
+
cardClick: [{ card: KanbanCard; columnId: string | number }]
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const dragCard = ref<{ cardId: string | number; columnId: string | number } | null>(null)
|
|
29
|
+
const overColumn = ref<string | number | null>(null)
|
|
30
|
+
const overCardIndex = ref<number | null>(null)
|
|
31
|
+
|
|
32
|
+
const colorMap: Record<string, string> = {
|
|
33
|
+
primary: 'bg-primary',
|
|
34
|
+
secondary: 'bg-secondary',
|
|
35
|
+
tertiary: 'bg-tertiary',
|
|
36
|
+
error: 'bg-error',
|
|
37
|
+
success: 'bg-success',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onCardDragStart(e: DragEvent, card: KanbanCard, columnId: string | number) {
|
|
41
|
+
dragCard.value = { cardId: card.id, columnId }
|
|
42
|
+
if (e.dataTransfer) {
|
|
43
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
44
|
+
e.dataTransfer.setData('text/plain', String(card.id))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function onColumnDragOver(e: DragEvent, columnId: string | number) {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
|
51
|
+
overColumn.value = columnId
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function onCardDragOver(e: DragEvent, _card: KanbanCard, index: number, columnId: string | number) {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
e.stopPropagation()
|
|
57
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
|
58
|
+
overColumn.value = columnId
|
|
59
|
+
overCardIndex.value = index
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onDrop(e: DragEvent, toColumnId: string | number) {
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
if (!dragCard.value) return
|
|
65
|
+
|
|
66
|
+
const { cardId, columnId: fromColumnId } = dragCard.value
|
|
67
|
+
if (fromColumnId === toColumnId && overCardIndex.value === null) {
|
|
68
|
+
reset()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const columns = props.modelValue.map((col) => ({ ...col, cards: [...col.cards] }))
|
|
73
|
+
const fromCol = columns.find((c) => c.id === fromColumnId)
|
|
74
|
+
const toCol = columns.find((c) => c.id === toColumnId)
|
|
75
|
+
if (!fromCol || !toCol) { reset(); return }
|
|
76
|
+
|
|
77
|
+
const cardIndex = fromCol.cards.findIndex((c) => c.id === cardId)
|
|
78
|
+
if (cardIndex === -1) { reset(); return }
|
|
79
|
+
|
|
80
|
+
const removed = fromCol.cards.splice(cardIndex, 1)
|
|
81
|
+
const toIndex = overCardIndex.value ?? toCol.cards.length
|
|
82
|
+
toCol.cards.splice(toIndex, 0, removed[0]!)
|
|
83
|
+
|
|
84
|
+
emit('update:modelValue', columns)
|
|
85
|
+
emit('cardMove', { cardId, fromColumn: fromColumnId, toColumn: toColumnId, toIndex })
|
|
86
|
+
reset()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reset() {
|
|
90
|
+
dragCard.value = null
|
|
91
|
+
overColumn.value = null
|
|
92
|
+
overCardIndex.value = null
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div class="flex gap-4 overflow-x-auto pb-2">
|
|
98
|
+
<div
|
|
99
|
+
v-for="column in modelValue"
|
|
100
|
+
:key="column.id"
|
|
101
|
+
class="flex w-72 shrink-0 flex-col rounded-xl bg-surface-container-low"
|
|
102
|
+
:class="overColumn === column.id && dragCard ? 'ring-2 ring-primary ring-inset' : ''"
|
|
103
|
+
@dragover="onColumnDragOver($event, column.id)"
|
|
104
|
+
@dragleave="overColumn = null"
|
|
105
|
+
@drop="onDrop($event, column.id)"
|
|
106
|
+
>
|
|
107
|
+
<!-- Column header -->
|
|
108
|
+
<div class="flex items-center gap-2 px-4 py-3">
|
|
109
|
+
<div v-if="column.color" class="h-2.5 w-2.5 rounded-full" :class="colorMap[column.color] ?? 'bg-primary'" />
|
|
110
|
+
<h3 class="flex-1 text-title-small font-medium text-on-surface">{{ column.title }}</h3>
|
|
111
|
+
<span class="rounded-full bg-surface-container-high px-2 py-0.5 text-label-small text-on-surface-variant">
|
|
112
|
+
{{ column.cards.length }}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Cards area -->
|
|
117
|
+
<div class="flex min-h-[60px] flex-1 flex-col gap-2 px-3 pb-3">
|
|
118
|
+
<div
|
|
119
|
+
v-for="(card, index) in column.cards"
|
|
120
|
+
:key="card.id"
|
|
121
|
+
draggable="true"
|
|
122
|
+
class="cursor-grab rounded-lg bg-surface p-3 shadow-elevation-1 transition-all duration-150 active:cursor-grabbing"
|
|
123
|
+
:class="[
|
|
124
|
+
dragCard?.cardId === card.id ? 'opacity-30' : 'hover:shadow-elevation-2',
|
|
125
|
+
overCardIndex === index && overColumn === column.id && dragCard ? 'border-t-2 border-primary' : '',
|
|
126
|
+
]"
|
|
127
|
+
@dragstart="onCardDragStart($event, card, column.id)"
|
|
128
|
+
@dragover="onCardDragOver($event, card, index, column.id)"
|
|
129
|
+
@dragend="reset"
|
|
130
|
+
@click="emit('cardClick', { card, columnId: column.id })"
|
|
131
|
+
>
|
|
132
|
+
<slot name="card" :card="card" :column="column">
|
|
133
|
+
<p class="text-body-medium text-on-surface">{{ card.id }}</p>
|
|
134
|
+
</slot>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Empty state -->
|
|
138
|
+
<div
|
|
139
|
+
v-if="column.cards.length === 0"
|
|
140
|
+
class="flex flex-1 items-center justify-center rounded-lg border border-dashed border-outline-variant/50 p-4"
|
|
141
|
+
>
|
|
142
|
+
<p class="text-body-small text-on-surface-variant/60">Sin tarjetas</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MSpinner from './MSpinner.vue'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<{
|
|
5
|
+
visible: boolean
|
|
6
|
+
text?: string
|
|
7
|
+
fullscreen?: boolean
|
|
8
|
+
opaque?: boolean
|
|
9
|
+
spinnerSize?: number
|
|
10
|
+
}>(), { fullscreen: false, opaque: false, spinnerSize: 40 })
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<Teleport v-if="fullscreen" to="body">
|
|
15
|
+
<Transition
|
|
16
|
+
enter-active-class="transition-opacity duration-200"
|
|
17
|
+
enter-from-class="opacity-0"
|
|
18
|
+
leave-active-class="transition-opacity duration-150"
|
|
19
|
+
leave-to-class="opacity-0"
|
|
20
|
+
>
|
|
21
|
+
<div
|
|
22
|
+
v-if="visible"
|
|
23
|
+
class="fixed inset-0 z-[300] flex flex-col items-center justify-center gap-4"
|
|
24
|
+
:class="opaque ? 'bg-surface' : 'bg-surface/80 backdrop-blur-sm'"
|
|
25
|
+
>
|
|
26
|
+
<MSpinner :size="spinnerSize" class="text-primary" />
|
|
27
|
+
<p v-if="text" class="text-body-large text-on-surface-variant">{{ text }}</p>
|
|
28
|
+
<slot />
|
|
29
|
+
</div>
|
|
30
|
+
</Transition>
|
|
31
|
+
</Teleport>
|
|
32
|
+
|
|
33
|
+
<div v-else class="relative">
|
|
34
|
+
<slot name="content" />
|
|
35
|
+
<Transition
|
|
36
|
+
enter-active-class="transition-opacity duration-200"
|
|
37
|
+
enter-from-class="opacity-0"
|
|
38
|
+
leave-active-class="transition-opacity duration-150"
|
|
39
|
+
leave-to-class="opacity-0"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
v-if="visible"
|
|
43
|
+
class="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 rounded-[inherit]"
|
|
44
|
+
:class="opaque ? 'bg-surface' : 'bg-surface/80 backdrop-blur-sm'"
|
|
45
|
+
>
|
|
46
|
+
<MSpinner :size="spinnerSize" class="text-primary" />
|
|
47
|
+
<p v-if="text" class="text-body-medium text-on-surface-variant">{{ text }}</p>
|
|
48
|
+
<slot />
|
|
49
|
+
</div>
|
|
50
|
+
</Transition>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import MarkdownIt from 'markdown-it'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
source: string
|
|
8
|
+
breaks?: boolean
|
|
9
|
+
linkify?: boolean
|
|
10
|
+
}>(),
|
|
11
|
+
{ breaks: true, linkify: true },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const md = new MarkdownIt({
|
|
15
|
+
html: false,
|
|
16
|
+
breaks: props.breaks,
|
|
17
|
+
linkify: props.linkify,
|
|
18
|
+
typographer: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
md.renderer.rules.link_open = (tokens, idx, options, _env, self) => {
|
|
22
|
+
const token = tokens[idx]
|
|
23
|
+
if (token) {
|
|
24
|
+
token.attrSet('target', '_blank')
|
|
25
|
+
token.attrSet('rel', 'noopener noreferrer')
|
|
26
|
+
}
|
|
27
|
+
return self.renderToken(tokens, idx, options)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rendered = computed(() => md.render(props.source))
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="m3-markdown text-body-large text-on-surface" v-html="rendered" />
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.m3-markdown :deep(h1) { font-size: var(--text-headline-large); line-height: var(--text-headline-large--line-height); font-weight: 600; margin: 1em 0 0.5em; color: var(--color-on-surface); }
|
|
39
|
+
.m3-markdown :deep(h2) { font-size: var(--text-headline-medium); line-height: var(--text-headline-medium--line-height); font-weight: 600; margin: 1em 0 0.5em; color: var(--color-on-surface); }
|
|
40
|
+
.m3-markdown :deep(h3) { font-size: var(--text-headline-small); line-height: var(--text-headline-small--line-height); font-weight: 600; margin: 0.75em 0 0.25em; color: var(--color-on-surface); }
|
|
41
|
+
.m3-markdown :deep(h4) { font-size: var(--text-title-large); line-height: var(--text-title-large--line-height); font-weight: 600; margin: 0.75em 0 0.25em; color: var(--color-on-surface); }
|
|
42
|
+
|
|
43
|
+
.m3-markdown :deep(p) { margin: 0.5em 0; }
|
|
44
|
+
|
|
45
|
+
.m3-markdown :deep(a) {
|
|
46
|
+
color: var(--color-primary);
|
|
47
|
+
text-decoration: underline;
|
|
48
|
+
text-underline-offset: 2px;
|
|
49
|
+
}
|
|
50
|
+
.m3-markdown :deep(a:hover) { opacity: 0.8; }
|
|
51
|
+
|
|
52
|
+
.m3-markdown :deep(strong) { font-weight: 600; color: var(--color-on-surface); }
|
|
53
|
+
.m3-markdown :deep(em) { font-style: italic; }
|
|
54
|
+
|
|
55
|
+
.m3-markdown :deep(ul),
|
|
56
|
+
.m3-markdown :deep(ol) { padding-left: 1.5em; margin: 0.5em 0; }
|
|
57
|
+
.m3-markdown :deep(li) { margin: 0.25em 0; }
|
|
58
|
+
.m3-markdown :deep(li::marker) { color: var(--color-on-surface-variant); }
|
|
59
|
+
|
|
60
|
+
.m3-markdown :deep(blockquote) {
|
|
61
|
+
border-left: 3px solid var(--color-primary);
|
|
62
|
+
padding: 0.5em 1em;
|
|
63
|
+
margin: 0.75em 0;
|
|
64
|
+
background: var(--color-surface-container);
|
|
65
|
+
border-radius: 0 8px 8px 0;
|
|
66
|
+
color: var(--color-on-surface-variant);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.m3-markdown :deep(code) {
|
|
70
|
+
background: var(--color-surface-container-highest);
|
|
71
|
+
padding: 0.15em 0.4em;
|
|
72
|
+
border-radius: 4px;
|
|
73
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
74
|
+
font-size: 0.875em;
|
|
75
|
+
color: var(--color-primary);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.m3-markdown :deep(pre) {
|
|
79
|
+
background: var(--color-surface-container-highest);
|
|
80
|
+
padding: 1em;
|
|
81
|
+
border-radius: 12px;
|
|
82
|
+
overflow-x: auto;
|
|
83
|
+
margin: 0.75em 0;
|
|
84
|
+
border: 1px solid var(--color-outline-variant);
|
|
85
|
+
}
|
|
86
|
+
.m3-markdown :deep(pre code) {
|
|
87
|
+
background: none;
|
|
88
|
+
padding: 0;
|
|
89
|
+
color: var(--color-on-surface);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.m3-markdown :deep(hr) {
|
|
93
|
+
border: none;
|
|
94
|
+
border-top: 1px solid var(--color-outline-variant);
|
|
95
|
+
margin: 1.5em 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.m3-markdown :deep(table) {
|
|
99
|
+
width: 100%;
|
|
100
|
+
border-collapse: collapse;
|
|
101
|
+
margin: 0.75em 0;
|
|
102
|
+
}
|
|
103
|
+
.m3-markdown :deep(th) {
|
|
104
|
+
background: var(--color-surface-container);
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
text-align: left;
|
|
107
|
+
padding: 0.5em 0.75em;
|
|
108
|
+
border-bottom: 2px solid var(--color-outline-variant);
|
|
109
|
+
font-size: var(--text-label-large);
|
|
110
|
+
color: var(--color-on-surface);
|
|
111
|
+
}
|
|
112
|
+
.m3-markdown :deep(td) {
|
|
113
|
+
padding: 0.5em 0.75em;
|
|
114
|
+
border-bottom: 1px solid var(--color-outline-variant);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.m3-markdown :deep(img) {
|
|
118
|
+
max-width: 100%;
|
|
119
|
+
height: auto;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
margin: 0.5em 0;
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick, useSlots } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
cols?: number
|
|
7
|
+
smCols?: number
|
|
8
|
+
mdCols?: number
|
|
9
|
+
lgCols?: number
|
|
10
|
+
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
11
|
+
}>(),
|
|
12
|
+
{ cols: 2, gap: 'md' },
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const gapPx: Record<string, number> = {
|
|
16
|
+
none: 0,
|
|
17
|
+
xs: 4,
|
|
18
|
+
sm: 8,
|
|
19
|
+
md: 16,
|
|
20
|
+
lg: 24,
|
|
21
|
+
xl: 32,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
25
|
+
const activeCols = ref(props.cols)
|
|
26
|
+
|
|
27
|
+
function getActiveCols() {
|
|
28
|
+
const w = window.innerWidth
|
|
29
|
+
if (props.lgCols && w >= 1024) return props.lgCols
|
|
30
|
+
if (props.mdCols && w >= 768) return props.mdCols
|
|
31
|
+
if (props.smCols && w >= 640) return props.smCols
|
|
32
|
+
return props.cols
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const slots = useSlots()
|
|
36
|
+
|
|
37
|
+
function layout() {
|
|
38
|
+
activeCols.value = getActiveCols()
|
|
39
|
+
const container = containerRef.value
|
|
40
|
+
if (!container) return
|
|
41
|
+
|
|
42
|
+
const gap = gapPx[props.gap] ?? 0
|
|
43
|
+
const cols = activeCols.value
|
|
44
|
+
const children = Array.from(container.children) as HTMLElement[]
|
|
45
|
+
|
|
46
|
+
const colWidth = (container.clientWidth - gap * (cols - 1)) / cols
|
|
47
|
+
const colHeights = new Array<number>(cols).fill(0)
|
|
48
|
+
|
|
49
|
+
for (const child of children) {
|
|
50
|
+
const shortest = colHeights.indexOf(Math.min(...colHeights))
|
|
51
|
+
const x = shortest * (colWidth + gap)
|
|
52
|
+
const y = colHeights[shortest] ?? 0
|
|
53
|
+
|
|
54
|
+
child.style.position = 'absolute'
|
|
55
|
+
child.style.left = `${x}px`
|
|
56
|
+
child.style.top = `${y}px`
|
|
57
|
+
child.style.width = `${colWidth}px`
|
|
58
|
+
|
|
59
|
+
colHeights[shortest] = (colHeights[shortest] ?? 0) + child.offsetHeight + gap
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
container.style.height = `${Math.max(...colHeights) - gap}px`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let resizeObserver: ResizeObserver | null = null
|
|
66
|
+
|
|
67
|
+
onMounted(() => {
|
|
68
|
+
nextTick(layout)
|
|
69
|
+
resizeObserver = new ResizeObserver(layout)
|
|
70
|
+
if (containerRef.value) resizeObserver.observe(containerRef.value)
|
|
71
|
+
window.addEventListener('resize', layout)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
onBeforeUnmount(() => {
|
|
75
|
+
resizeObserver?.disconnect()
|
|
76
|
+
window.removeEventListener('resize', layout)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
watch(() => [props.cols, props.smCols, props.mdCols, props.lgCols, props.gap], () => nextTick(layout))
|
|
80
|
+
watch(() => slots.default?.(), () => nextTick(layout), { flush: 'post' })
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<template>
|
|
84
|
+
<div ref="containerRef" class="relative w-full">
|
|
85
|
+
<slot />
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
/** Which edge of the trigger the dropdown aligns to. */
|
|
7
|
+
align?: 'left' | 'right'
|
|
8
|
+
}>(),
|
|
9
|
+
{ align: 'right' },
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const open = ref(false)
|
|
13
|
+
const triggerEl = ref<HTMLElement | null>(null)
|
|
14
|
+
const dropdownEl = ref<HTMLElement | null>(null)
|
|
15
|
+
const dropStyle = ref<Record<string, string>>({})
|
|
16
|
+
|
|
17
|
+
function computePos() {
|
|
18
|
+
if (!triggerEl.value) return
|
|
19
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
20
|
+
const spaceBelow = window.innerHeight - rect.bottom - 8
|
|
21
|
+
const openAbove = spaceBelow < 200 && rect.top > spaceBelow
|
|
22
|
+
|
|
23
|
+
const style: Record<string, string> = {
|
|
24
|
+
maxHeight: `${Math.min(openAbove ? rect.top - 12 : spaceBelow, 400)}px`,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (openAbove) {
|
|
28
|
+
style.bottom = `${window.innerHeight - rect.top + 4}px`
|
|
29
|
+
} else {
|
|
30
|
+
style.top = `${rect.bottom + 4}px`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (props.align === 'right') {
|
|
34
|
+
style.right = `${window.innerWidth - rect.right}px`
|
|
35
|
+
} else {
|
|
36
|
+
style.left = `${rect.left}px`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dropStyle.value = style
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toggle() {
|
|
43
|
+
if (!open.value) computePos()
|
|
44
|
+
open.value = !open.value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function close() {
|
|
48
|
+
open.value = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
defineExpose({ close, open })
|
|
52
|
+
|
|
53
|
+
function onOutsideClick(e: MouseEvent) {
|
|
54
|
+
const t = e.target as Node
|
|
55
|
+
if (!triggerEl.value?.contains(t) && !dropdownEl.value?.contains(t)) close()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onScroll(e: Event) {
|
|
59
|
+
if (!open.value) return
|
|
60
|
+
if (dropdownEl.value?.contains(e.target as Node)) return
|
|
61
|
+
if (!triggerEl.value) return
|
|
62
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
63
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) { close(); return }
|
|
64
|
+
computePos()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onKeydown(e: KeyboardEvent) {
|
|
68
|
+
if (e.key === 'Escape') close()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onMounted(() => {
|
|
72
|
+
document.addEventListener('mousedown', onOutsideClick)
|
|
73
|
+
document.addEventListener('keydown', onKeydown)
|
|
74
|
+
window.addEventListener('scroll', onScroll, true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
onUnmounted(() => {
|
|
78
|
+
document.removeEventListener('mousedown', onOutsideClick)
|
|
79
|
+
document.removeEventListener('keydown', onKeydown)
|
|
80
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const origin = computed(() =>
|
|
84
|
+
props.align === 'right' ? 'top right' : 'top left',
|
|
85
|
+
)
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<div ref="triggerEl" class="inline-block" @click="toggle">
|
|
90
|
+
<slot name="trigger" :open="open" />
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<Teleport to="body">
|
|
94
|
+
<Transition
|
|
95
|
+
enter-active-class="transition-[opacity,transform] duration-100 ease-out"
|
|
96
|
+
enter-from-class="opacity-0 scale-95"
|
|
97
|
+
enter-to-class="opacity-100 scale-100"
|
|
98
|
+
leave-active-class="transition-[opacity,transform] duration-75 ease-in"
|
|
99
|
+
leave-from-class="opacity-100 scale-100"
|
|
100
|
+
leave-to-class="opacity-0 scale-95"
|
|
101
|
+
>
|
|
102
|
+
<div
|
|
103
|
+
v-if="open"
|
|
104
|
+
ref="dropdownEl"
|
|
105
|
+
class="fixed z-[500] min-w-48 overflow-y-auto overflow-x-hidden rounded-xs bg-surface-container py-1 shadow-elevation-2"
|
|
106
|
+
:style="{ ...dropStyle, transformOrigin: origin }"
|
|
107
|
+
@click="close"
|
|
108
|
+
>
|
|
109
|
+
<slot />
|
|
110
|
+
</div>
|
|
111
|
+
</Transition>
|
|
112
|
+
</Teleport>
|
|
113
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MIcon from './MIcon.vue'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<{ icon?: string }>(), {})
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<button
|
|
9
|
+
type="button"
|
|
10
|
+
class="flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left text-body-large text-on-surface hover:bg-on-surface/8"
|
|
11
|
+
>
|
|
12
|
+
<MIcon v-if="icon" :name="icon" :size="20" class="text-on-surface-variant" />
|
|
13
|
+
<slot />
|
|
14
|
+
</button>
|
|
15
|
+
</template>
|