@m3ui-vue/m3ui-vue 0.1.0 → 0.1.2
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/README.md +80 -23
- package/dist/MIcon-CaEooCmZ.js +20 -0
- package/dist/MIcon-CaEooCmZ.js.map +1 -0
- package/dist/_plugin-vue_export-helper-B3ysoDQm.js +8 -0
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +141 -0
- package/dist/chart.js.map +1 -0
- package/dist/code-editor.d.ts +2 -0
- package/dist/code-editor.js +379 -0
- package/dist/code-editor.js.map +1 -0
- package/dist/components/MButton.vue.d.ts +1 -1
- package/dist/components/MCalendar.vue.d.ts +1 -1
- package/dist/components/MChart.vue.d.ts +2 -3
- package/dist/components/MCodeEditor.vue.d.ts +3 -1
- package/dist/components/MContainer.vue.d.ts +1 -1
- package/dist/components/MDataTable.vue.d.ts +1 -1
- package/dist/components/MFab.vue.d.ts +0 -2
- package/dist/components/MIconButton.vue.d.ts +1 -1
- package/dist/components/MMultiSelect.vue.d.ts +1 -1
- package/dist/components/MProgressBar.vue.d.ts +1 -1
- package/dist/components/MRichTextEditor.vue.d.ts +1 -1
- package/dist/components/MScheduler.vue.d.ts +1 -1
- package/dist/components/MSelect.vue.d.ts +1 -1
- package/dist/components/MSkeleton.vue.d.ts +1 -1
- package/dist/components/MSpotlightSearch.vue.d.ts +1 -1
- package/dist/components/MStack.vue.d.ts +2 -2
- package/dist/components/MTerminal.vue.d.ts +6 -6
- package/dist/components/MTextField.vue.d.ts +1 -1
- package/dist/components/MTooltip.vue.d.ts +1 -1
- package/dist/dist-Dsrzt6J5.js +1192 -0
- package/dist/dist-Dsrzt6J5.js.map +1 -0
- package/dist/index.d.ts +0 -6
- package/dist/m3ui-vue.css +2 -0
- package/dist/m3ui.js +2738 -3367
- package/dist/m3ui.js.map +1 -1
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +41 -0
- package/dist/markdown.js.map +1 -0
- package/dist/rich-text-editor.d.ts +1 -0
- package/dist/rich-text-editor.js +215 -0
- package/dist/rich-text-editor.js.map +1 -0
- package/dist/styles/theme.css +3 -0
- package/dist/styles.css +2 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +97 -0
- package/dist/terminal.js.map +1 -0
- package/package.json +28 -2
- package/src/chart.ts +1 -0
- package/src/code-editor.ts +2 -0
- package/src/components/MAlert.vue +1 -1
- package/src/components/MChart.vue +54 -47
- package/src/components/MCodeEditor.vue +149 -44
- package/src/components/MFab.vue +64 -48
- package/src/components/MMarkdown.vue +24 -17
- package/src/components/MMultiSelect.vue +3 -2
- package/src/components/MRichTextEditor.vue +101 -67
- package/src/components/MTerminal.vue +10 -8
- package/src/components/MTooltip.vue +8 -1
- package/src/index.ts +6 -6
- package/src/markdown.ts +1 -0
- package/src/rich-text-editor.ts +1 -0
- package/src/styles/theme.css +3 -0
- package/src/terminal.ts +1 -0
- package/dist/m3ui.css +0 -2
|
@@ -1,49 +1,22 @@
|
|
|
1
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
|
-
)
|
|
2
|
+
import { computed, ref, watch, onMounted, onBeforeUnmount, shallowRef, type Component } from 'vue'
|
|
34
3
|
|
|
35
4
|
type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'radar'
|
|
36
5
|
|
|
37
6
|
const props = withDefaults(
|
|
38
7
|
defineProps<{
|
|
39
8
|
type: ChartType
|
|
40
|
-
data:
|
|
41
|
-
options?:
|
|
9
|
+
data: Record<string, any>
|
|
10
|
+
options?: Record<string, any>
|
|
42
11
|
height?: string
|
|
43
12
|
}>(),
|
|
44
13
|
{ height: '300px' },
|
|
45
14
|
)
|
|
46
15
|
|
|
16
|
+
const ready = ref(false)
|
|
17
|
+
const chartComponent = shallowRef<Component | null>(null)
|
|
18
|
+
const componentMap = shallowRef<Record<string, Component>>({})
|
|
19
|
+
|
|
47
20
|
function getM3Colors() {
|
|
48
21
|
const style = getComputedStyle(document.documentElement)
|
|
49
22
|
const get = (v: string) => style.getPropertyValue(v).trim()
|
|
@@ -59,19 +32,57 @@ function getM3Colors() {
|
|
|
59
32
|
|
|
60
33
|
const m3Colors = ref(getM3Colors())
|
|
61
34
|
|
|
62
|
-
|
|
35
|
+
let themeObserver: MutationObserver | null = null
|
|
36
|
+
|
|
37
|
+
onMounted(async () => {
|
|
38
|
+
m3Colors.value = getM3Colors()
|
|
39
|
+
|
|
40
|
+
themeObserver = new MutationObserver(() => { m3Colors.value = getM3Colors() })
|
|
41
|
+
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
63
42
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
const [chartjs, vueChartjs] = await Promise.all([
|
|
44
|
+
import('chart.js'),
|
|
45
|
+
import('vue-chartjs'),
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
chartjs.Chart.register(
|
|
49
|
+
chartjs.CategoryScale,
|
|
50
|
+
chartjs.LinearScale,
|
|
51
|
+
chartjs.PointElement,
|
|
52
|
+
chartjs.LineElement,
|
|
53
|
+
chartjs.BarElement,
|
|
54
|
+
chartjs.ArcElement,
|
|
55
|
+
chartjs.RadialLinearScale,
|
|
56
|
+
chartjs.Filler,
|
|
57
|
+
chartjs.Tooltip,
|
|
58
|
+
chartjs.Legend,
|
|
59
|
+
chartjs.Title,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
componentMap.value = {
|
|
63
|
+
line: vueChartjs.Line,
|
|
64
|
+
bar: vueChartjs.Bar,
|
|
65
|
+
pie: vueChartjs.Pie,
|
|
66
|
+
doughnut: vueChartjs.Doughnut,
|
|
67
|
+
radar: vueChartjs.Radar,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ready.value = true
|
|
68
71
|
})
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
onBeforeUnmount(() => { themeObserver?.disconnect() })
|
|
74
|
+
|
|
75
|
+
watch(() => props.type, () => {
|
|
76
|
+
if (ready.value) chartComponent.value = componentMap.value[props.type] ?? null
|
|
77
|
+
}, { immediate: true })
|
|
78
|
+
|
|
79
|
+
watch(ready, () => {
|
|
80
|
+
chartComponent.value = componentMap.value[props.type] ?? null
|
|
81
|
+
})
|
|
71
82
|
|
|
72
|
-
const mergedOptions = computed<
|
|
83
|
+
const mergedOptions = computed<Record<string, any>>(() => {
|
|
73
84
|
const c = m3Colors.value
|
|
74
|
-
const base:
|
|
85
|
+
const base: Record<string, any> = {
|
|
75
86
|
responsive: true,
|
|
76
87
|
maintainAspectRatio: false,
|
|
77
88
|
plugins: {
|
|
@@ -140,16 +151,12 @@ function deepMerge(target: any, source: any): any {
|
|
|
140
151
|
}
|
|
141
152
|
return output
|
|
142
153
|
}
|
|
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
154
|
</script>
|
|
149
155
|
|
|
150
156
|
<template>
|
|
151
157
|
<div class="rounded-lg border border-outline-variant bg-surface p-4" :style="{ height }">
|
|
152
158
|
<component
|
|
159
|
+
v-if="ready && chartComponent"
|
|
153
160
|
:is="chartComponent"
|
|
154
161
|
:data="data"
|
|
155
162
|
:options="mergedOptions"
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldGutter, indentOnInput } from '@codemirror/language'
|
|
7
|
-
import { javascript } from '@codemirror/lang-javascript'
|
|
8
|
-
import { json } from '@codemirror/lang-json'
|
|
9
|
-
import { html } from '@codemirror/lang-html'
|
|
10
|
-
import { css } from '@codemirror/lang-css'
|
|
11
|
-
import { python } from '@codemirror/lang-python'
|
|
12
|
-
import { oneDark } from '@codemirror/theme-one-dark'
|
|
13
|
-
|
|
14
|
-
type Language = 'javascript' | 'typescript' | 'json' | 'html' | 'css' | 'python' | 'plain'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
type Language = 'javascript' | 'typescript' | 'json' | 'html' | 'css' | 'python' | 'vue' | 'plain'
|
|
15
6
|
|
|
16
7
|
const props = withDefaults(
|
|
17
8
|
defineProps<{
|
|
@@ -23,6 +14,7 @@ const props = withDefaults(
|
|
|
23
14
|
minHeight?: string
|
|
24
15
|
maxHeight?: string
|
|
25
16
|
placeholder?: string
|
|
17
|
+
wrap?: boolean
|
|
26
18
|
}>(),
|
|
27
19
|
{
|
|
28
20
|
language: 'javascript',
|
|
@@ -31,13 +23,22 @@ const props = withDefaults(
|
|
|
31
23
|
theme: 'light',
|
|
32
24
|
minHeight: '200px',
|
|
33
25
|
maxHeight: '600px',
|
|
26
|
+
wrap: true,
|
|
34
27
|
},
|
|
35
28
|
)
|
|
36
29
|
|
|
37
30
|
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
38
31
|
|
|
39
32
|
const containerRef = ref<HTMLElement | null>(null)
|
|
40
|
-
let view:
|
|
33
|
+
let view: any = null
|
|
34
|
+
let cmModules: any = null
|
|
35
|
+
const copied = ref(false)
|
|
36
|
+
|
|
37
|
+
async function copyCode() {
|
|
38
|
+
await navigator.clipboard.writeText(props.modelValue)
|
|
39
|
+
copied.value = true
|
|
40
|
+
setTimeout(() => { copied.value = false }, 1500)
|
|
41
|
+
}
|
|
41
42
|
|
|
42
43
|
const langLabel = computed(() => {
|
|
43
44
|
const labels: Record<Language, string> = {
|
|
@@ -47,54 +48,133 @@ const langLabel = computed(() => {
|
|
|
47
48
|
html: 'HTML',
|
|
48
49
|
css: 'CSS',
|
|
49
50
|
python: 'Python',
|
|
51
|
+
vue: 'Vue',
|
|
50
52
|
plain: 'Texto',
|
|
51
53
|
}
|
|
52
54
|
return labels[props.language]
|
|
53
55
|
})
|
|
54
56
|
|
|
55
|
-
function
|
|
57
|
+
async function loadModules() {
|
|
58
|
+
if (cmModules) return cmModules
|
|
59
|
+
|
|
60
|
+
const [viewMod, stateMod, commandsMod, languageMod, highlightMod, oneDarkMod, jsMod, jsonMod, htmlMod, cssMod, pyMod, vueMod] = await Promise.all([
|
|
61
|
+
import('@codemirror/view'),
|
|
62
|
+
import('@codemirror/state'),
|
|
63
|
+
import('@codemirror/commands'),
|
|
64
|
+
import('@codemirror/language'),
|
|
65
|
+
import('@lezer/highlight'),
|
|
66
|
+
import('@codemirror/theme-one-dark'),
|
|
67
|
+
import('@codemirror/lang-javascript'),
|
|
68
|
+
import('@codemirror/lang-json'),
|
|
69
|
+
import('@codemirror/lang-html'),
|
|
70
|
+
import('@codemirror/lang-css'),
|
|
71
|
+
import('@codemirror/lang-python'),
|
|
72
|
+
import('@codemirror/lang-vue'),
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
cmModules = { viewMod, stateMod, commandsMod, languageMod, highlightMod, oneDarkMod, jsMod, jsonMod, htmlMod, cssMod, pyMod, vueMod }
|
|
76
|
+
return cmModules
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildM3HighlightStyle(languageMod: any, tags: any) {
|
|
80
|
+
return languageMod.HighlightStyle.define([
|
|
81
|
+
{ tag: tags.keyword, color: 'var(--color-primary)' },
|
|
82
|
+
{ tag: tags.controlKeyword, color: 'var(--color-primary)', fontWeight: '500' },
|
|
83
|
+
{ tag: tags.operatorKeyword, color: 'var(--color-primary)' },
|
|
84
|
+
{ tag: tags.definitionKeyword, color: 'var(--color-primary)' },
|
|
85
|
+
{ tag: tags.moduleKeyword, color: 'var(--color-primary)' },
|
|
86
|
+
|
|
87
|
+
{ tag: tags.string, color: 'var(--color-tertiary)' },
|
|
88
|
+
{ tag: tags.regexp, color: 'var(--color-tertiary)' },
|
|
89
|
+
|
|
90
|
+
{ tag: tags.number, color: 'var(--color-error)' },
|
|
91
|
+
{ tag: tags.bool, color: 'var(--color-error)' },
|
|
92
|
+
|
|
93
|
+
{ tag: tags.function(tags.variableName), color: 'var(--color-secondary)' },
|
|
94
|
+
{ tag: tags.function(tags.definition(tags.variableName)), color: 'var(--color-secondary)', fontWeight: '500' },
|
|
95
|
+
|
|
96
|
+
{ tag: tags.typeName, color: 'var(--color-primary)', fontStyle: 'italic' },
|
|
97
|
+
{ tag: tags.className, color: 'var(--color-primary)', fontStyle: 'italic' },
|
|
98
|
+
{ tag: tags.namespace, color: 'var(--color-on-surface-variant)' },
|
|
99
|
+
|
|
100
|
+
{ tag: tags.propertyName, color: 'var(--color-on-surface)' },
|
|
101
|
+
{ tag: tags.definition(tags.propertyName), color: 'var(--color-on-surface)' },
|
|
102
|
+
|
|
103
|
+
{ tag: tags.variableName, color: 'var(--color-on-surface)' },
|
|
104
|
+
{ tag: tags.definition(tags.variableName), color: 'var(--color-on-surface)' },
|
|
105
|
+
|
|
106
|
+
{ tag: tags.comment, color: 'var(--color-outline)', fontStyle: 'italic' },
|
|
107
|
+
{ tag: tags.lineComment, color: 'var(--color-outline)', fontStyle: 'italic' },
|
|
108
|
+
{ tag: tags.blockComment, color: 'var(--color-outline)', fontStyle: 'italic' },
|
|
109
|
+
|
|
110
|
+
{ tag: tags.meta, color: 'var(--color-on-surface-variant)' },
|
|
111
|
+
{ tag: tags.tagName, color: 'var(--color-primary)' },
|
|
112
|
+
{ tag: tags.attributeName, color: 'var(--color-tertiary)' },
|
|
113
|
+
{ tag: tags.attributeValue, color: 'var(--color-secondary)' },
|
|
114
|
+
|
|
115
|
+
{ tag: tags.atom, color: 'var(--color-error)' },
|
|
116
|
+
{ tag: tags.null, color: 'var(--color-error)' },
|
|
117
|
+
|
|
118
|
+
{ tag: tags.punctuation, color: 'var(--color-on-surface-variant)' },
|
|
119
|
+
{ tag: tags.bracket, color: 'var(--color-on-surface-variant)' },
|
|
120
|
+
{ tag: tags.operator, color: 'var(--color-on-surface-variant)' },
|
|
121
|
+
{ tag: tags.separator, color: 'var(--color-on-surface-variant)' },
|
|
122
|
+
])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getLangExtension(mods: any) {
|
|
56
126
|
switch (props.language) {
|
|
57
|
-
case 'javascript': return javascript()
|
|
58
|
-
case 'typescript': return javascript({ typescript: true })
|
|
59
|
-
case 'json': return json()
|
|
60
|
-
case 'html': return html()
|
|
61
|
-
case 'css': return css()
|
|
62
|
-
case 'python': return python()
|
|
127
|
+
case 'javascript': return mods.jsMod.javascript()
|
|
128
|
+
case 'typescript': return mods.jsMod.javascript({ typescript: true })
|
|
129
|
+
case 'json': return mods.jsonMod.json()
|
|
130
|
+
case 'html': return mods.htmlMod.html()
|
|
131
|
+
case 'css': return mods.cssMod.css()
|
|
132
|
+
case 'python': return mods.pyMod.python()
|
|
133
|
+
case 'vue': return mods.vueMod.vue()
|
|
63
134
|
default: return []
|
|
64
135
|
}
|
|
65
136
|
}
|
|
66
137
|
|
|
67
|
-
function buildExtensions() {
|
|
138
|
+
function buildExtensions(mods: any) {
|
|
139
|
+
const { viewMod, stateMod, commandsMod, languageMod, highlightMod, oneDarkMod } = mods
|
|
140
|
+
|
|
141
|
+
const m3Style = buildM3HighlightStyle(languageMod, highlightMod.tags)
|
|
142
|
+
|
|
68
143
|
const exts = [
|
|
69
|
-
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
70
|
-
history(),
|
|
71
|
-
bracketMatching(),
|
|
72
|
-
indentOnInput(),
|
|
73
|
-
foldGutter(),
|
|
74
|
-
highlightActiveLine(),
|
|
75
|
-
highlightActiveLineGutter(),
|
|
76
|
-
syntaxHighlighting(
|
|
77
|
-
|
|
78
|
-
|
|
144
|
+
viewMod.keymap.of([...commandsMod.defaultKeymap, ...commandsMod.historyKeymap, commandsMod.indentWithTab]),
|
|
145
|
+
commandsMod.history(),
|
|
146
|
+
languageMod.bracketMatching(),
|
|
147
|
+
languageMod.indentOnInput(),
|
|
148
|
+
languageMod.foldGutter(),
|
|
149
|
+
viewMod.highlightActiveLine(),
|
|
150
|
+
viewMod.highlightActiveLineGutter(),
|
|
151
|
+
languageMod.syntaxHighlighting(m3Style),
|
|
152
|
+
languageMod.syntaxHighlighting(languageMod.defaultHighlightStyle, { fallback: true }),
|
|
153
|
+
getLangExtension(mods),
|
|
154
|
+
viewMod.EditorView.updateListener.of((update: any) => {
|
|
79
155
|
if (update.docChanged) emit('update:modelValue', update.state.doc.toString())
|
|
80
156
|
}),
|
|
81
|
-
EditorState.readOnly.of(props.readonly),
|
|
157
|
+
stateMod.EditorState.readOnly.of(props.readonly),
|
|
82
158
|
]
|
|
83
159
|
|
|
84
|
-
if (props.
|
|
85
|
-
if (props.
|
|
160
|
+
if (props.wrap) exts.push(viewMod.EditorView.lineWrapping)
|
|
161
|
+
if (props.lineNumbers) exts.push(viewMod.lineNumbers())
|
|
162
|
+
if (props.theme === 'dark') exts.push(oneDarkMod.oneDark)
|
|
86
163
|
|
|
87
164
|
return exts
|
|
88
165
|
}
|
|
89
166
|
|
|
90
|
-
function createEditor() {
|
|
167
|
+
async function createEditor() {
|
|
91
168
|
if (!containerRef.value) return
|
|
169
|
+
const mods = await loadModules()
|
|
170
|
+
const { viewMod, stateMod } = mods
|
|
171
|
+
|
|
92
172
|
view?.destroy()
|
|
93
173
|
|
|
94
|
-
view = new EditorView({
|
|
95
|
-
state: EditorState.create({
|
|
174
|
+
view = new viewMod.EditorView({
|
|
175
|
+
state: stateMod.EditorState.create({
|
|
96
176
|
doc: props.modelValue,
|
|
97
|
-
extensions: buildExtensions(),
|
|
177
|
+
extensions: buildExtensions(mods),
|
|
98
178
|
}),
|
|
99
179
|
parent: containerRef.value,
|
|
100
180
|
})
|
|
@@ -108,7 +188,7 @@ watch(() => props.modelValue, (val) => {
|
|
|
108
188
|
}
|
|
109
189
|
})
|
|
110
190
|
|
|
111
|
-
watch([() => props.language, () => props.theme, () => props.readonly, () => props.lineNumbers], createEditor)
|
|
191
|
+
watch([() => props.language, () => props.theme, () => props.readonly, () => props.lineNumbers, () => props.wrap], createEditor)
|
|
112
192
|
|
|
113
193
|
onBeforeUnmount(() => view?.destroy())
|
|
114
194
|
</script>
|
|
@@ -118,13 +198,24 @@ onBeforeUnmount(() => view?.destroy())
|
|
|
118
198
|
<!-- Header bar -->
|
|
119
199
|
<div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-2">
|
|
120
200
|
<span class="text-label-medium text-on-surface-variant">{{ langLabel }}</span>
|
|
121
|
-
<
|
|
201
|
+
<div class="flex items-center gap-2">
|
|
202
|
+
<slot name="actions" />
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
class="flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2 text-label-medium text-on-surface-variant transition-colors hover:bg-on-surface/8"
|
|
206
|
+
:title="copied ? 'Copied!' : 'Copy code'"
|
|
207
|
+
@click="copyCode"
|
|
208
|
+
>
|
|
209
|
+
<MIcon :name="copied ? 'check' : 'content_copy'" :size="16" :class="copied ? 'text-primary' : ''" />
|
|
210
|
+
<span v-if="copied" class="text-primary">Copied</span>
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
122
213
|
</div>
|
|
123
214
|
|
|
124
215
|
<!-- Editor -->
|
|
125
216
|
<div
|
|
126
217
|
ref="containerRef"
|
|
127
|
-
class="code-editor-container overflow-auto bg-surface"
|
|
218
|
+
class="code-editor-container overflow-auto bg-surface text-on-surface"
|
|
128
219
|
:style="{ minHeight, maxHeight }"
|
|
129
220
|
/>
|
|
130
221
|
</div>
|
|
@@ -134,8 +225,9 @@ onBeforeUnmount(() => view?.destroy())
|
|
|
134
225
|
.code-editor-container :deep(.cm-editor) {
|
|
135
226
|
height: 100%;
|
|
136
227
|
min-height: inherit;
|
|
137
|
-
font-family: '
|
|
138
|
-
font-size: 0.
|
|
228
|
+
font-family: 'Roboto Mono', 'Fira Code', 'Consolas', monospace;
|
|
229
|
+
font-size: 0.8125rem;
|
|
230
|
+
line-height: 1.6;
|
|
139
231
|
}
|
|
140
232
|
|
|
141
233
|
.code-editor-container :deep(.cm-editor.cm-focused) {
|
|
@@ -146,14 +238,25 @@ onBeforeUnmount(() => view?.destroy())
|
|
|
146
238
|
min-height: inherit;
|
|
147
239
|
}
|
|
148
240
|
|
|
241
|
+
.code-editor-container :deep(.cm-content) {
|
|
242
|
+
padding: 12px 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.code-editor-container :deep(.cm-line) {
|
|
246
|
+
padding: 0 16px;
|
|
247
|
+
}
|
|
248
|
+
|
|
149
249
|
.code-editor-container :deep(.cm-gutters) {
|
|
150
250
|
background: var(--color-surface-container);
|
|
151
251
|
border-right: 1px solid var(--color-outline-variant);
|
|
152
|
-
color: var(--color-
|
|
252
|
+
color: var(--color-outline);
|
|
253
|
+
font-size: 0.75rem;
|
|
254
|
+
padding: 0 4px;
|
|
153
255
|
}
|
|
154
256
|
|
|
155
257
|
.code-editor-container :deep(.cm-activeLineGutter) {
|
|
156
258
|
background: var(--color-surface-container-high);
|
|
259
|
+
color: var(--color-on-surface-variant);
|
|
157
260
|
}
|
|
158
261
|
|
|
159
262
|
.code-editor-container :deep(.cm-activeLine) {
|
|
@@ -166,11 +269,13 @@ onBeforeUnmount(() => view?.destroy())
|
|
|
166
269
|
|
|
167
270
|
.code-editor-container :deep(.cm-cursor) {
|
|
168
271
|
border-left-color: var(--color-primary);
|
|
272
|
+
border-left-width: 2px;
|
|
169
273
|
}
|
|
170
274
|
|
|
171
275
|
.code-editor-container :deep(.cm-matchingBracket) {
|
|
172
276
|
background: var(--color-tertiary-container);
|
|
173
277
|
color: var(--color-on-tertiary-container);
|
|
278
|
+
border-radius: 2px;
|
|
174
279
|
}
|
|
175
280
|
|
|
176
281
|
.code-editor-container :deep(.cm-foldGutter span) {
|
package/src/components/MFab.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue'
|
|
3
3
|
import MIcon from './MIcon.vue'
|
|
4
4
|
|
|
5
5
|
export interface SpeedDialItem {
|
|
@@ -15,9 +15,7 @@ const props = withDefaults(
|
|
|
15
15
|
color?: 'primary' | 'secondary' | 'tertiary' | 'surface'
|
|
16
16
|
size?: 'small' | 'regular' | 'large'
|
|
17
17
|
disabled?: boolean
|
|
18
|
-
/** Speed-dial child items. If provided, clicking the FAB toggles them instead of emitting click. */
|
|
19
18
|
items?: SpeedDialItem[]
|
|
20
|
-
/** Direction the speed-dial items expand toward. */
|
|
21
19
|
direction?: 'up' | 'down' | 'left' | 'right' | 'radial'
|
|
22
20
|
}>(),
|
|
23
21
|
{
|
|
@@ -31,7 +29,7 @@ const props = withDefaults(
|
|
|
31
29
|
const emit = defineEmits<{ click: [MouseEvent] }>()
|
|
32
30
|
|
|
33
31
|
const open = ref(false)
|
|
34
|
-
const
|
|
32
|
+
const fabEl = ref<HTMLElement>()
|
|
35
33
|
|
|
36
34
|
const hasItems = computed(() => !!props.items?.length)
|
|
37
35
|
|
|
@@ -60,7 +58,6 @@ const fabIconSize = computed(() => {
|
|
|
60
58
|
}
|
|
61
59
|
})
|
|
62
60
|
|
|
63
|
-
// FAB height in px — used to position items relative to the container
|
|
64
61
|
const fabPx = computed(() => {
|
|
65
62
|
if (props.label) return 56
|
|
66
63
|
switch (props.size) {
|
|
@@ -70,17 +67,24 @@ const fabPx = computed(() => {
|
|
|
70
67
|
}
|
|
71
68
|
})
|
|
72
69
|
|
|
73
|
-
// Item size (always small-FAB-sized): 40px
|
|
74
70
|
const ITEM_PX = 40
|
|
75
71
|
const ITEM_GAP = 8
|
|
76
72
|
|
|
73
|
+
function getRect(): DOMRect | null {
|
|
74
|
+
return fabEl.value?.getBoundingClientRect() ?? null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
77
|
function itemStyle(index: number): Record<string, string> {
|
|
78
|
+
const rect = getRect()
|
|
79
|
+
if (!rect) return { position: 'fixed', opacity: '0', pointerEvents: 'none' }
|
|
80
|
+
|
|
81
|
+
const cx = rect.left + rect.width / 2
|
|
82
|
+
const cy = rect.top + rect.height / 2
|
|
78
83
|
const count = props.items?.length ?? 0
|
|
79
|
-
|
|
84
|
+
|
|
80
85
|
const delay = open.value
|
|
81
86
|
? `${index * 35}ms`
|
|
82
87
|
: `${(count - 1 - index) * 35}ms`
|
|
83
|
-
|
|
84
88
|
const transition = `transform 220ms cubic-bezier(0.2,0,0,1) ${delay}, opacity 180ms ease ${delay}`
|
|
85
89
|
|
|
86
90
|
if (props.direction === 'radial') {
|
|
@@ -89,27 +93,25 @@ function itemStyle(index: number): Record<string, string> {
|
|
|
89
93
|
const dx = (Math.cos(angle) * r).toFixed(1)
|
|
90
94
|
const dy = (Math.sin(angle) * r).toFixed(1)
|
|
91
95
|
return {
|
|
92
|
-
position: '
|
|
93
|
-
top:
|
|
94
|
-
left:
|
|
95
|
-
marginTop: `${-ITEM_PX / 2}px`,
|
|
96
|
-
marginLeft: `${-ITEM_PX / 2}px`,
|
|
96
|
+
position: 'fixed',
|
|
97
|
+
top: `${cy - ITEM_PX / 2}px`,
|
|
98
|
+
left: `${cx - ITEM_PX / 2}px`,
|
|
97
99
|
transform: open.value ? `translate(${dx}px, ${dy}px) scale(1)` : 'translate(0,0) scale(0)',
|
|
98
100
|
opacity: open.value ? '1' : '0',
|
|
99
101
|
transition,
|
|
100
102
|
pointerEvents: open.value ? 'auto' : 'none',
|
|
103
|
+
zIndex: '1000',
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
// Linear directions: offset from the container edge
|
|
105
107
|
const step = ITEM_PX + ITEM_GAP
|
|
106
|
-
const
|
|
108
|
+
const offset = fabPx.value / 2 + ITEM_GAP + ITEM_PX / 2 + index * step
|
|
107
109
|
|
|
108
|
-
const
|
|
109
|
-
up: {
|
|
110
|
-
down: { top:
|
|
111
|
-
left: {
|
|
112
|
-
right: {
|
|
110
|
+
const posMap: Record<string, { top: string; left: string }> = {
|
|
111
|
+
up: { top: `${cy - offset - ITEM_PX / 2}px`, left: `${cx - ITEM_PX / 2}px` },
|
|
112
|
+
down: { top: `${cy + offset - ITEM_PX / 2}px`, left: `${cx - ITEM_PX / 2}px` },
|
|
113
|
+
left: { top: `${cy - ITEM_PX / 2}px`, left: `${cx - offset - ITEM_PX / 2}px` },
|
|
114
|
+
right: { top: `${cy - ITEM_PX / 2}px`, left: `${cx + offset - ITEM_PX / 2}px` },
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
const translateFrom: Record<string, string> = {
|
|
@@ -119,19 +121,27 @@ function itemStyle(index: number): Record<string, string> {
|
|
|
119
121
|
right: 'translateX(-12px) scale(0.75)',
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
const pos = posMap[props.direction] ?? posMap.up
|
|
125
|
+
|
|
122
126
|
return {
|
|
123
|
-
position: '
|
|
124
|
-
...
|
|
127
|
+
position: 'fixed',
|
|
128
|
+
...pos,
|
|
125
129
|
transform: open.value ? 'translate(0,0) scale(1)' : (translateFrom[props.direction] ?? 'scale(0.75)'),
|
|
126
130
|
opacity: open.value ? '1' : '0',
|
|
127
131
|
transition,
|
|
128
132
|
pointerEvents: open.value ? 'auto' : 'none',
|
|
133
|
+
zIndex: '1000',
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
// Label only makes sense for up/down; placed to the left of the item button
|
|
133
137
|
const showLabel = computed(() => props.direction === 'up' || props.direction === 'down')
|
|
134
138
|
|
|
139
|
+
// Force re-render to recalculate positions on scroll
|
|
140
|
+
const scrollTick = ref(0)
|
|
141
|
+
function onScroll() {
|
|
142
|
+
if (open.value) scrollTick.value++
|
|
143
|
+
}
|
|
144
|
+
|
|
135
145
|
function createRipple(event: PointerEvent | MouseEvent, target?: HTMLElement) {
|
|
136
146
|
const button = (target ?? event.currentTarget) as HTMLElement
|
|
137
147
|
const rect = button.getBoundingClientRect()
|
|
@@ -159,19 +169,44 @@ function handleItemClick(e: PointerEvent, item: SpeedDialItem, buttonEl: HTMLEle
|
|
|
159
169
|
|
|
160
170
|
function onDocClick(e: MouseEvent) {
|
|
161
171
|
if (!open.value) return
|
|
162
|
-
if (
|
|
172
|
+
if (fabEl.value && !fabEl.value.contains(e.target as Node)) {
|
|
163
173
|
open.value = false
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
onMounted(() =>
|
|
168
|
-
|
|
177
|
+
onMounted(() => {
|
|
178
|
+
document.addEventListener('click', onDocClick, true)
|
|
179
|
+
window.addEventListener('scroll', onScroll, true)
|
|
180
|
+
})
|
|
181
|
+
onUnmounted(() => {
|
|
182
|
+
document.removeEventListener('click', onDocClick, true)
|
|
183
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
184
|
+
})
|
|
169
185
|
</script>
|
|
170
186
|
|
|
171
187
|
<template>
|
|
172
|
-
<div ref="
|
|
173
|
-
|
|
188
|
+
<div ref="fabEl" class="relative inline-flex items-center justify-center">
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
class="relative inline-flex cursor-pointer items-center justify-center overflow-hidden shadow-elevation-1 transition-shadow duration-150 hover:shadow-elevation-2 active:shadow-elevation-1 disabled:cursor-not-allowed disabled:opacity-[0.38] before:content-[''] before:pointer-events-none before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity before:duration-150 hover:before:opacity-[0.08] active:before:opacity-[0.12]"
|
|
192
|
+
:class="[colorMap[color], fabSizeClasses]"
|
|
193
|
+
:disabled="disabled"
|
|
194
|
+
@pointerdown="(e) => { createRipple(e); handleFabClick(e) }"
|
|
195
|
+
>
|
|
196
|
+
<MIcon
|
|
197
|
+
:name="icon"
|
|
198
|
+
:size="fabIconSize"
|
|
199
|
+
class="transition-transform duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)]"
|
|
200
|
+
:class="hasItems && open ? 'rotate-45' : ''"
|
|
201
|
+
/>
|
|
202
|
+
<span v-if="label" class="text-label-large font-medium">{{ label }}</span>
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<Teleport to="body">
|
|
174
207
|
<template v-if="hasItems">
|
|
208
|
+
<!-- hidden dep on scrollTick to force style recalc -->
|
|
209
|
+
<span :data-tick="scrollTick" class="hidden" />
|
|
175
210
|
<div
|
|
176
211
|
v-for="(item, i) in items"
|
|
177
212
|
:key="i"
|
|
@@ -179,7 +214,6 @@ onUnmounted(() => document.removeEventListener('click', onDocClick, true))
|
|
|
179
214
|
class="flex items-center gap-3"
|
|
180
215
|
:class="showLabel ? 'flex-row-reverse' : ''"
|
|
181
216
|
>
|
|
182
|
-
<!-- Label pill (up/down only) -->
|
|
183
217
|
<span
|
|
184
218
|
v-if="item.label && showLabel"
|
|
185
219
|
class="whitespace-nowrap rounded-md bg-surface-container-high px-3 py-1.5 text-label-medium text-on-surface shadow-elevation-1"
|
|
@@ -187,7 +221,6 @@ onUnmounted(() => document.removeEventListener('click', onDocClick, true))
|
|
|
187
221
|
{{ item.label }}
|
|
188
222
|
</span>
|
|
189
223
|
|
|
190
|
-
<!-- Mini FAB button -->
|
|
191
224
|
<button
|
|
192
225
|
type="button"
|
|
193
226
|
class="relative flex cursor-pointer items-center justify-center overflow-hidden rounded-lg shadow-elevation-1 transition-shadow duration-150 hover:shadow-elevation-2 active:shadow-elevation-1 before:content-[''] before:pointer-events-none before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity before:duration-150 hover:before:opacity-[0.08] active:before:opacity-[0.12]"
|
|
@@ -199,22 +232,5 @@ onUnmounted(() => document.removeEventListener('click', onDocClick, true))
|
|
|
199
232
|
</button>
|
|
200
233
|
</div>
|
|
201
234
|
</template>
|
|
202
|
-
|
|
203
|
-
<!-- Main FAB -->
|
|
204
|
-
<button
|
|
205
|
-
type="button"
|
|
206
|
-
class="relative inline-flex cursor-pointer items-center justify-center overflow-hidden shadow-elevation-1 transition-shadow duration-150 hover:shadow-elevation-2 active:shadow-elevation-1 disabled:cursor-not-allowed disabled:opacity-[0.38] before:content-[''] before:pointer-events-none before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity before:duration-150 hover:before:opacity-[0.08] active:before:opacity-[0.12]"
|
|
207
|
-
:class="[colorMap[color], fabSizeClasses]"
|
|
208
|
-
:disabled="disabled"
|
|
209
|
-
@pointerdown="(e) => { createRipple(e); handleFabClick(e) }"
|
|
210
|
-
>
|
|
211
|
-
<MIcon
|
|
212
|
-
:name="icon"
|
|
213
|
-
:size="fabIconSize"
|
|
214
|
-
class="transition-transform duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)]"
|
|
215
|
-
:class="hasItems && open ? 'rotate-45' : ''"
|
|
216
|
-
/>
|
|
217
|
-
<span v-if="label" class="text-label-large font-medium">{{ label }}</span>
|
|
218
|
-
</button>
|
|
219
|
-
</div>
|
|
235
|
+
</Teleport>
|
|
220
236
|
</template>
|