@m3ui-vue/m3ui-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,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>