@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,404 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import MCheckbox from './MCheckbox.vue'
|
|
4
|
+
import MIcon from './MIcon.vue'
|
|
5
|
+
import MPagination from './MPagination.vue'
|
|
6
|
+
|
|
7
|
+
export interface TableColumn {
|
|
8
|
+
key: string
|
|
9
|
+
label: string
|
|
10
|
+
sortable?: boolean
|
|
11
|
+
width?: string
|
|
12
|
+
align?: 'left' | 'center' | 'right'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TableFetchParams {
|
|
16
|
+
page: number
|
|
17
|
+
perPage: number
|
|
18
|
+
search: string
|
|
19
|
+
sortKey: string
|
|
20
|
+
sortDir: 'asc' | 'desc' | ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Static widths so skeleton markup is stable across re-renders
|
|
24
|
+
const SKEL = [65, 80, 50, 75, 90, 55, 70, 85, 60, 78, 88, 52, 70, 83, 58]
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(
|
|
27
|
+
defineProps<{
|
|
28
|
+
columns: TableColumn[]
|
|
29
|
+
rows: Record<string, any>[]
|
|
30
|
+
loading?: boolean
|
|
31
|
+
emptyText?: string
|
|
32
|
+
rowKey?: string
|
|
33
|
+
selectable?: boolean
|
|
34
|
+
modelValue?: Record<string, any>[]
|
|
35
|
+
perPage?: number
|
|
36
|
+
searchable?: boolean
|
|
37
|
+
/** Emit `fetch` instead of filtering locally. Requires :total. */
|
|
38
|
+
serverSide?: boolean
|
|
39
|
+
total?: number
|
|
40
|
+
page?: number
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
loading: false,
|
|
44
|
+
emptyText: 'Sin resultados',
|
|
45
|
+
rowKey: 'id',
|
|
46
|
+
selectable: false,
|
|
47
|
+
modelValue: () => [],
|
|
48
|
+
perPage: 10,
|
|
49
|
+
searchable: true,
|
|
50
|
+
serverSide: false,
|
|
51
|
+
total: 0,
|
|
52
|
+
page: 1,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
'update:modelValue': [Record<string, any>[]]
|
|
58
|
+
'update:page': [number]
|
|
59
|
+
fetch: [TableFetchParams]
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
// ── Search ─────────────────────────────────────────────────────────────────
|
|
63
|
+
const search = ref('')
|
|
64
|
+
|
|
65
|
+
// ── Sort ───────────────────────────────────────────────────────────────────
|
|
66
|
+
const sortKey = ref('')
|
|
67
|
+
const sortDir = ref<'asc' | 'desc' | ''>('')
|
|
68
|
+
|
|
69
|
+
function toggleSort(key: string) {
|
|
70
|
+
if (sortKey.value !== key) {
|
|
71
|
+
sortKey.value = key
|
|
72
|
+
sortDir.value = 'asc'
|
|
73
|
+
} else if (sortDir.value === 'asc') {
|
|
74
|
+
sortDir.value = 'desc'
|
|
75
|
+
} else {
|
|
76
|
+
sortKey.value = ''
|
|
77
|
+
sortDir.value = ''
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Pagination ─────────────────────────────────────────────────────────────
|
|
82
|
+
const internalPage = ref(1)
|
|
83
|
+
|
|
84
|
+
const currentPage = computed({
|
|
85
|
+
get: () => (props.serverSide ? (props.page ?? 1) : internalPage.value),
|
|
86
|
+
set: (val: number) => {
|
|
87
|
+
if (props.serverSide) emit('update:page', val)
|
|
88
|
+
else internalPage.value = val
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// ── Client-side data processing ────────────────────────────────────────────
|
|
93
|
+
const processedRows = computed(() => {
|
|
94
|
+
if (props.serverSide) return props.rows
|
|
95
|
+
|
|
96
|
+
let result = props.rows
|
|
97
|
+
|
|
98
|
+
if (search.value.trim()) {
|
|
99
|
+
const q = search.value.toLowerCase()
|
|
100
|
+
result = result.filter((row) =>
|
|
101
|
+
props.columns.some((col) => {
|
|
102
|
+
const val = row[col.key]
|
|
103
|
+
return val != null && String(val).toLowerCase().includes(q)
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sortKey.value && sortDir.value) {
|
|
109
|
+
const key = sortKey.value
|
|
110
|
+
const dir = sortDir.value
|
|
111
|
+
result = [...result].sort((a, b) => {
|
|
112
|
+
const cmp = String(a[key] ?? '').localeCompare(String(b[key] ?? ''), undefined, {
|
|
113
|
+
numeric: true,
|
|
114
|
+
sensitivity: 'base',
|
|
115
|
+
})
|
|
116
|
+
return dir === 'asc' ? cmp : -cmp
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const totalCount = computed(() =>
|
|
124
|
+
props.serverSide ? (props.total ?? 0) : processedRows.value.length,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const visibleRows = computed(() => {
|
|
128
|
+
if (props.serverSide) return props.rows
|
|
129
|
+
const start = (currentPage.value - 1) * props.perPage
|
|
130
|
+
return processedRows.value.slice(start, start + props.perPage)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
watch([search, sortKey, sortDir], () => {
|
|
134
|
+
if (!props.serverSide) internalPage.value = 1
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ── Server-side fetch ──────────────────────────────────────────────────────
|
|
138
|
+
const mounted = ref(false)
|
|
139
|
+
|
|
140
|
+
function emitFetch() {
|
|
141
|
+
emit('fetch', {
|
|
142
|
+
page: currentPage.value,
|
|
143
|
+
perPage: props.perPage,
|
|
144
|
+
search: search.value,
|
|
145
|
+
sortKey: sortKey.value,
|
|
146
|
+
sortDir: sortDir.value,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onMounted(() => {
|
|
151
|
+
mounted.value = true
|
|
152
|
+
if (props.serverSide) emitFetch()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
watch([search, sortKey, sortDir], () => {
|
|
156
|
+
if (!props.serverSide || !mounted.value) return
|
|
157
|
+
internalPage.value = 1
|
|
158
|
+
emitFetch()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
watch(currentPage, () => {
|
|
162
|
+
if (!props.serverSide || !mounted.value) return
|
|
163
|
+
emitFetch()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// ── Row selection ──────────────────────────────────────────────────────────
|
|
167
|
+
const selected = computed({
|
|
168
|
+
get: () => props.modelValue ?? [],
|
|
169
|
+
set: (val) => emit('update:modelValue', val),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
function rowId(row: Record<string, any>) {
|
|
173
|
+
return row[props.rowKey]
|
|
174
|
+
}
|
|
175
|
+
function isSelected(row: Record<string, any>) {
|
|
176
|
+
return selected.value.some((r) => rowId(r) === rowId(row))
|
|
177
|
+
}
|
|
178
|
+
function toggleRow(row: Record<string, any>) {
|
|
179
|
+
if (isSelected(row)) selected.value = selected.value.filter((r) => rowId(r) !== rowId(row))
|
|
180
|
+
else selected.value = [...selected.value, row]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allOnPageSelected = computed(
|
|
184
|
+
() => visibleRows.value.length > 0 && visibleRows.value.every((r) => isSelected(r)),
|
|
185
|
+
)
|
|
186
|
+
const someOnPageSelected = computed(
|
|
187
|
+
() => visibleRows.value.some((r) => isSelected(r)) && !allOnPageSelected.value,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
function toggleAll() {
|
|
191
|
+
if (allOnPageSelected.value) {
|
|
192
|
+
selected.value = selected.value.filter(
|
|
193
|
+
(r) => !visibleRows.value.some((v) => rowId(v) === rowId(r)),
|
|
194
|
+
)
|
|
195
|
+
} else {
|
|
196
|
+
selected.value = [...selected.value, ...visibleRows.value.filter((r) => !isSelected(r))]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
201
|
+
const extraCols = computed(
|
|
202
|
+
() => (props.selectable ? 1 : 0) + (useSlots()['row-actions'] ? 1 : 0),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
function alignClass(align?: string) {
|
|
206
|
+
return align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left'
|
|
207
|
+
}
|
|
208
|
+
function skelWidth(ri: number, ci: number) {
|
|
209
|
+
return `${SKEL[(ri * 3 + ci) % SKEL.length]}%`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
import { useSlots } from 'vue'
|
|
213
|
+
const slots = useSlots()
|
|
214
|
+
const hasActions = computed(() => !!slots['row-actions'])
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<div class="flex flex-col overflow-hidden rounded-sm border border-outline-variant">
|
|
219
|
+
|
|
220
|
+
<!-- ── Toolbar ───────────────────────────────────────────────────────── -->
|
|
221
|
+
<div
|
|
222
|
+
v-if="searchable || $slots.toolbar"
|
|
223
|
+
class="flex flex-wrap items-center gap-3 border-b border-outline-variant bg-surface-container-lowest px-4 py-2.5"
|
|
224
|
+
>
|
|
225
|
+
<!-- Search -->
|
|
226
|
+
<div v-if="searchable" class="flex min-w-48 flex-1 items-center gap-2 rounded-full border border-outline-variant bg-surface-container px-3 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary/30 transition-[border-color,box-shadow] duration-150">
|
|
227
|
+
<MIcon name="search" :size="16" class="shrink-0 text-on-surface-variant" />
|
|
228
|
+
<input
|
|
229
|
+
v-model="search"
|
|
230
|
+
type="text"
|
|
231
|
+
placeholder="Buscar..."
|
|
232
|
+
class="w-full bg-transparent text-body-medium text-on-surface outline-none placeholder:text-on-surface-variant"
|
|
233
|
+
/>
|
|
234
|
+
<button
|
|
235
|
+
v-if="search"
|
|
236
|
+
class="text-on-surface-variant transition-colors hover:text-on-surface"
|
|
237
|
+
@click="search = ''"
|
|
238
|
+
>
|
|
239
|
+
<MIcon name="close" :size="14" />
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Extra toolbar content (filters, buttons, etc.) -->
|
|
244
|
+
<slot name="toolbar" />
|
|
245
|
+
|
|
246
|
+
<!-- Selection count pill -->
|
|
247
|
+
<Transition
|
|
248
|
+
enter-active-class="transition-[opacity,transform] duration-150"
|
|
249
|
+
enter-from-class="opacity-0 scale-90"
|
|
250
|
+
leave-active-class="transition-[opacity,transform] duration-100"
|
|
251
|
+
leave-to-class="opacity-0 scale-90"
|
|
252
|
+
>
|
|
253
|
+
<span
|
|
254
|
+
v-if="selectable && selected.length > 0"
|
|
255
|
+
class="rounded-full bg-primary/12 px-3 py-1 text-label-small font-medium text-primary"
|
|
256
|
+
>
|
|
257
|
+
{{ selected.length }} seleccionado{{ selected.length !== 1 ? 's' : '' }}
|
|
258
|
+
</span>
|
|
259
|
+
</Transition>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- ── Table ─────────────────────────────────────────────────────────── -->
|
|
263
|
+
<div class="overflow-x-auto">
|
|
264
|
+
<table class="w-full border-collapse">
|
|
265
|
+
|
|
266
|
+
<!-- Header -->
|
|
267
|
+
<thead>
|
|
268
|
+
<tr class="bg-surface-container-high">
|
|
269
|
+
<th v-if="selectable" class="w-12 px-4 py-3">
|
|
270
|
+
<MCheckbox
|
|
271
|
+
:model-value="allOnPageSelected"
|
|
272
|
+
:indeterminate="someOnPageSelected"
|
|
273
|
+
@update:model-value="toggleAll"
|
|
274
|
+
/>
|
|
275
|
+
</th>
|
|
276
|
+
<th
|
|
277
|
+
v-for="col in columns"
|
|
278
|
+
:key="col.key"
|
|
279
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
280
|
+
:class="[
|
|
281
|
+
'px-4 py-3 text-label-medium font-medium text-on-surface-variant whitespace-nowrap',
|
|
282
|
+
alignClass(col.align),
|
|
283
|
+
col.sortable
|
|
284
|
+
? 'cursor-pointer select-none hover:text-on-surface transition-colors duration-100'
|
|
285
|
+
: '',
|
|
286
|
+
]"
|
|
287
|
+
@click="col.sortable ? toggleSort(col.key) : undefined"
|
|
288
|
+
>
|
|
289
|
+
<span class="inline-flex items-center gap-1">
|
|
290
|
+
{{ col.label }}
|
|
291
|
+
<span v-if="col.sortable" class="inline-flex">
|
|
292
|
+
<MIcon
|
|
293
|
+
v-if="sortKey === col.key && sortDir === 'asc'"
|
|
294
|
+
name="arrow_upward"
|
|
295
|
+
:size="14"
|
|
296
|
+
class="text-primary"
|
|
297
|
+
/>
|
|
298
|
+
<MIcon
|
|
299
|
+
v-else-if="sortKey === col.key && sortDir === 'desc'"
|
|
300
|
+
name="arrow_downward"
|
|
301
|
+
:size="14"
|
|
302
|
+
class="text-primary"
|
|
303
|
+
/>
|
|
304
|
+
<MIcon v-else name="unfold_more" :size="14" class="opacity-30" />
|
|
305
|
+
</span>
|
|
306
|
+
</span>
|
|
307
|
+
</th>
|
|
308
|
+
<th v-if="hasActions" class="w-1 px-4 py-3" />
|
|
309
|
+
</tr>
|
|
310
|
+
</thead>
|
|
311
|
+
|
|
312
|
+
<!-- Body -->
|
|
313
|
+
<tbody>
|
|
314
|
+
<!-- Loading skeleton -->
|
|
315
|
+
<template v-if="loading">
|
|
316
|
+
<tr
|
|
317
|
+
v-for="ri in perPage"
|
|
318
|
+
:key="`sk-${ri}`"
|
|
319
|
+
class="border-t border-outline-variant"
|
|
320
|
+
>
|
|
321
|
+
<td v-if="selectable" class="px-4 py-3.5">
|
|
322
|
+
<div class="h-4 w-4 animate-pulse rounded bg-on-surface/10" />
|
|
323
|
+
</td>
|
|
324
|
+
<td
|
|
325
|
+
v-for="(col, ci) in columns"
|
|
326
|
+
:key="col.key"
|
|
327
|
+
class="px-4 py-3.5"
|
|
328
|
+
>
|
|
329
|
+
<div
|
|
330
|
+
class="h-4 animate-pulse rounded-full bg-on-surface/10"
|
|
331
|
+
:style="{ width: skelWidth(ri, ci) }"
|
|
332
|
+
/>
|
|
333
|
+
</td>
|
|
334
|
+
<td v-if="hasActions" class="px-4 py-3.5">
|
|
335
|
+
<div class="ml-auto h-4 w-16 animate-pulse rounded-full bg-on-surface/10" />
|
|
336
|
+
</td>
|
|
337
|
+
</tr>
|
|
338
|
+
</template>
|
|
339
|
+
|
|
340
|
+
<!-- Empty state -->
|
|
341
|
+
<template v-else-if="visibleRows.length === 0">
|
|
342
|
+
<tr>
|
|
343
|
+
<td
|
|
344
|
+
:colspan="columns.length + extraCols"
|
|
345
|
+
class="border-t border-outline-variant px-4 py-14 text-center"
|
|
346
|
+
>
|
|
347
|
+
<slot name="empty">
|
|
348
|
+
<MIcon name="search_off" :size="36" class="mb-2 text-on-surface-variant opacity-30" />
|
|
349
|
+
<p class="text-body-medium text-on-surface-variant">{{ emptyText }}</p>
|
|
350
|
+
</slot>
|
|
351
|
+
</td>
|
|
352
|
+
</tr>
|
|
353
|
+
</template>
|
|
354
|
+
|
|
355
|
+
<!-- Data rows -->
|
|
356
|
+
<template v-else>
|
|
357
|
+
<tr
|
|
358
|
+
v-for="row in visibleRows"
|
|
359
|
+
:key="rowId(row)"
|
|
360
|
+
:class="[
|
|
361
|
+
'border-t border-outline-variant transition-colors duration-100',
|
|
362
|
+
'hover:bg-on-surface/[0.04]',
|
|
363
|
+
selectable && isSelected(row) ? 'bg-primary/[0.06]' : '',
|
|
364
|
+
selectable ? 'cursor-pointer' : '',
|
|
365
|
+
]"
|
|
366
|
+
@click="selectable ? toggleRow(row) : undefined"
|
|
367
|
+
>
|
|
368
|
+
<td v-if="selectable" class="px-4 py-3" @click.stop="toggleRow(row)">
|
|
369
|
+
<MCheckbox
|
|
370
|
+
:model-value="isSelected(row)"
|
|
371
|
+
@update:model-value="toggleRow(row)"
|
|
372
|
+
/>
|
|
373
|
+
</td>
|
|
374
|
+
<td
|
|
375
|
+
v-for="col in columns"
|
|
376
|
+
:key="col.key"
|
|
377
|
+
:class="['px-4 py-3 text-body-medium text-on-surface', alignClass(col.align)]"
|
|
378
|
+
>
|
|
379
|
+
<slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]" :col="col">
|
|
380
|
+
{{ row[col.key] ?? '—' }}
|
|
381
|
+
</slot>
|
|
382
|
+
</td>
|
|
383
|
+
<td v-if="hasActions" class="px-4 py-3 text-right" @click.stop>
|
|
384
|
+
<slot name="row-actions" :row="row" />
|
|
385
|
+
</td>
|
|
386
|
+
</tr>
|
|
387
|
+
</template>
|
|
388
|
+
</tbody>
|
|
389
|
+
|
|
390
|
+
</table>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<!-- ── Footer ────────────────────────────────────────────────────────── -->
|
|
394
|
+
<div class="border-t border-outline-variant bg-surface-container-lowest px-4 py-2">
|
|
395
|
+
<MPagination
|
|
396
|
+
:page="currentPage"
|
|
397
|
+
:per-page="perPage"
|
|
398
|
+
:total="totalCount"
|
|
399
|
+
@update:page="currentPage = $event"
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
</div>
|
|
404
|
+
</template>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import MIcon from './MIcon.vue'
|
|
4
|
+
|
|
5
|
+
interface Tab {
|
|
6
|
+
value: string | number
|
|
7
|
+
label: string
|
|
8
|
+
icon?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
modelValue: string | number
|
|
14
|
+
tabs: Tab[]
|
|
15
|
+
variant?: 'primary' | 'secondary'
|
|
16
|
+
}>(), { variant: 'primary' })
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{ 'update:modelValue': [string | number] }>()
|
|
19
|
+
|
|
20
|
+
// Primary tab: refs for indicator position
|
|
21
|
+
const tabEls = ref<HTMLElement[]>([])
|
|
22
|
+
const indicatorLeft = ref(0)
|
|
23
|
+
const indicatorWidth = ref(0)
|
|
24
|
+
|
|
25
|
+
function updateIndicator() {
|
|
26
|
+
nextTick(() => {
|
|
27
|
+
const idx = props.tabs.findIndex((t) => t.value === props.modelValue)
|
|
28
|
+
const el = tabEls.value[idx]
|
|
29
|
+
if (!el) return
|
|
30
|
+
indicatorLeft.value = el.offsetLeft
|
|
31
|
+
indicatorWidth.value = el.offsetWidth
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMounted(updateIndicator)
|
|
36
|
+
watch(() => props.modelValue, updateIndicator)
|
|
37
|
+
watch(() => props.tabs, updateIndicator, { deep: true })
|
|
38
|
+
|
|
39
|
+
function select(tab: Tab) {
|
|
40
|
+
if (!tab.disabled) emit('update:modelValue', tab.value)
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<!-- ── Primary: underline with sliding indicator ──────────────────────── -->
|
|
46
|
+
<div v-if="variant === 'primary'" class="relative border-b border-outline-variant">
|
|
47
|
+
<div class="flex overflow-x-auto" style="scrollbar-width: none">
|
|
48
|
+
<button
|
|
49
|
+
v-for="tab in tabs"
|
|
50
|
+
:key="tab.value"
|
|
51
|
+
:ref="(el) => { if (el) tabEls[tabs.indexOf(tab)] = el as HTMLElement }"
|
|
52
|
+
type="button"
|
|
53
|
+
class="relative flex h-12 shrink-0 flex-col items-center justify-center gap-0.5 px-6 text-label-large transition-colors duration-150 focus-visible:outline-none"
|
|
54
|
+
:class="[
|
|
55
|
+
tab.value === modelValue
|
|
56
|
+
? 'text-primary'
|
|
57
|
+
: tab.disabled
|
|
58
|
+
? 'cursor-not-allowed text-on-surface/38'
|
|
59
|
+
: 'cursor-pointer text-on-surface-variant hover:text-on-surface',
|
|
60
|
+
tab.icon ? 'pt-2 pb-1' : '',
|
|
61
|
+
]"
|
|
62
|
+
:disabled="tab.disabled"
|
|
63
|
+
@click="select(tab)"
|
|
64
|
+
>
|
|
65
|
+
<MIcon v-if="tab.icon" :name="tab.icon" :size="20" />
|
|
66
|
+
<span>{{ tab.label }}</span>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<!-- Sliding indicator -->
|
|
70
|
+
<div
|
|
71
|
+
class="absolute bottom-0 h-[3px] rounded-t-sm bg-primary transition-[left,width] duration-200 ease-[cubic-bezier(0.2,0,0,1)]"
|
|
72
|
+
:style="{ left: `${indicatorLeft}px`, width: `${indicatorWidth}px` }"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- ── Secondary: pill style ──────────────────────────────────────────── -->
|
|
77
|
+
<div v-else class="flex flex-wrap gap-1 rounded-full bg-surface-container p-1">
|
|
78
|
+
<button
|
|
79
|
+
v-for="tab in tabs"
|
|
80
|
+
:key="tab.value"
|
|
81
|
+
type="button"
|
|
82
|
+
class="flex h-8 items-center gap-2 rounded-full px-4 text-label-large transition-all duration-150 focus-visible:outline-none"
|
|
83
|
+
:class="
|
|
84
|
+
tab.value === modelValue
|
|
85
|
+
? 'bg-secondary-container text-on-secondary-container shadow-elevation-1'
|
|
86
|
+
: tab.disabled
|
|
87
|
+
? 'cursor-not-allowed text-on-surface/38'
|
|
88
|
+
: 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8 hover:text-on-surface'
|
|
89
|
+
"
|
|
90
|
+
:disabled="tab.disabled"
|
|
91
|
+
@click="select(tab)"
|
|
92
|
+
>
|
|
93
|
+
<MIcon v-if="tab.icon" :name="tab.icon" :size="16" />
|
|
94
|
+
{{ tab.label }}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
3
|
+
import { Terminal } from '@xterm/xterm'
|
|
4
|
+
import { FitAddon } from '@xterm/addon-fit'
|
|
5
|
+
import MIcon from './MIcon.vue'
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
lines?: string[]
|
|
10
|
+
readonly?: boolean
|
|
11
|
+
title?: string
|
|
12
|
+
minHeight?: string
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
lines: () => [],
|
|
16
|
+
readonly: false,
|
|
17
|
+
title: 'Terminal',
|
|
18
|
+
minHeight: '300px',
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
input: [string]
|
|
24
|
+
line: [string]
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
28
|
+
let terminal: Terminal | null = null
|
|
29
|
+
let fitAddon: FitAddon | null = null
|
|
30
|
+
let resizeObserver: ResizeObserver | null = null
|
|
31
|
+
let lineBuffer = ''
|
|
32
|
+
|
|
33
|
+
function getThemeColors() {
|
|
34
|
+
const style = getComputedStyle(document.documentElement)
|
|
35
|
+
return {
|
|
36
|
+
background: style.getPropertyValue('--color-surface-container-lowest').trim() || '#0f0d13',
|
|
37
|
+
foreground: style.getPropertyValue('--color-on-surface').trim() || '#e6e1e5',
|
|
38
|
+
cursor: style.getPropertyValue('--color-primary').trim() || '#d0bcff',
|
|
39
|
+
cursorAccent: style.getPropertyValue('--color-on-primary').trim() || '#381e72',
|
|
40
|
+
selectionBackground: style.getPropertyValue('--color-primary-container').trim() || '#4f378b',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onMounted(async () => {
|
|
45
|
+
if (!containerRef.value) return
|
|
46
|
+
|
|
47
|
+
try { await import('@xterm/xterm/css/xterm.css') } catch { /* consumer will provide styles */ }
|
|
48
|
+
|
|
49
|
+
const colors = getThemeColors()
|
|
50
|
+
|
|
51
|
+
terminal = new Terminal({
|
|
52
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
|
53
|
+
fontSize: 14,
|
|
54
|
+
lineHeight: 1.4,
|
|
55
|
+
cursorBlink: !props.readonly,
|
|
56
|
+
disableStdin: props.readonly,
|
|
57
|
+
theme: {
|
|
58
|
+
background: colors.background,
|
|
59
|
+
foreground: colors.foreground,
|
|
60
|
+
cursor: colors.cursor,
|
|
61
|
+
cursorAccent: colors.cursorAccent,
|
|
62
|
+
selectionBackground: colors.selectionBackground,
|
|
63
|
+
},
|
|
64
|
+
scrollback: 1000,
|
|
65
|
+
convertEol: true,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
fitAddon = new FitAddon()
|
|
69
|
+
terminal.loadAddon(fitAddon)
|
|
70
|
+
terminal.open(containerRef.value)
|
|
71
|
+
|
|
72
|
+
try { fitAddon.fit() } catch { /* container might not be visible yet */ }
|
|
73
|
+
|
|
74
|
+
for (const line of props.lines) {
|
|
75
|
+
terminal.writeln(line)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!props.readonly) {
|
|
79
|
+
terminal.onData((data) => {
|
|
80
|
+
emit('input', data)
|
|
81
|
+
|
|
82
|
+
if (data === '\r') {
|
|
83
|
+
terminal!.write('\r\n')
|
|
84
|
+
emit('line', lineBuffer)
|
|
85
|
+
lineBuffer = ''
|
|
86
|
+
} else if (data === '\x7f') {
|
|
87
|
+
if (lineBuffer.length > 0) {
|
|
88
|
+
lineBuffer = lineBuffer.slice(0, -1)
|
|
89
|
+
terminal!.write('\b \b')
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
lineBuffer += data
|
|
93
|
+
terminal!.write(data)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
resizeObserver = new ResizeObserver(() => {
|
|
99
|
+
try { fitAddon?.fit() } catch { /* ignore */ }
|
|
100
|
+
})
|
|
101
|
+
resizeObserver.observe(containerRef.value)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
watch(() => props.lines, (newLines) => {
|
|
105
|
+
if (!terminal) return
|
|
106
|
+
terminal.clear()
|
|
107
|
+
for (const line of newLines) {
|
|
108
|
+
terminal.writeln(line)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
onBeforeUnmount(() => {
|
|
113
|
+
resizeObserver?.disconnect()
|
|
114
|
+
terminal?.dispose()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
defineExpose({
|
|
118
|
+
write: (text: string) => terminal?.write(text),
|
|
119
|
+
writeln: (text: string) => terminal?.writeln(text),
|
|
120
|
+
clear: () => terminal?.clear(),
|
|
121
|
+
focus: () => terminal?.focus(),
|
|
122
|
+
})
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<template>
|
|
126
|
+
<div class="overflow-hidden rounded-lg border border-outline-variant">
|
|
127
|
+
<!-- Title bar -->
|
|
128
|
+
<div class="flex items-center gap-2 border-b border-outline-variant bg-surface-container px-4 py-2">
|
|
129
|
+
<MIcon name="terminal" :size="18" class="text-on-surface-variant" />
|
|
130
|
+
<span class="flex-1 text-label-medium text-on-surface-variant">{{ title }}</span>
|
|
131
|
+
<div class="flex gap-1.5">
|
|
132
|
+
<div class="h-3 w-3 rounded-full bg-error/60" />
|
|
133
|
+
<div class="h-3 w-3 rounded-full bg-tertiary-container" />
|
|
134
|
+
<div class="h-3 w-3 rounded-full bg-success/60" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Terminal container -->
|
|
139
|
+
<div
|
|
140
|
+
ref="containerRef"
|
|
141
|
+
class="bg-surface-container-lowest px-2 py-1"
|
|
142
|
+
:style="{ minHeight }"
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|