@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,180 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, useId, useSlots } from "vue";
|
|
3
|
+
import MIcon from "./MIcon.vue";
|
|
4
|
+
import { useFieldBg } from "../composables/useFieldBg";
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(
|
|
7
|
+
defineProps<{
|
|
8
|
+
modelValue: string | number;
|
|
9
|
+
label: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
variant?: "filled" | "outlined";
|
|
12
|
+
error?: string;
|
|
13
|
+
hint?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
multiline?: boolean;
|
|
17
|
+
rows?: number;
|
|
18
|
+
autocomplete?: string;
|
|
19
|
+
leadingIcon?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Background color behind the label in outlined variant.
|
|
22
|
+
* Defaults to the page surface color. Pass e.g. 'var(--color-surface-container-low)'
|
|
23
|
+
* when the input is inside a card.
|
|
24
|
+
*/
|
|
25
|
+
fieldBg?: string;
|
|
26
|
+
}>(),
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
variant: "filled",
|
|
30
|
+
rows: 3,
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{ "update:modelValue": [string] }>();
|
|
35
|
+
|
|
36
|
+
const id = useId();
|
|
37
|
+
const slots = useSlots();
|
|
38
|
+
|
|
39
|
+
const fieldBgEl = ref<HTMLElement | null>(null);
|
|
40
|
+
const { resolvedFieldBg } = useFieldBg(fieldBgEl, () => props.fieldBg);
|
|
41
|
+
|
|
42
|
+
const inputClasses = computed(() => {
|
|
43
|
+
const hasTrailing = !!slots.trailing;
|
|
44
|
+
const pl = props.leadingIcon ? "pl-12" : "pl-4";
|
|
45
|
+
const pr = hasTrailing ? "pr-12" : "pr-4";
|
|
46
|
+
const size = props.multiline ? "resize-y min-h-[56px]" : "h-14";
|
|
47
|
+
const base = [
|
|
48
|
+
"peer block w-full text-body-large text-on-surface outline-none placeholder:text-transparent",
|
|
49
|
+
"transition-[border-color,border-width] duration-150",
|
|
50
|
+
"disabled:cursor-not-allowed disabled:opacity-[0.38]",
|
|
51
|
+
size,
|
|
52
|
+
pl,
|
|
53
|
+
pr,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if (props.variant === "outlined") {
|
|
57
|
+
return [
|
|
58
|
+
...base,
|
|
59
|
+
"rounded-sm border bg-transparent py-4",
|
|
60
|
+
props.error
|
|
61
|
+
? "border-error focus:border-2 focus:border-error"
|
|
62
|
+
: "border-outline hover:border-on-surface focus:border-2 focus:border-primary",
|
|
63
|
+
].join(" ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
...base,
|
|
68
|
+
"rounded-t-sm bg-surface-container-highest border-b pt-6 pb-2",
|
|
69
|
+
props.error
|
|
70
|
+
? "border-error focus:border-b-2 focus:border-error"
|
|
71
|
+
: "border-on-surface-variant hover:border-on-surface focus:border-b-2 focus:border-primary",
|
|
72
|
+
].join(" ");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const labelClasses = computed(() => {
|
|
76
|
+
const left = props.leadingIcon
|
|
77
|
+
? props.variant === "outlined"
|
|
78
|
+
? "left-11"
|
|
79
|
+
: "left-12"
|
|
80
|
+
: props.variant === "outlined"
|
|
81
|
+
? "left-3"
|
|
82
|
+
: "left-4";
|
|
83
|
+
|
|
84
|
+
const base = [
|
|
85
|
+
"pointer-events-none absolute truncate transition-all duration-200",
|
|
86
|
+
left,
|
|
87
|
+
"right-4",
|
|
88
|
+
"top-1/2 -translate-y-1/2 text-body-large",
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (props.variant === "outlined") {
|
|
92
|
+
// When floated: drop right-4 (right-auto) and cap max-width so the label
|
|
93
|
+
// shrinks to its own text width. The bg then only covers the glyphs + px-1,
|
|
94
|
+
// cutting the border just where the text sits instead of a long strip.
|
|
95
|
+
return [
|
|
96
|
+
...base,
|
|
97
|
+
"peer-focus:-top-2.5 peer-focus:translate-y-0 peer-focus:text-label-small peer-focus:right-auto peer-focus:max-w-[calc(100%-1.5rem)] peer-focus:bg-[var(--field-bg)] peer-focus:px-1",
|
|
98
|
+
"peer-[&:not(:placeholder-shown)]:-top-2.5 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:right-auto peer-[&:not(:placeholder-shown)]:max-w-[calc(100%-1.5rem)]",
|
|
99
|
+
"peer-[&:not(:placeholder-shown)]:text-label-small peer-[&:not(:placeholder-shown)]:bg-[var(--field-bg)] peer-[&:not(:placeholder-shown)]:px-1",
|
|
100
|
+
props.error
|
|
101
|
+
? "text-error peer-focus:text-error"
|
|
102
|
+
: "text-on-surface-variant peer-focus:text-primary",
|
|
103
|
+
].join(" ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Filled: label floats to top-2 (slightly higher than before)
|
|
107
|
+
return [
|
|
108
|
+
...base,
|
|
109
|
+
"peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-label-small",
|
|
110
|
+
"peer-[&:not(:placeholder-shown)]:top-2 peer-[&:not(:placeholder-shown)]:translate-y-0 peer-[&:not(:placeholder-shown)]:text-label-small",
|
|
111
|
+
props.error
|
|
112
|
+
? "text-error peer-focus:text-error"
|
|
113
|
+
: "text-on-surface-variant peer-focus:text-primary",
|
|
114
|
+
].join(" ");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function onInput(event: Event) {
|
|
118
|
+
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
|
119
|
+
emit("update:modelValue", target.value);
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<div class="flex flex-col gap-1">
|
|
125
|
+
<!--
|
|
126
|
+
--field-bg: background behind the floating label in outlined mode, so it
|
|
127
|
+
"cuts through" the border. Auto-detected from the nearest opaque ancestor
|
|
128
|
+
(see resolveBg); overridable via the fieldBg prop; falls back to surface.
|
|
129
|
+
-->
|
|
130
|
+
<div
|
|
131
|
+
ref="fieldBgEl"
|
|
132
|
+
class="relative"
|
|
133
|
+
:class="variant === 'outlined' ? 'mt-2' : ''"
|
|
134
|
+
:style="variant === 'outlined' ? { '--field-bg': resolvedFieldBg } : undefined"
|
|
135
|
+
>
|
|
136
|
+
<!-- Leading icon: centered in the 48px left zone (left-3.5 → center at 24px) -->
|
|
137
|
+
<div
|
|
138
|
+
v-if="leadingIcon"
|
|
139
|
+
class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-on-surface-variant"
|
|
140
|
+
>
|
|
141
|
+
<MIcon :name="leadingIcon" :size="20" />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<textarea
|
|
145
|
+
v-if="multiline"
|
|
146
|
+
:id="id"
|
|
147
|
+
:value="String(modelValue)"
|
|
148
|
+
:rows="rows"
|
|
149
|
+
:disabled="disabled"
|
|
150
|
+
:required="required"
|
|
151
|
+
placeholder=" "
|
|
152
|
+
:class="inputClasses"
|
|
153
|
+
@input="onInput"
|
|
154
|
+
/>
|
|
155
|
+
<input
|
|
156
|
+
v-else
|
|
157
|
+
:id="id"
|
|
158
|
+
:type="type"
|
|
159
|
+
:value="modelValue"
|
|
160
|
+
:disabled="disabled"
|
|
161
|
+
:required="required"
|
|
162
|
+
:autocomplete="autocomplete"
|
|
163
|
+
placeholder=" "
|
|
164
|
+
:class="inputClasses"
|
|
165
|
+
@input="onInput"
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<label :for="id" :class="labelClasses">
|
|
169
|
+
{{ label }}<span v-if="required" class="text-error"> *</span>
|
|
170
|
+
</label>
|
|
171
|
+
|
|
172
|
+
<div v-if="$slots.trailing" class="absolute top-1/2 right-2 -translate-y-1/2">
|
|
173
|
+
<slot name="trailing" />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
178
|
+
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
179
|
+
</div>
|
|
180
|
+
</template>
|
|
@@ -0,0 +1,227 @@
|
|
|
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 | null
|
|
8
|
+
label?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
error?: string
|
|
11
|
+
hint?: string
|
|
12
|
+
minuteStep?: number
|
|
13
|
+
use24h?: boolean
|
|
14
|
+
fieldBg?: string
|
|
15
|
+
}>(), { minuteStep: 5, use24h: true })
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{ 'update:modelValue': [string | null] }>()
|
|
18
|
+
|
|
19
|
+
const open = ref(false)
|
|
20
|
+
const triggerEl = ref<HTMLElement | null>(null)
|
|
21
|
+
const panelEl = ref<HTMLElement | null>(null)
|
|
22
|
+
const mode = ref<'hour' | 'minute'>('hour')
|
|
23
|
+
const dropPos = ref({ top: '0px', left: '0px' })
|
|
24
|
+
const { resolvedFieldBg } = useFieldBg(triggerEl, () => props.fieldBg)
|
|
25
|
+
|
|
26
|
+
const parsed = computed(() => {
|
|
27
|
+
if (!props.modelValue) return { h: 12, m: 0 }
|
|
28
|
+
const parts = props.modelValue.split(':').map(Number)
|
|
29
|
+
return { h: parts[0] ?? 12, m: parts[1] ?? 0 }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const selectedHour = ref(parsed.value.h)
|
|
33
|
+
const selectedMinute = ref(parsed.value.m)
|
|
34
|
+
watch(() => props.modelValue, () => {
|
|
35
|
+
selectedHour.value = parsed.value.h
|
|
36
|
+
selectedMinute.value = parsed.value.m
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const hours = Array.from({ length: 24 }, (_, i) => i)
|
|
40
|
+
const minutes = computed(() => {
|
|
41
|
+
const arr: number[] = []
|
|
42
|
+
for (let m = 0; m < 60; m += props.minuteStep) arr.push(m)
|
|
43
|
+
return arr
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function pad(n: number) { return String(n).padStart(2, '0') }
|
|
47
|
+
|
|
48
|
+
function selectHour(h: number) {
|
|
49
|
+
selectedHour.value = h
|
|
50
|
+
mode.value = 'minute'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function selectMinute(m: number) {
|
|
54
|
+
selectedMinute.value = m
|
|
55
|
+
emit('update:modelValue', `${pad(selectedHour.value)}:${pad(m)}`)
|
|
56
|
+
open.value = false
|
|
57
|
+
mode.value = 'hour'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clear() {
|
|
61
|
+
emit('update:modelValue', null)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const displayValue = computed(() => {
|
|
65
|
+
if (!props.modelValue) return ''
|
|
66
|
+
return `${pad(parsed.value.h)}:${pad(parsed.value.m)}`
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function computeDropPos() {
|
|
70
|
+
if (!triggerEl.value) return
|
|
71
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
72
|
+
const panelH = 320
|
|
73
|
+
const spaceBelow = window.innerHeight - rect.bottom - 8
|
|
74
|
+
const above = spaceBelow < panelH && rect.top > panelH
|
|
75
|
+
dropPos.value = {
|
|
76
|
+
top: above ? `${rect.top - 4 - panelH}px` : `${rect.bottom + 4}px`,
|
|
77
|
+
left: `${rect.left}px`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onOut(e: MouseEvent) {
|
|
82
|
+
const t = e.target as Node
|
|
83
|
+
if (triggerEl.value?.contains(t)) return
|
|
84
|
+
if (panelEl.value?.contains(t)) return
|
|
85
|
+
open.value = false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onScroll(e: Event) {
|
|
89
|
+
if (!open.value) return
|
|
90
|
+
if (panelEl.value?.contains(e.target as Node)) return
|
|
91
|
+
if (!triggerEl.value) return
|
|
92
|
+
const rect = triggerEl.value.getBoundingClientRect()
|
|
93
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight) { open.value = false; return }
|
|
94
|
+
computeDropPos()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
watch(open, (v) => {
|
|
98
|
+
if (v) {
|
|
99
|
+
mode.value = 'hour'
|
|
100
|
+
selectedHour.value = parsed.value.h
|
|
101
|
+
selectedMinute.value = parsed.value.m
|
|
102
|
+
computeDropPos()
|
|
103
|
+
setTimeout(() => document.addEventListener('mousedown', onOut), 0)
|
|
104
|
+
} else {
|
|
105
|
+
document.removeEventListener('mousedown', onOut)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
onMounted(() => window.addEventListener('scroll', onScroll, true))
|
|
110
|
+
onUnmounted(() => {
|
|
111
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
112
|
+
document.removeEventListener('mousedown', onOut)
|
|
113
|
+
})
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="flex flex-col gap-1">
|
|
118
|
+
<div ref="triggerEl" class="relative mt-2" :style="{ '--field-bg': resolvedFieldBg }">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class="flex h-14 w-full items-center gap-2 rounded-sm border bg-transparent px-4 text-left text-body-large transition-[border-color,border-width] duration-150"
|
|
122
|
+
:class="[
|
|
123
|
+
disabled ? 'pointer-events-none opacity-[0.38]' : 'cursor-pointer',
|
|
124
|
+
open
|
|
125
|
+
? error ? 'border-2 border-error' : 'border-2 border-primary'
|
|
126
|
+
: error ? 'border-error' : 'border-outline hover:border-on-surface',
|
|
127
|
+
]"
|
|
128
|
+
@click="!disabled && (open = !open)"
|
|
129
|
+
>
|
|
130
|
+
<MIcon name="schedule" :size="20" class="shrink-0 text-on-surface-variant" />
|
|
131
|
+
<span v-if="displayValue" class="flex-1 font-mono text-on-surface">{{ displayValue }}</span>
|
|
132
|
+
<span v-else class="flex-1 text-on-surface-variant">{{ label || 'Seleccionar hora' }}</span>
|
|
133
|
+
<MIcon
|
|
134
|
+
v-if="modelValue"
|
|
135
|
+
name="close"
|
|
136
|
+
:size="18"
|
|
137
|
+
class="shrink-0 cursor-pointer text-on-surface-variant hover:text-on-surface"
|
|
138
|
+
@click.stop="clear"
|
|
139
|
+
/>
|
|
140
|
+
</button>
|
|
141
|
+
<label
|
|
142
|
+
v-if="label"
|
|
143
|
+
class="pointer-events-none absolute -top-2.5 left-3 bg-[var(--field-bg)] px-1 text-label-small transition-colors"
|
|
144
|
+
:class="open ? (error ? 'text-error' : 'text-primary') : error ? 'text-error' : 'text-on-surface-variant'"
|
|
145
|
+
>
|
|
146
|
+
{{ label }}
|
|
147
|
+
</label>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<p v-if="error" class="px-4 text-body-small text-error">{{ error }}</p>
|
|
151
|
+
<p v-else-if="hint" class="px-4 text-body-small text-on-surface-variant">{{ hint }}</p>
|
|
152
|
+
|
|
153
|
+
<Teleport to="body">
|
|
154
|
+
<Transition
|
|
155
|
+
enter-active-class="transition-[opacity,transform] duration-150"
|
|
156
|
+
enter-from-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
157
|
+
leave-active-class="transition-[opacity,transform] duration-100"
|
|
158
|
+
leave-to-class="opacity-0 -translate-y-1 scale-[0.98]"
|
|
159
|
+
>
|
|
160
|
+
<div
|
|
161
|
+
v-if="open"
|
|
162
|
+
ref="panelEl"
|
|
163
|
+
class="fixed z-[500] w-[280px] rounded-lg bg-surface-container shadow-elevation-3"
|
|
164
|
+
:style="dropPos"
|
|
165
|
+
>
|
|
166
|
+
<!-- Display -->
|
|
167
|
+
<div class="flex items-center justify-center gap-1 border-b border-outline-variant px-4 py-4">
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
class="rounded-lg px-3 py-2 font-mono text-headline-medium transition-colors"
|
|
171
|
+
:class="mode === 'hour' ? 'bg-primary-container text-on-primary-container cursor-default' : 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8'"
|
|
172
|
+
@click="mode = 'hour'"
|
|
173
|
+
>
|
|
174
|
+
{{ pad(selectedHour) }}
|
|
175
|
+
</button>
|
|
176
|
+
<span class="text-headline-medium text-on-surface-variant">:</span>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
class="rounded-lg px-3 py-2 font-mono text-headline-medium transition-colors"
|
|
180
|
+
:class="mode === 'minute' ? 'bg-primary-container text-on-primary-container cursor-default' : 'cursor-pointer text-on-surface-variant hover:bg-on-surface/8'"
|
|
181
|
+
@click="mode = 'minute'"
|
|
182
|
+
>
|
|
183
|
+
{{ pad(selectedMinute) }}
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<!-- Grid -->
|
|
188
|
+
<div class="p-3">
|
|
189
|
+
<div v-if="mode === 'hour'" class="grid grid-cols-6 gap-1">
|
|
190
|
+
<button
|
|
191
|
+
v-for="h in hours"
|
|
192
|
+
:key="h"
|
|
193
|
+
type="button"
|
|
194
|
+
class="flex h-9 cursor-pointer items-center justify-center rounded-full text-body-medium transition-colors duration-100"
|
|
195
|
+
:class="
|
|
196
|
+
h === selectedHour
|
|
197
|
+
? 'bg-primary text-on-primary'
|
|
198
|
+
: 'text-on-surface hover:bg-on-surface/8'
|
|
199
|
+
"
|
|
200
|
+
@click="selectHour(h)"
|
|
201
|
+
>
|
|
202
|
+
{{ pad(h) }}
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div v-else class="grid grid-cols-6 gap-1">
|
|
207
|
+
<button
|
|
208
|
+
v-for="m in minutes"
|
|
209
|
+
:key="m"
|
|
210
|
+
type="button"
|
|
211
|
+
class="flex h-9 cursor-pointer items-center justify-center rounded-full text-body-medium transition-colors duration-100"
|
|
212
|
+
:class="
|
|
213
|
+
m === selectedMinute
|
|
214
|
+
? 'bg-primary text-on-primary'
|
|
215
|
+
: 'text-on-surface hover:bg-on-surface/8'
|
|
216
|
+
"
|
|
217
|
+
@click="selectMinute(m)"
|
|
218
|
+
>
|
|
219
|
+
{{ pad(m) }}
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</Transition>
|
|
225
|
+
</Teleport>
|
|
226
|
+
</div>
|
|
227
|
+
</template>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MIcon from "./MIcon.vue";
|
|
3
|
+
|
|
4
|
+
export interface TimelineItem {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
date?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
color?: "primary" | "secondary" | "tertiary" | "error" | "success";
|
|
10
|
+
dotColor?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
withDefaults(
|
|
14
|
+
defineProps<{
|
|
15
|
+
items: TimelineItem[];
|
|
16
|
+
dense?: boolean;
|
|
17
|
+
alternating?: boolean;
|
|
18
|
+
}>(),
|
|
19
|
+
{ dense: false, alternating: false },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const dotBg: Record<string, string> = {
|
|
23
|
+
primary: "bg-primary text-on-primary",
|
|
24
|
+
secondary: "bg-secondary text-on-secondary",
|
|
25
|
+
tertiary: "bg-tertiary text-on-tertiary",
|
|
26
|
+
error: "bg-error text-on-error",
|
|
27
|
+
success: "bg-success text-on-success",
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div :class="alternating ? 'relative' : 'flex flex-col'">
|
|
33
|
+
<!-- Standard layout -->
|
|
34
|
+
<template v-if="!alternating">
|
|
35
|
+
<div
|
|
36
|
+
v-for="(item, i) in items"
|
|
37
|
+
:key="i"
|
|
38
|
+
class="relative flex gap-4"
|
|
39
|
+
:class="dense ? 'pb-4' : 'pb-8'"
|
|
40
|
+
>
|
|
41
|
+
<!-- Line + dot -->
|
|
42
|
+
<div class="flex flex-col items-center">
|
|
43
|
+
<div
|
|
44
|
+
class="z-[1] flex shrink-0 items-center justify-center rounded-full"
|
|
45
|
+
:class="[item.icon ? 'h-9 w-9' : 'h-3 w-3', dotBg[item.color ?? 'primary']]"
|
|
46
|
+
:style="item.dotColor ? { backgroundColor: item.dotColor } : undefined"
|
|
47
|
+
>
|
|
48
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="18" />
|
|
49
|
+
</div>
|
|
50
|
+
<div
|
|
51
|
+
v-if="i < items.length - 1"
|
|
52
|
+
class="w-[2px] flex-1"
|
|
53
|
+
:class="dotBg[item.color ?? 'primary']!.split(' ')[0] + '/30'"
|
|
54
|
+
style="min-height: 16px"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Content -->
|
|
59
|
+
<div :class="item.icon ? '' : 'pt-0'" class="-mt-0.5 flex-1">
|
|
60
|
+
<div class="flex items-baseline justify-between gap-2">
|
|
61
|
+
<p class="text-body-large font-medium text-on-surface">{{ item.title }}</p>
|
|
62
|
+
<span v-if="item.date" class="shrink-0 text-label-small text-on-surface-variant">{{
|
|
63
|
+
item.date
|
|
64
|
+
}}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<p v-if="item.description" class="mt-1 text-body-medium text-on-surface-variant">
|
|
67
|
+
{{ item.description }}
|
|
68
|
+
</p>
|
|
69
|
+
<div v-if="$slots[`item-${i}`]" class="mt-2">
|
|
70
|
+
<slot :name="`item-${i}`" :item="item" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<!-- Alternating layout -->
|
|
77
|
+
<template v-else>
|
|
78
|
+
<div
|
|
79
|
+
v-for="(item, i) in items"
|
|
80
|
+
:key="i"
|
|
81
|
+
class="flex items-stretch"
|
|
82
|
+
:class="i % 2 === 0 ? 'flex-row' : 'flex-row-reverse'"
|
|
83
|
+
>
|
|
84
|
+
<!-- Content side -->
|
|
85
|
+
<div
|
|
86
|
+
class="flex-1"
|
|
87
|
+
:class="[i % 2 === 0 ? 'text-right' : 'text-left', dense ? 'pb-4' : 'pb-8']"
|
|
88
|
+
>
|
|
89
|
+
<p class="text-body-large font-medium text-on-surface">{{ item.title }}</p>
|
|
90
|
+
<p v-if="item.description" class="mt-1 text-body-medium text-on-surface-variant">
|
|
91
|
+
{{ item.description }}
|
|
92
|
+
</p>
|
|
93
|
+
<span
|
|
94
|
+
v-if="item.date"
|
|
95
|
+
class="mt-1 inline-block text-label-small text-on-surface-variant"
|
|
96
|
+
>{{ item.date }}</span
|
|
97
|
+
>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Center column: dot + continuous line -->
|
|
101
|
+
<div class="flex w-14 shrink-0 flex-col items-center">
|
|
102
|
+
<div
|
|
103
|
+
class="z-[1] flex shrink-0 items-center justify-center rounded-full"
|
|
104
|
+
:class="[item.icon ? 'h-9 w-9' : 'h-3.5 w-3.5', dotBg[item.color ?? 'primary']]"
|
|
105
|
+
:style="item.dotColor ? { backgroundColor: item.dotColor } : undefined"
|
|
106
|
+
>
|
|
107
|
+
<MIcon v-if="item.icon" :name="item.icon" :size="18" />
|
|
108
|
+
</div>
|
|
109
|
+
<div v-if="i < items.length - 1" class="w-[2px] flex-1 bg-outline-variant" />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Empty side -->
|
|
113
|
+
<div class="flex-1" />
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { nextTick, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
text: string
|
|
6
|
+
placement?: 'top' | 'bottom' | 'left' | 'right'
|
|
7
|
+
delay?: number
|
|
8
|
+
}>(), { placement: 'top', delay: 600 })
|
|
9
|
+
|
|
10
|
+
const visible = ref(false)
|
|
11
|
+
const tipEl = ref<HTMLElement>()
|
|
12
|
+
const triggerEl = ref<HTMLElement>()
|
|
13
|
+
const tipStyle = ref<Record<string, string>>({})
|
|
14
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
15
|
+
|
|
16
|
+
async function show() {
|
|
17
|
+
if (timer) clearTimeout(timer)
|
|
18
|
+
timer = setTimeout(async () => {
|
|
19
|
+
visible.value = true
|
|
20
|
+
await nextTick()
|
|
21
|
+
reposition()
|
|
22
|
+
}, props.delay)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hide() {
|
|
26
|
+
if (timer) { clearTimeout(timer); timer = null }
|
|
27
|
+
visible.value = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function reposition() {
|
|
31
|
+
if (!triggerEl.value || !tipEl.value) return
|
|
32
|
+
const tr = triggerEl.value.getBoundingClientRect()
|
|
33
|
+
const tt = tipEl.value.getBoundingClientRect()
|
|
34
|
+
const GAP = 6
|
|
35
|
+
|
|
36
|
+
let top = 0, left = 0
|
|
37
|
+
switch (props.placement) {
|
|
38
|
+
case 'top': top = tr.top - tt.height - GAP; left = tr.left + (tr.width - tt.width) / 2; break
|
|
39
|
+
case 'bottom': top = tr.bottom + GAP; left = tr.left + (tr.width - tt.width) / 2; break
|
|
40
|
+
case 'left': top = tr.top + (tr.height - tt.height) / 2; left = tr.left - tt.width - GAP; break
|
|
41
|
+
case 'right': top = tr.top + (tr.height - tt.height) / 2; left = tr.right + GAP; break
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
top = Math.max(6, Math.min(top, window.innerHeight - tt.height - 6))
|
|
45
|
+
left = Math.max(6, Math.min(left, window.innerWidth - tt.width - 6))
|
|
46
|
+
tipStyle.value = { top: `${top}px`, left: `${left}px` }
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<span
|
|
52
|
+
ref="triggerEl"
|
|
53
|
+
class="inline-flex"
|
|
54
|
+
@mouseenter="show"
|
|
55
|
+
@mouseleave="hide"
|
|
56
|
+
@focusin="show"
|
|
57
|
+
@focusout="hide"
|
|
58
|
+
>
|
|
59
|
+
<slot />
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<Teleport to="body">
|
|
63
|
+
<Transition
|
|
64
|
+
enter-active-class="transition-opacity duration-150"
|
|
65
|
+
enter-from-class="opacity-0"
|
|
66
|
+
enter-to-class="opacity-100"
|
|
67
|
+
leave-active-class="transition-opacity duration-100"
|
|
68
|
+
leave-from-class="opacity-100"
|
|
69
|
+
leave-to-class="opacity-0"
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
v-if="visible && text"
|
|
73
|
+
ref="tipEl"
|
|
74
|
+
class="pointer-events-none fixed z-[400] max-w-[220px] rounded bg-inverse-surface px-3 py-1.5 text-label-medium text-inverse-on-surface shadow-elevation-2"
|
|
75
|
+
:style="tipStyle"
|
|
76
|
+
role="tooltip"
|
|
77
|
+
>
|
|
78
|
+
{{ text }}
|
|
79
|
+
</div>
|
|
80
|
+
</Transition>
|
|
81
|
+
</Teleport>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import MIcon from './MIcon.vue'
|
|
3
|
+
import MIconButton from './MIconButton.vue'
|
|
4
|
+
|
|
5
|
+
withDefaults(defineProps<{
|
|
6
|
+
title?: string
|
|
7
|
+
variant?: 'center' | 'small' | 'medium' | 'large'
|
|
8
|
+
navigationIcon?: string
|
|
9
|
+
elevated?: boolean
|
|
10
|
+
}>(), { variant: 'small' })
|
|
11
|
+
|
|
12
|
+
defineEmits<{ navigation: [] }>()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<header
|
|
17
|
+
class="flex w-full flex-col bg-surface transition-shadow"
|
|
18
|
+
:class="elevated ? 'shadow-elevation-2' : ''"
|
|
19
|
+
>
|
|
20
|
+
<!-- Top row -->
|
|
21
|
+
<div class="flex h-16 items-center gap-1 px-2">
|
|
22
|
+
<!-- Navigation icon -->
|
|
23
|
+
<MIconButton
|
|
24
|
+
v-if="navigationIcon"
|
|
25
|
+
:icon="navigationIcon"
|
|
26
|
+
label="Navegación"
|
|
27
|
+
@click="$emit('navigation')"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Title: center or small variant -->
|
|
31
|
+
<h1
|
|
32
|
+
v-if="variant === 'center' || variant === 'small'"
|
|
33
|
+
class="flex-1 truncate px-2 text-title-large text-on-surface"
|
|
34
|
+
:class="variant === 'center' ? 'text-center' : ''"
|
|
35
|
+
>
|
|
36
|
+
<slot name="title">{{ title }}</slot>
|
|
37
|
+
</h1>
|
|
38
|
+
|
|
39
|
+
<!-- Spacer for medium/large (title is below) -->
|
|
40
|
+
<div v-else class="flex-1" />
|
|
41
|
+
|
|
42
|
+
<!-- Trailing actions -->
|
|
43
|
+
<div v-if="$slots.actions" class="flex items-center gap-1">
|
|
44
|
+
<slot name="actions" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Large title row for medium/large variants -->
|
|
49
|
+
<div
|
|
50
|
+
v-if="variant === 'medium' || variant === 'large'"
|
|
51
|
+
class="px-4 pb-6"
|
|
52
|
+
:class="variant === 'large' ? 'pt-4' : 'pt-1'"
|
|
53
|
+
>
|
|
54
|
+
<h1
|
|
55
|
+
class="text-on-surface"
|
|
56
|
+
:class="variant === 'large' ? 'text-headline-medium' : 'text-headline-small'"
|
|
57
|
+
>
|
|
58
|
+
<slot name="title">{{ title }}</slot>
|
|
59
|
+
</h1>
|
|
60
|
+
</div>
|
|
61
|
+
</header>
|
|
62
|
+
</template>
|