@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,179 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
|
|
3
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view'
|
|
4
|
+
import { EditorState } from '@codemirror/state'
|
|
5
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
|
|
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'
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
modelValue: string
|
|
19
|
+
language?: Language
|
|
20
|
+
readonly?: boolean
|
|
21
|
+
lineNumbers?: boolean
|
|
22
|
+
theme?: 'light' | 'dark'
|
|
23
|
+
minHeight?: string
|
|
24
|
+
maxHeight?: string
|
|
25
|
+
placeholder?: string
|
|
26
|
+
}>(),
|
|
27
|
+
{
|
|
28
|
+
language: 'javascript',
|
|
29
|
+
readonly: false,
|
|
30
|
+
lineNumbers: true,
|
|
31
|
+
theme: 'light',
|
|
32
|
+
minHeight: '200px',
|
|
33
|
+
maxHeight: '600px',
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
38
|
+
|
|
39
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
40
|
+
let view: EditorView | null = null
|
|
41
|
+
|
|
42
|
+
const langLabel = computed(() => {
|
|
43
|
+
const labels: Record<Language, string> = {
|
|
44
|
+
javascript: 'JavaScript',
|
|
45
|
+
typescript: 'TypeScript',
|
|
46
|
+
json: 'JSON',
|
|
47
|
+
html: 'HTML',
|
|
48
|
+
css: 'CSS',
|
|
49
|
+
python: 'Python',
|
|
50
|
+
plain: 'Texto',
|
|
51
|
+
}
|
|
52
|
+
return labels[props.language]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function getLangExtension() {
|
|
56
|
+
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()
|
|
63
|
+
default: return []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildExtensions() {
|
|
68
|
+
const exts = [
|
|
69
|
+
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
70
|
+
history(),
|
|
71
|
+
bracketMatching(),
|
|
72
|
+
indentOnInput(),
|
|
73
|
+
foldGutter(),
|
|
74
|
+
highlightActiveLine(),
|
|
75
|
+
highlightActiveLineGutter(),
|
|
76
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
77
|
+
getLangExtension(),
|
|
78
|
+
EditorView.updateListener.of((update) => {
|
|
79
|
+
if (update.docChanged) emit('update:modelValue', update.state.doc.toString())
|
|
80
|
+
}),
|
|
81
|
+
EditorState.readOnly.of(props.readonly),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
if (props.lineNumbers) exts.push(lineNumbers())
|
|
85
|
+
if (props.theme === 'dark') exts.push(oneDark)
|
|
86
|
+
|
|
87
|
+
return exts
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createEditor() {
|
|
91
|
+
if (!containerRef.value) return
|
|
92
|
+
view?.destroy()
|
|
93
|
+
|
|
94
|
+
view = new EditorView({
|
|
95
|
+
state: EditorState.create({
|
|
96
|
+
doc: props.modelValue,
|
|
97
|
+
extensions: buildExtensions(),
|
|
98
|
+
}),
|
|
99
|
+
parent: containerRef.value,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onMounted(createEditor)
|
|
104
|
+
|
|
105
|
+
watch(() => props.modelValue, (val) => {
|
|
106
|
+
if (view && view.state.doc.toString() !== val) {
|
|
107
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: val } })
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
watch([() => props.language, () => props.theme, () => props.readonly, () => props.lineNumbers], createEditor)
|
|
112
|
+
|
|
113
|
+
onBeforeUnmount(() => view?.destroy())
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="overflow-hidden rounded-lg border border-outline-variant">
|
|
118
|
+
<!-- Header bar -->
|
|
119
|
+
<div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-2">
|
|
120
|
+
<span class="text-label-medium text-on-surface-variant">{{ langLabel }}</span>
|
|
121
|
+
<slot name="actions" />
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Editor -->
|
|
125
|
+
<div
|
|
126
|
+
ref="containerRef"
|
|
127
|
+
class="code-editor-container overflow-auto bg-surface"
|
|
128
|
+
:style="{ minHeight, maxHeight }"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<style scoped>
|
|
134
|
+
.code-editor-container :deep(.cm-editor) {
|
|
135
|
+
height: 100%;
|
|
136
|
+
min-height: inherit;
|
|
137
|
+
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.code-editor-container :deep(.cm-editor.cm-focused) {
|
|
142
|
+
outline: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.code-editor-container :deep(.cm-scroller) {
|
|
146
|
+
min-height: inherit;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.code-editor-container :deep(.cm-gutters) {
|
|
150
|
+
background: var(--color-surface-container);
|
|
151
|
+
border-right: 1px solid var(--color-outline-variant);
|
|
152
|
+
color: var(--color-on-surface-variant);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.code-editor-container :deep(.cm-activeLineGutter) {
|
|
156
|
+
background: var(--color-surface-container-high);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.code-editor-container :deep(.cm-activeLine) {
|
|
160
|
+
background: var(--color-surface-container-lowest);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.code-editor-container :deep(.cm-selectionBackground) {
|
|
164
|
+
background: var(--color-primary-container) !important;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.code-editor-container :deep(.cm-cursor) {
|
|
168
|
+
border-left-color: var(--color-primary);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.code-editor-container :deep(.cm-matchingBracket) {
|
|
172
|
+
background: var(--color-tertiary-container);
|
|
173
|
+
color: var(--color-on-tertiary-container);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.code-editor-container :deep(.cm-foldGutter span) {
|
|
177
|
+
color: var(--color-on-surface-variant);
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
import { useFieldBg } from '../composables/useFieldBg'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
modelValue: string
|
|
8
|
+
label?: string
|
|
9
|
+
presets?: string[]
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
error?: string
|
|
12
|
+
hint?: string
|
|
13
|
+
fieldBg?: string
|
|
14
|
+
}>(), {
|
|
15
|
+
presets: () => [
|
|
16
|
+
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3',
|
|
17
|
+
'#03a9f4', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#cddc39',
|
|
18
|
+
'#ffeb3b', '#ffc107', '#ff9800', '#ff5722', '#795548', '#607d8b',
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
23
|
+
|
|
24
|
+
const open = ref(false)
|
|
25
|
+
const triggerEl = ref<HTMLElement | null>(null)
|
|
26
|
+
const panelEl = ref<HTMLElement | null>(null)
|
|
27
|
+
const dropPos = ref({ top: '0px', left: '0px' })
|
|
28
|
+
const { resolvedFieldBg } = useFieldBg(triggerEl, () => props.fieldBg)
|
|
29
|
+
|
|
30
|
+
const satBrightEl = ref<HTMLElement>()
|
|
31
|
+
const draggingSB = ref(false)
|
|
32
|
+
|
|
33
|
+
// HSV state
|
|
34
|
+
const hue = ref(0)
|
|
35
|
+
const sat = ref(100)
|
|
36
|
+
const bright = ref(100)
|
|
37
|
+
|
|
38
|
+
function hexToHsv(hex: string) {
|
|
39
|
+
let c = hex.replace('#', '')
|
|
40
|
+
if (c.length === 3) c = c[0]!+c[0]!+c[1]!+c[1]!+c[2]!+c[2]!
|
|
41
|
+
const r = parseInt(c.substring(0, 2), 16) / 255
|
|
42
|
+
const g = parseInt(c.substring(2, 4), 16) / 255
|
|
43
|
+
const b = parseInt(c.substring(4, 6), 16) / 255
|
|
44
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b)
|
|
45
|
+
const d = max - min
|
|
46
|
+
let h = 0
|
|
47
|
+
if (d !== 0) {
|
|
48
|
+
if (max === r) h = ((g - b) / d + 6) % 6
|
|
49
|
+
else if (max === g) h = (b - r) / d + 2
|
|
50
|
+
else h = (r - g) / d + 4
|
|
51
|
+
h *= 60
|
|
52
|
+
}
|
|
53
|
+
const s = max === 0 ? 0 : (d / max) * 100
|
|
54
|
+
const v = max * 100
|
|
55
|
+
return { h, s, v }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hsvToHex(h: number, s: number, v: number) {
|
|
59
|
+
s /= 100; v /= 100
|
|
60
|
+
const c = v * s
|
|
61
|
+
const x = c * (1 - Math.abs((h / 60) % 2 - 1))
|
|
62
|
+
const m = v - c
|
|
63
|
+
let r = 0, g = 0, b = 0
|
|
64
|
+
if (h < 60) { r = c; g = x }
|
|
65
|
+
else if (h < 120) { r = x; g = c }
|
|
66
|
+
else if (h < 180) { g = c; b = x }
|
|
67
|
+
else if (h < 240) { g = x; b = c }
|
|
68
|
+
else if (h < 300) { r = x; b = c }
|
|
69
|
+
else { r = c; b = x }
|
|
70
|
+
const toHex = (n: number) => Math.round((n + m) * 255).toString(16).padStart(2, '0')
|
|
71
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function syncFromProp() {
|
|
75
|
+
const hsv = hexToHsv(props.modelValue)
|
|
76
|
+
hue.value = hsv.h
|
|
77
|
+
sat.value = hsv.s
|
|
78
|
+
bright.value = hsv.v
|
|
79
|
+
}
|
|
80
|
+
syncFromProp()
|
|
81
|
+
|
|
82
|
+
watch(() => props.modelValue, syncFromProp)
|
|
83
|
+
|
|
84
|
+
function emitColor() {
|
|
85
|
+
emit('update:modelValue', hsvToHex(hue.value, sat.value, bright.value))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const currentHex = computed(() => hsvToHex(hue.value, sat.value, bright.value))
|
|
89
|
+
const hueColor = computed(() => `hsl(${hue.value}, 100%, 50%)`)
|
|
90
|
+
|
|
91
|
+
function onSBPointerDown(e: PointerEvent) {
|
|
92
|
+
draggingSB.value = true
|
|
93
|
+
updateSB(e)
|
|
94
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
95
|
+
}
|
|
96
|
+
function onSBPointerMove(e: PointerEvent) {
|
|
97
|
+
if (!draggingSB.value) return
|
|
98
|
+
updateSB(e)
|
|
99
|
+
}
|
|
100
|
+
function onSBPointerUp() {
|
|
101
|
+
draggingSB.value = false
|
|
102
|
+
emitColor()
|
|
103
|
+
}
|
|
104
|
+
function updateSB(e: PointerEvent) {
|
|
105
|
+
if (!satBrightEl.value) return
|
|
106
|
+
const rect = satBrightEl.value.getBoundingClientRect()
|
|
107
|
+
sat.value = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100))
|
|
108
|
+
bright.value = Math.max(0, Math.min(100, (1 - (e.clientY - rect.top) / rect.height) * 100))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onHueInput(e: Event) {
|
|
112
|
+
hue.value = Number((e.target as HTMLInputElement).value)
|
|
113
|
+
emitColor()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function selectPreset(color: string) {
|
|
117
|
+
emit('update:modelValue', color)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function onHexInput(e: Event) {
|
|
121
|
+
const v = (e.target as HTMLInputElement).value
|
|
122
|
+
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
|
123
|
+
emit('update:modelValue', v)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function computeDropPos() {
|
|
128
|
+
if (!triggerEl.value) return
|
|
129
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
130
|
+
const panelH = 380
|
|
131
|
+
const spaceBelow = window.innerHeight - rect.bottom - 8
|
|
132
|
+
const above = spaceBelow < panelH && rect.top > panelH
|
|
133
|
+
dropPos.value = {
|
|
134
|
+
top: above ? `${rect.top - 4 - panelH}px` : `${rect.bottom + 4}px`,
|
|
135
|
+
left: `${rect.left}px`,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function onOut(e: MouseEvent) {
|
|
140
|
+
const t = e.target as Node
|
|
141
|
+
if (triggerEl.value?.contains(t)) return
|
|
142
|
+
if (panelEl.value?.contains(t)) return
|
|
143
|
+
open.value = false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onScroll(e: Event) {
|
|
147
|
+
if (!open.value) return
|
|
148
|
+
if (panelEl.value?.contains(e.target as Node)) return
|
|
149
|
+
if (!triggerEl.value) return
|
|
150
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
151
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) { open.value = false; return }
|
|
152
|
+
computeDropPos()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
watch(open, (v) => {
|
|
156
|
+
if (v) {
|
|
157
|
+
computeDropPos()
|
|
158
|
+
setTimeout(() => document.addEventListener('mousedown', onOut), 0)
|
|
159
|
+
} else {
|
|
160
|
+
document.removeEventListener('mousedown', onOut)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
onMounted(() => window.addEventListener('scroll', onScroll, true))
|
|
165
|
+
onUnmounted(() => {
|
|
166
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
167
|
+
document.removeEventListener('mousedown', onOut)
|
|
168
|
+
})
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<template>
|
|
172
|
+
<div class="flex flex-col gap-1">
|
|
173
|
+
<div ref="triggerEl" class="relative mt-2" :style="{ '--field-bg': resolvedFieldBg }">
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
class="flex h-14 w-full items-center gap-3 rounded-sm border bg-transparent px-4 text-left text-body-large transition-[border-color,border-width] duration-150"
|
|
177
|
+
:class="[
|
|
178
|
+
disabled ? 'pointer-events-none opacity-[0.38]' : 'cursor-pointer',
|
|
179
|
+
open
|
|
180
|
+
? error ? 'border-2 border-error' : 'border-2 border-primary'
|
|
181
|
+
: error ? 'border-error' : 'border-outline hover:border-on-surface',
|
|
182
|
+
]"
|
|
183
|
+
@click="!disabled && (open = !open)"
|
|
184
|
+
>
|
|
185
|
+
<span
|
|
186
|
+
class="h-6 w-6 shrink-0 rounded-full border border-outline-variant"
|
|
187
|
+
:style="{ backgroundColor: modelValue }"
|
|
188
|
+
/>
|
|
189
|
+
<span class="flex-1 font-mono text-on-surface">{{ modelValue }}</span>
|
|
190
|
+
<MIcon name="palette" :size="20" class="shrink-0 text-on-surface-variant" />
|
|
191
|
+
</button>
|
|
192
|
+
<label
|
|
193
|
+
v-if="label"
|
|
194
|
+
class="pointer-events-none absolute -top-2.5 left-3 bg-[var(--field-bg)] px-1 text-label-small transition-colors"
|
|
195
|
+
:class="open ? (error ? 'text-error' : 'text-primary') : error ? 'text-error' : 'text-on-surface-variant'"
|
|
196
|
+
>
|
|
197
|
+
{{ label }}
|
|
198
|
+
</label>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
202
|
+
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
203
|
+
|
|
204
|
+
<Teleport to="body">
|
|
205
|
+
<Transition
|
|
206
|
+
enter-active-class="transition-[opacity,transform] duration-150"
|
|
207
|
+
enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
208
|
+
leave-active-class="transition-[opacity,transform] duration-100"
|
|
209
|
+
leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
v-if="open"
|
|
213
|
+
ref="panelEl"
|
|
214
|
+
class="fixed z-[500] w-[280px] rounded-lg bg-surface-container p-4 shadow-elevation-3"
|
|
215
|
+
:style="dropPos"
|
|
216
|
+
>
|
|
217
|
+
<!-- Saturation / Brightness area -->
|
|
218
|
+
<div
|
|
219
|
+
ref="satBrightEl"
|
|
220
|
+
class="relative mb-3 h-40 w-full cursor-crosshair overflow-hidden rounded-lg"
|
|
221
|
+
:style="{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, ${hueColor})` }"
|
|
222
|
+
@pointerdown="onSBPointerDown"
|
|
223
|
+
@pointermove="onSBPointerMove"
|
|
224
|
+
@pointerup="onSBPointerUp"
|
|
225
|
+
>
|
|
226
|
+
<div
|
|
227
|
+
class="pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white shadow-elevation-1"
|
|
228
|
+
:style="{
|
|
229
|
+
left: `${sat}%`,
|
|
230
|
+
top: `${100 - bright}%`,
|
|
231
|
+
backgroundColor: currentHex,
|
|
232
|
+
}"
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- Hue slider -->
|
|
237
|
+
<div class="mb-3">
|
|
238
|
+
<input
|
|
239
|
+
type="range"
|
|
240
|
+
min="0"
|
|
241
|
+
max="360"
|
|
242
|
+
:value="hue"
|
|
243
|
+
class="hue-slider h-3 w-full cursor-pointer appearance-none rounded-full outline-none"
|
|
244
|
+
@input="onHueInput"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<!-- Preview + hex input -->
|
|
249
|
+
<div class="mb-3 flex items-center gap-3">
|
|
250
|
+
<span
|
|
251
|
+
class="h-9 w-9 shrink-0 rounded-full border border-outline-variant"
|
|
252
|
+
:style="{ backgroundColor: currentHex }"
|
|
253
|
+
/>
|
|
254
|
+
<input
|
|
255
|
+
type="text"
|
|
256
|
+
:value="modelValue"
|
|
257
|
+
maxlength="7"
|
|
258
|
+
class="flex-1 rounded-sm border border-outline bg-transparent px-3 py-2 font-mono text-body-medium text-on-surface outline-none transition-colors focus:border-primary"
|
|
259
|
+
@input="onHexInput"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<!-- Presets -->
|
|
264
|
+
<div class="flex flex-wrap gap-1.5">
|
|
265
|
+
<button
|
|
266
|
+
v-for="color in presets"
|
|
267
|
+
:key="color"
|
|
268
|
+
type="button"
|
|
269
|
+
class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full border transition-transform duration-100 hover:scale-110"
|
|
270
|
+
:class="color === modelValue ? 'border-on-surface' : 'border-transparent'"
|
|
271
|
+
:style="{ backgroundColor: color }"
|
|
272
|
+
@click="selectPreset(color)"
|
|
273
|
+
>
|
|
274
|
+
<MIcon v-if="color === modelValue" name="check" :size="14" class="text-white drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]" />
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</Transition>
|
|
279
|
+
</Teleport>
|
|
280
|
+
</div>
|
|
281
|
+
</template>
|
|
282
|
+
|
|
283
|
+
<style scoped>
|
|
284
|
+
.hue-slider {
|
|
285
|
+
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
|
286
|
+
}
|
|
287
|
+
.hue-slider::-webkit-slider-thumb {
|
|
288
|
+
-webkit-appearance: none;
|
|
289
|
+
width: 16px;
|
|
290
|
+
height: 16px;
|
|
291
|
+
border-radius: 50%;
|
|
292
|
+
background: white;
|
|
293
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
}
|
|
296
|
+
.hue-slider::-moz-range-thumb {
|
|
297
|
+
width: 16px;
|
|
298
|
+
height: 16px;
|
|
299
|
+
border-radius: 50%;
|
|
300
|
+
background: white;
|
|
301
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
302
|
+
border: none;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
export interface CommandItem {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
icon?: string
|
|
9
|
+
shortcut?: string
|
|
10
|
+
group?: string
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
onSelect?: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(
|
|
16
|
+
defineProps<{
|
|
17
|
+
modelValue: boolean
|
|
18
|
+
items: CommandItem[]
|
|
19
|
+
placeholder?: string
|
|
20
|
+
noResultsText?: string
|
|
21
|
+
hotkey?: string
|
|
22
|
+
}>(),
|
|
23
|
+
{
|
|
24
|
+
placeholder: 'Buscar comando...',
|
|
25
|
+
noResultsText: 'Sin resultados',
|
|
26
|
+
hotkey: 'k',
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
'update:modelValue': [boolean]
|
|
32
|
+
select: [CommandItem]
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const query = ref('')
|
|
36
|
+
const activeIndex = ref(0)
|
|
37
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
38
|
+
|
|
39
|
+
const filtered = computed(() => {
|
|
40
|
+
if (!query.value) return props.items.filter(i => !i.disabled)
|
|
41
|
+
const q = query.value.toLowerCase()
|
|
42
|
+
return props.items.filter(
|
|
43
|
+
i => !i.disabled && (i.label.toLowerCase().includes(q) || i.group?.toLowerCase().includes(q)),
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const grouped = computed(() => {
|
|
48
|
+
const map = new Map<string, CommandItem[]>()
|
|
49
|
+
for (const item of filtered.value) {
|
|
50
|
+
const g = item.group ?? ''
|
|
51
|
+
if (!map.has(g)) map.set(g, [])
|
|
52
|
+
map.get(g)!.push(item)
|
|
53
|
+
}
|
|
54
|
+
return map
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function open() {
|
|
58
|
+
emit('update:modelValue', true)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function close() {
|
|
62
|
+
query.value = ''
|
|
63
|
+
activeIndex.value = 0
|
|
64
|
+
emit('update:modelValue', false)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function selectItem(item: CommandItem) {
|
|
68
|
+
emit('select', item)
|
|
69
|
+
item.onSelect?.()
|
|
70
|
+
close()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onKeydown(e: KeyboardEvent) {
|
|
74
|
+
if (e.key === 'ArrowDown') {
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
activeIndex.value = (activeIndex.value + 1) % filtered.value.length
|
|
77
|
+
scrollToActive()
|
|
78
|
+
} else if (e.key === 'ArrowUp') {
|
|
79
|
+
e.preventDefault()
|
|
80
|
+
activeIndex.value = (activeIndex.value - 1 + filtered.value.length) % filtered.value.length
|
|
81
|
+
scrollToActive()
|
|
82
|
+
} else if (e.key === 'Enter' && filtered.value.length) {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
selectItem(filtered.value[activeIndex.value]!)
|
|
85
|
+
} else if (e.key === 'Escape') {
|
|
86
|
+
close()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scrollToActive() {
|
|
91
|
+
nextTick(() => {
|
|
92
|
+
const el = document.querySelector('[data-cmd-active="true"]')
|
|
93
|
+
el?.scrollIntoView({ block: 'nearest' })
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onGlobalKeydown(e: KeyboardEvent) {
|
|
98
|
+
if ((e.metaKey || e.ctrlKey) && e.key === props.hotkey) {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
if (props.modelValue) close()
|
|
101
|
+
else open()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
watch(
|
|
106
|
+
() => props.modelValue,
|
|
107
|
+
(open) => {
|
|
108
|
+
if (open) {
|
|
109
|
+
document.body.style.overflow = 'hidden'
|
|
110
|
+
nextTick(() => inputRef.value?.focus())
|
|
111
|
+
} else {
|
|
112
|
+
document.body.style.overflow = ''
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
watch(query, () => { activeIndex.value = 0 })
|
|
118
|
+
|
|
119
|
+
onMounted(() => document.addEventListener('keydown', onGlobalKeydown))
|
|
120
|
+
onBeforeUnmount(() => document.removeEventListener('keydown', onGlobalKeydown))
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<Teleport to="body">
|
|
125
|
+
<Transition name="m3-cmd">
|
|
126
|
+
<div
|
|
127
|
+
v-if="modelValue"
|
|
128
|
+
class="fixed inset-0 z-50 flex items-start justify-center bg-black/40 pt-[15vh]"
|
|
129
|
+
@click.self="close"
|
|
130
|
+
>
|
|
131
|
+
<div class="cmd-box flex w-full max-w-lg flex-col overflow-hidden rounded-xl bg-surface-container-high shadow-elevation-3">
|
|
132
|
+
<!-- Search input -->
|
|
133
|
+
<div class="flex items-center gap-3 border-b border-outline-variant px-4">
|
|
134
|
+
<MIcon name="search" :size="20" class="shrink-0 text-on-surface-variant" />
|
|
135
|
+
<input
|
|
136
|
+
ref="inputRef"
|
|
137
|
+
v-model="query"
|
|
138
|
+
type="text"
|
|
139
|
+
:placeholder="placeholder"
|
|
140
|
+
class="h-12 flex-1 bg-transparent text-body-large text-on-surface outline-none placeholder:text-on-surface-variant/50"
|
|
141
|
+
@keydown="onKeydown"
|
|
142
|
+
/>
|
|
143
|
+
<kbd class="rounded bg-surface-container px-1.5 py-0.5 text-label-small text-on-surface-variant">
|
|
144
|
+
ESC
|
|
145
|
+
</kbd>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Results -->
|
|
149
|
+
<div class="max-h-80 overflow-y-auto py-2">
|
|
150
|
+
<template v-if="filtered.length">
|
|
151
|
+
<template v-for="[group, items] in grouped" :key="group">
|
|
152
|
+
<p v-if="group" class="px-4 pt-3 pb-1 text-label-small font-medium tracking-wide text-on-surface-variant uppercase">
|
|
153
|
+
{{ group }}
|
|
154
|
+
</p>
|
|
155
|
+
<button
|
|
156
|
+
v-for="(item, i) in items"
|
|
157
|
+
:key="item.id"
|
|
158
|
+
type="button"
|
|
159
|
+
:data-cmd-active="filtered.indexOf(item) === activeIndex || undefined"
|
|
160
|
+
class="flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left transition-colors"
|
|
161
|
+
:class="filtered.indexOf(item) === activeIndex ? 'bg-primary/12 text-primary' : 'text-on-surface hover:bg-on-surface/4'"
|
|
162
|
+
@click="selectItem(item)"
|
|
163
|
+
@pointerenter="activeIndex = filtered.indexOf(item)"
|
|
164
|
+
>
|
|
165
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="20" class="shrink-0 opacity-70" />
|
|
166
|
+
<span class="flex-1 truncate text-body-medium">{{ item.label }}</span>
|
|
167
|
+
<kbd v-if="item.shortcut" class="rounded bg-surface-container px-1.5 py-0.5 text-label-small text-on-surface-variant">
|
|
168
|
+
{{ item.shortcut }}
|
|
169
|
+
</kbd>
|
|
170
|
+
</button>
|
|
171
|
+
</template>
|
|
172
|
+
</template>
|
|
173
|
+
<p v-else class="px-4 py-6 text-center text-body-medium text-on-surface-variant">
|
|
174
|
+
{{ noResultsText }}
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Footer -->
|
|
179
|
+
<div class="flex items-center gap-4 border-t border-outline-variant px-4 py-2">
|
|
180
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
181
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">↑↓</kbd> navegar
|
|
182
|
+
</span>
|
|
183
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
184
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">↵</kbd> seleccionar
|
|
185
|
+
</span>
|
|
186
|
+
<span class="flex items-center gap-1 text-label-small text-on-surface-variant">
|
|
187
|
+
<kbd class="rounded bg-surface-container px-1 py-0.5">esc</kbd> cerrar
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</Transition>
|
|
193
|
+
</Teleport>
|
|
194
|
+
</template>
|
|
195
|
+
|
|
196
|
+
<style scoped>
|
|
197
|
+
.m3-cmd-enter-active,
|
|
198
|
+
.m3-cmd-leave-active {
|
|
199
|
+
transition: opacity 0.15s ease;
|
|
200
|
+
}
|
|
201
|
+
.m3-cmd-enter-from,
|
|
202
|
+
.m3-cmd-leave-to {
|
|
203
|
+
opacity: 0;
|
|
204
|
+
}
|
|
205
|
+
.m3-cmd-enter-active .cmd-box,
|
|
206
|
+
.m3-cmd-leave-active .cmd-box {
|
|
207
|
+
transition: transform 0.15s ease;
|
|
208
|
+
}
|
|
209
|
+
.m3-cmd-enter-from .cmd-box,
|
|
210
|
+
.m3-cmd-leave-to .cmd-box {
|
|
211
|
+
transform: scale(0.95) translateY(-10px);
|
|
212
|
+
}
|
|
213
|
+
</style>
|