@m3ui-vue/m3ui-vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/components/MAlert.vue.d.ts +27 -0
  4. package/dist/components/MAppBar.vue.d.ts +24 -0
  5. package/dist/components/MAvatar.vue.d.ts +9 -0
  6. package/dist/components/MBadge.vue.d.ts +22 -0
  7. package/dist/components/MBottomSheet.vue.d.ts +26 -0
  8. package/dist/components/MBreadcrumbs.vue.d.ts +19 -0
  9. package/dist/components/MButton.vue.d.ts +32 -0
  10. package/dist/components/MCalendar.vue.d.ts +23 -0
  11. package/dist/components/MCard.vue.d.ts +28 -0
  12. package/dist/components/MChart.vue.d.ts +13 -0
  13. package/dist/components/MCheckbox.vue.d.ts +26 -0
  14. package/dist/components/MChip.vue.d.ts +33 -0
  15. package/dist/components/MCodeEditor.vue.d.ts +35 -0
  16. package/dist/components/MColorPicker.vue.d.ts +18 -0
  17. package/dist/components/MCommandPalette.vue.d.ts +29 -0
  18. package/dist/components/MConfirmDialog.vue.d.ts +23 -0
  19. package/dist/components/MContainer.vue.d.ts +24 -0
  20. package/dist/components/MContextMenu.vue.d.ts +35 -0
  21. package/dist/components/MDataTable.vue.d.ts +83 -0
  22. package/dist/components/MDatePicker.vue.d.ts +21 -0
  23. package/dist/components/MDateRangePicker.vue.d.ts +24 -0
  24. package/dist/components/MDialog.vue.d.ts +30 -0
  25. package/dist/components/MDivider.vue.d.ts +11 -0
  26. package/dist/components/MDragDropList.vue.d.ts +40 -0
  27. package/dist/components/MEmptyState.vue.d.ts +21 -0
  28. package/dist/components/MExpansionPanel.vue.d.ts +28 -0
  29. package/dist/components/MFab.vue.d.ts +28 -0
  30. package/dist/components/MFileUpload.vue.d.ts +25 -0
  31. package/dist/components/MGrid.vue.d.ts +26 -0
  32. package/dist/components/MHotkeys.vue.d.ts +16 -0
  33. package/dist/components/MIcon.vue.d.ts +9 -0
  34. package/dist/components/MIconButton.vue.d.ts +14 -0
  35. package/dist/components/MInfiniteScroll.vue.d.ts +34 -0
  36. package/dist/components/MJsonEditor.vue.d.ts +17 -0
  37. package/dist/components/MJsonViewer.vue.d.ts +14 -0
  38. package/dist/components/MKanban.vue.d.ts +53 -0
  39. package/dist/components/MLoadingOverlay.vue.d.ts +28 -0
  40. package/dist/components/MMarkdown.vue.d.ts +11 -0
  41. package/dist/components/MMasonry.vue.d.ts +23 -0
  42. package/dist/components/MMenu.vue.d.ts +27 -0
  43. package/dist/components/MMenuItem.vue.d.ts +16 -0
  44. package/dist/components/MMultiSelect.vue.d.ts +34 -0
  45. package/dist/components/MNavigationBar.vue.d.ts +18 -0
  46. package/dist/components/MNavigationDrawer.vue.d.ts +41 -0
  47. package/dist/components/MNavigationRail.vue.d.ts +32 -0
  48. package/dist/components/MPagination.vue.d.ts +12 -0
  49. package/dist/components/MProgressBar.vue.d.ts +13 -0
  50. package/dist/components/MRadio.vue.d.ts +17 -0
  51. package/dist/components/MRadioGroup.vue.d.ts +24 -0
  52. package/dist/components/MRating.vue.d.ts +23 -0
  53. package/dist/components/MResult.vue.d.ts +20 -0
  54. package/dist/components/MRichTextEditor.vue.d.ts +17 -0
  55. package/dist/components/MScheduler.vue.d.ts +35 -0
  56. package/dist/components/MSegmentedButton.vue.d.ts +24 -0
  57. package/dist/components/MSelect.vue.d.ts +29 -0
  58. package/dist/components/MSideSheet.vue.d.ts +28 -0
  59. package/dist/components/MSkeleton.vue.d.ts +14 -0
  60. package/dist/components/MSlider.vue.d.ts +24 -0
  61. package/dist/components/MSnackbar.vue.d.ts +3 -0
  62. package/dist/components/MSpinner.vue.d.ts +10 -0
  63. package/dist/components/MSplitter.vue.d.ts +26 -0
  64. package/dist/components/MSpotlightSearch.vue.d.ts +34 -0
  65. package/dist/components/MStack.vue.d.ts +30 -0
  66. package/dist/components/MStatCard.vue.d.ts +24 -0
  67. package/dist/components/MStepper.vue.d.ts +33 -0
  68. package/dist/components/MSwitch.vue.d.ts +14 -0
  69. package/dist/components/MTable.vue.d.ts +73 -0
  70. package/dist/components/MTabs.vue.d.ts +20 -0
  71. package/dist/components/MTerminal.vue.d.ts +25 -0
  72. package/dist/components/MTextField.vue.d.ts +41 -0
  73. package/dist/components/MTimePicker.vue.d.ts +20 -0
  74. package/dist/components/MTimeline.vue.d.ts +31 -0
  75. package/dist/components/MTooltip.vue.d.ts +21 -0
  76. package/dist/components/MTopAppBar.vue.d.ts +29 -0
  77. package/dist/components/MTour.vue.d.ts +19 -0
  78. package/dist/components/MTransferList.vue.d.ts +23 -0
  79. package/dist/components/MTree.vue.d.ts +68 -0
  80. package/dist/components/MTreeTable.vue.d.ts +57 -0
  81. package/dist/components/MVirtualTable.vue.d.ts +40 -0
  82. package/dist/components/_MContextMenuPanel.vue.d.ts +13 -0
  83. package/dist/components/_MTreeNode.vue.d.ts +26 -0
  84. package/dist/composables/useColorPalette.d.ts +11 -0
  85. package/dist/composables/useFieldBg.d.ts +13 -0
  86. package/dist/composables/useTheme.d.ts +5 -0
  87. package/dist/composables/useToast.d.ts +59 -0
  88. package/dist/index.d.ts +112 -0
  89. package/dist/m3ui.css +2 -0
  90. package/dist/m3ui.js +7432 -0
  91. package/dist/m3ui.js.map +1 -0
  92. package/dist/plugin.d.ts +9 -0
  93. package/dist/styles/palettes.css +1253 -0
  94. package/dist/styles/theme.css +249 -0
  95. package/package.json +166 -0
  96. package/src/components/MAlert.vue +69 -0
  97. package/src/components/MAppBar.vue +40 -0
  98. package/src/components/MAvatar.vue +21 -0
  99. package/src/components/MBadge.vue +46 -0
  100. package/src/components/MBottomSheet.vue +113 -0
  101. package/src/components/MBreadcrumbs.vue +52 -0
  102. package/src/components/MButton.vue +111 -0
  103. package/src/components/MCalendar.vue +173 -0
  104. package/src/components/MCard.vue +56 -0
  105. package/src/components/MChart.vue +158 -0
  106. package/src/components/MCheckbox.vue +48 -0
  107. package/src/components/MChip.vue +87 -0
  108. package/src/components/MCodeEditor.vue +179 -0
  109. package/src/components/MColorPicker.vue +305 -0
  110. package/src/components/MCommandPalette.vue +213 -0
  111. package/src/components/MConfirmDialog.vue +43 -0
  112. package/src/components/MContainer.vue +36 -0
  113. package/src/components/MContextMenu.vue +66 -0
  114. package/src/components/MDataTable.vue +376 -0
  115. package/src/components/MDatePicker.vue +253 -0
  116. package/src/components/MDateRangePicker.vue +265 -0
  117. package/src/components/MDialog.vue +90 -0
  118. package/src/components/MDivider.vue +26 -0
  119. package/src/components/MDragDropList.vue +111 -0
  120. package/src/components/MEmptyState.vue +40 -0
  121. package/src/components/MExpansionPanel.vue +112 -0
  122. package/src/components/MFab.vue +220 -0
  123. package/src/components/MFileUpload.vue +206 -0
  124. package/src/components/MGrid.vue +99 -0
  125. package/src/components/MHotkeys.vue +122 -0
  126. package/src/components/MIcon.vue +9 -0
  127. package/src/components/MIconButton.vue +49 -0
  128. package/src/components/MInfiniteScroll.vue +68 -0
  129. package/src/components/MJsonEditor.vue +118 -0
  130. package/src/components/MJsonViewer.vue +106 -0
  131. package/src/components/MKanban.vue +147 -0
  132. package/src/components/MLoadingOverlay.vue +52 -0
  133. package/src/components/MMarkdown.vue +123 -0
  134. package/src/components/MMasonry.vue +87 -0
  135. package/src/components/MMenu.vue +113 -0
  136. package/src/components/MMenuItem.vue +15 -0
  137. package/src/components/MMultiSelect.vue +306 -0
  138. package/src/components/MNavigationBar.vue +62 -0
  139. package/src/components/MNavigationDrawer.vue +157 -0
  140. package/src/components/MNavigationRail.vue +80 -0
  141. package/src/components/MPagination.vue +37 -0
  142. package/src/components/MProgressBar.vue +200 -0
  143. package/src/components/MRadio.vue +89 -0
  144. package/src/components/MRadioGroup.vue +41 -0
  145. package/src/components/MRating.vue +108 -0
  146. package/src/components/MResult.vue +62 -0
  147. package/src/components/MRichTextEditor.vue +199 -0
  148. package/src/components/MScheduler.vue +225 -0
  149. package/src/components/MSegmentedButton.vue +75 -0
  150. package/src/components/MSelect.vue +259 -0
  151. package/src/components/MSideSheet.vue +112 -0
  152. package/src/components/MSkeleton.vue +60 -0
  153. package/src/components/MSlider.vue +188 -0
  154. package/src/components/MSnackbar.vue +244 -0
  155. package/src/components/MSpinner.vue +122 -0
  156. package/src/components/MSplitter.vue +97 -0
  157. package/src/components/MSpotlightSearch.vue +244 -0
  158. package/src/components/MStack.vue +67 -0
  159. package/src/components/MStatCard.vue +56 -0
  160. package/src/components/MStepper.vue +161 -0
  161. package/src/components/MSwitch.vue +63 -0
  162. package/src/components/MTable.vue +404 -0
  163. package/src/components/MTabs.vue +97 -0
  164. package/src/components/MTerminal.vue +146 -0
  165. package/src/components/MTextField.vue +180 -0
  166. package/src/components/MTimePicker.vue +227 -0
  167. package/src/components/MTimeline.vue +117 -0
  168. package/src/components/MTooltip.vue +82 -0
  169. package/src/components/MTopAppBar.vue +62 -0
  170. package/src/components/MTour.vue +226 -0
  171. package/src/components/MTransferList.vue +181 -0
  172. package/src/components/MTree.vue +164 -0
  173. package/src/components/MTreeTable.vue +159 -0
  174. package/src/components/MVirtualTable.vue +155 -0
  175. package/src/components/_MContextMenuPanel.vue +129 -0
  176. package/src/components/_MTreeNode.vue +171 -0
  177. package/src/composables/useColorPalette.ts +60 -0
  178. package/src/composables/useFieldBg.ts +91 -0
  179. package/src/composables/useTheme.ts +55 -0
  180. package/src/composables/useToast.ts +51 -0
  181. package/src/env.d.ts +1 -0
  182. package/src/index.ts +119 -0
  183. package/src/plugin.ts +18 -0
  184. package/src/styles/palettes.css +1253 -0
  185. package/src/styles/theme.css +249 -0
@@ -0,0 +1,199 @@
1
+ <script setup lang="ts">
2
+ import { watch } from 'vue'
3
+ import { useEditor, EditorContent } from '@tiptap/vue-3'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import Underline from '@tiptap/extension-underline'
6
+ import TextAlign from '@tiptap/extension-text-align'
7
+ import Link from '@tiptap/extension-link'
8
+ import Image from '@tiptap/extension-image'
9
+ import Highlight from '@tiptap/extension-highlight'
10
+ import Placeholder from '@tiptap/extension-placeholder'
11
+ import { TextStyle } from '@tiptap/extension-text-style'
12
+ import Color from '@tiptap/extension-color'
13
+ import MIcon from './MIcon.vue'
14
+
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ modelValue: string
18
+ placeholder?: string
19
+ disabled?: boolean
20
+ minHeight?: string
21
+ }>(),
22
+ { placeholder: 'Escribe aquí...', disabled: false, minHeight: '200px' },
23
+ )
24
+
25
+ const emit = defineEmits<{ 'update:modelValue': [string] }>()
26
+
27
+ const editor = useEditor({
28
+ content: props.modelValue,
29
+ editable: !props.disabled,
30
+ extensions: [
31
+ StarterKit,
32
+ Underline,
33
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
34
+ Link.configure({ openOnClick: false }),
35
+ Image,
36
+ Highlight.configure({ multicolor: true }),
37
+ Placeholder.configure({ placeholder: props.placeholder }),
38
+ TextStyle,
39
+ Color,
40
+ ],
41
+ onUpdate: ({ editor: e }) => emit('update:modelValue', e.getHTML()),
42
+ })
43
+
44
+ watch(() => props.modelValue, (val) => {
45
+ if (editor.value && editor.value.getHTML() !== val) editor.value.commands.setContent(val)
46
+ })
47
+
48
+ watch(() => props.disabled, (v) => editor.value?.setEditable(!v))
49
+
50
+ interface ToolBtn {
51
+ icon: string
52
+ label: string
53
+ action: () => void
54
+ active?: () => boolean
55
+ }
56
+
57
+ const toolGroups: ToolBtn[][] = [
58
+ [
59
+ { icon: 'format_bold', label: 'Negrita', action: () => editor.value?.chain().focus().toggleBold().run(), active: () => !!editor.value?.isActive('bold') },
60
+ { icon: 'format_italic', label: 'Cursiva', action: () => editor.value?.chain().focus().toggleItalic().run(), active: () => !!editor.value?.isActive('italic') },
61
+ { icon: 'format_underlined', label: 'Subrayado', action: () => editor.value?.chain().focus().toggleUnderline().run(), active: () => !!editor.value?.isActive('underline') },
62
+ { icon: 'format_strikethrough', label: 'Tachado', action: () => editor.value?.chain().focus().toggleStrike().run(), active: () => !!editor.value?.isActive('strike') },
63
+ { icon: 'ink_highlighter', label: 'Resaltar', action: () => editor.value?.chain().focus().toggleHighlight().run(), active: () => !!editor.value?.isActive('highlight') },
64
+ ],
65
+ [
66
+ { icon: 'format_list_bulleted', label: 'Lista', action: () => editor.value?.chain().focus().toggleBulletList().run(), active: () => !!editor.value?.isActive('bulletList') },
67
+ { icon: 'format_list_numbered', label: 'Lista numerada', action: () => editor.value?.chain().focus().toggleOrderedList().run(), active: () => !!editor.value?.isActive('orderedList') },
68
+ { icon: 'format_quote', label: 'Cita', action: () => editor.value?.chain().focus().toggleBlockquote().run(), active: () => !!editor.value?.isActive('blockquote') },
69
+ { icon: 'code', label: 'Código', action: () => editor.value?.chain().focus().toggleCode().run(), active: () => !!editor.value?.isActive('code') },
70
+ ],
71
+ [
72
+ { icon: 'format_align_left', label: 'Izquierda', action: () => editor.value?.chain().focus().setTextAlign('left').run(), active: () => !!editor.value?.isActive({ textAlign: 'left' }) },
73
+ { icon: 'format_align_center', label: 'Centro', action: () => editor.value?.chain().focus().setTextAlign('center').run(), active: () => !!editor.value?.isActive({ textAlign: 'center' }) },
74
+ { icon: 'format_align_right', label: 'Derecha', action: () => editor.value?.chain().focus().setTextAlign('right').run(), active: () => !!editor.value?.isActive({ textAlign: 'right' }) },
75
+ ],
76
+ [
77
+ { icon: 'undo', label: 'Deshacer', action: () => editor.value?.chain().focus().undo().run() },
78
+ { icon: 'redo', label: 'Rehacer', action: () => editor.value?.chain().focus().redo().run() },
79
+ ],
80
+ ]
81
+
82
+ function insertLink() {
83
+ const url = window.prompt('URL del enlace:')
84
+ if (url) editor.value?.chain().focus().setLink({ href: url }).run()
85
+ }
86
+
87
+ function insertImage() {
88
+ const url = window.prompt('URL de la imagen:')
89
+ if (url) editor.value?.chain().focus().setImage({ src: url }).run()
90
+ }
91
+
92
+ function setHeading(level: 1 | 2 | 3) {
93
+ editor.value?.chain().focus().toggleHeading({ level }).run()
94
+ }
95
+ </script>
96
+
97
+ <template>
98
+ <div
99
+ class="overflow-hidden rounded-lg border transition-colors duration-150"
100
+ :class="disabled ? 'border-outline-variant/50 opacity-60' : 'border-outline-variant focus-within:border-primary'"
101
+ >
102
+ <!-- Toolbar -->
103
+ <div class="flex flex-wrap items-center gap-0.5 border-b border-outline-variant bg-surface-container px-2 py-1.5">
104
+ <!-- Heading select -->
105
+ <select
106
+ class="h-8 cursor-pointer rounded bg-transparent px-2 text-label-large text-on-surface-variant outline-none hover:bg-on-surface/8"
107
+ :value="
108
+ editor?.isActive('heading', { level: 1 }) ? '1'
109
+ : editor?.isActive('heading', { level: 2 }) ? '2'
110
+ : editor?.isActive('heading', { level: 3 }) ? '3'
111
+ : '0'
112
+ "
113
+ @change="(e: Event) => {
114
+ const v = (e.target as HTMLSelectElement).value
115
+ if (v === '0') editor?.chain().focus().setParagraph().run()
116
+ else setHeading(Number(v) as 1 | 2 | 3)
117
+ }"
118
+ >
119
+ <option value="0">Párrafo</option>
120
+ <option value="1">Título 1</option>
121
+ <option value="2">Título 2</option>
122
+ <option value="3">Título 3</option>
123
+ </select>
124
+
125
+ <div class="mx-1 h-6 w-px bg-outline-variant" />
126
+
127
+ <template v-for="(group, gi) in toolGroups" :key="gi">
128
+ <button
129
+ v-for="btn in group"
130
+ :key="btn.icon"
131
+ type="button"
132
+ :title="btn.label"
133
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded transition-colors duration-100"
134
+ :class="btn.active?.() ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-on-surface/8'"
135
+ @click="btn.action"
136
+ >
137
+ <MIcon :name="btn.icon" :size="20" />
138
+ </button>
139
+ <div v-if="gi < toolGroups.length - 1" class="mx-1 h-6 w-px bg-outline-variant" />
140
+ </template>
141
+
142
+ <div class="mx-1 h-6 w-px bg-outline-variant" />
143
+
144
+ <button
145
+ type="button"
146
+ title="Enlace"
147
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded text-on-surface-variant transition-colors hover:bg-on-surface/8"
148
+ @click="insertLink"
149
+ >
150
+ <MIcon name="link" :size="20" />
151
+ </button>
152
+ <button
153
+ type="button"
154
+ title="Imagen"
155
+ class="flex h-8 w-8 cursor-pointer items-center justify-center rounded text-on-surface-variant transition-colors hover:bg-on-surface/8"
156
+ @click="insertImage"
157
+ >
158
+ <MIcon name="image" :size="20" />
159
+ </button>
160
+ </div>
161
+
162
+ <!-- Editor content -->
163
+ <EditorContent
164
+ :editor="editor"
165
+ class="rte-content bg-surface px-4 py-3 text-body-large text-on-surface"
166
+ :style="{ minHeight: minHeight }"
167
+ />
168
+ </div>
169
+ </template>
170
+
171
+ <style scoped>
172
+ .rte-content :deep(.tiptap) {
173
+ outline: none;
174
+ min-height: inherit;
175
+ }
176
+
177
+ .rte-content :deep(.tiptap p.is-editor-empty:first-child::before) {
178
+ content: attr(data-placeholder);
179
+ float: left;
180
+ color: var(--color-on-surface-variant);
181
+ opacity: 0.5;
182
+ pointer-events: none;
183
+ height: 0;
184
+ }
185
+
186
+ .rte-content :deep(h1) { font-size: var(--text-headline-large); line-height: var(--text-headline-large--line-height); font-weight: 600; margin: 0.75em 0 0.25em; }
187
+ .rte-content :deep(h2) { font-size: var(--text-headline-medium); line-height: var(--text-headline-medium--line-height); font-weight: 600; margin: 0.75em 0 0.25em; }
188
+ .rte-content :deep(h3) { font-size: var(--text-headline-small); line-height: var(--text-headline-small--line-height); font-weight: 600; margin: 0.75em 0 0.25em; }
189
+ .rte-content :deep(p) { margin: 0.5em 0; }
190
+ .rte-content :deep(ul),
191
+ .rte-content :deep(ol) { padding-left: 1.5em; margin: 0.5em 0; }
192
+ .rte-content :deep(blockquote) { border-left: 3px solid var(--color-primary); padding-left: 1em; margin: 0.5em 0; color: var(--color-on-surface-variant); }
193
+ .rte-content :deep(code) { background: var(--color-surface-container-highest); padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.875em; }
194
+ .rte-content :deep(pre) { background: var(--color-surface-container-highest); padding: 1em; border-radius: 8px; overflow-x: auto; margin: 0.5em 0; }
195
+ .rte-content :deep(pre code) { background: none; padding: 0; }
196
+ .rte-content :deep(a) { color: var(--color-primary); text-decoration: underline; }
197
+ .rte-content :deep(mark) { background: var(--color-tertiary-container); color: var(--color-on-tertiary-container); padding: 0.1em 0.2em; border-radius: 2px; }
198
+ .rte-content :deep(img) { max-width: 100%; height: auto; border-radius: 8px; margin: 0.5em 0; }
199
+ </style>
@@ -0,0 +1,225 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import MIconButton from './MIconButton.vue'
4
+
5
+ export interface SchedulerEvent {
6
+ id: string | number
7
+ title: string
8
+ start: string
9
+ end: string
10
+ color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'success'
11
+ }
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ events?: SchedulerEvent[]
16
+ view?: 'week' | 'day'
17
+ startHour?: number
18
+ endHour?: number
19
+ locale?: string
20
+ }>(),
21
+ {
22
+ events: () => [],
23
+ view: 'week',
24
+ startHour: 7,
25
+ endHour: 22,
26
+ locale: 'es-ES',
27
+ },
28
+ )
29
+
30
+ const emit = defineEmits<{
31
+ eventClick: [SchedulerEvent]
32
+ slotClick: [{ date: string; hour: number }]
33
+ }>()
34
+
35
+ const currentDate = ref(new Date())
36
+ const currentView = ref(props.view)
37
+
38
+ const hours = computed(() =>
39
+ Array.from({ length: props.endHour - props.startHour }, (_, i) => props.startHour + i),
40
+ )
41
+
42
+ function startOfWeek(d: Date) {
43
+ const dt = new Date(d)
44
+ const day = dt.getDay()
45
+ const diff = day === 0 ? -6 : 1 - day
46
+ dt.setDate(dt.getDate() + diff)
47
+ dt.setHours(0, 0, 0, 0)
48
+ return dt
49
+ }
50
+
51
+ const weekDays = computed(() => {
52
+ const start = startOfWeek(currentDate.value)
53
+ return Array.from({ length: 7 }, (_, i) => {
54
+ const d = new Date(start)
55
+ d.setDate(d.getDate() + i)
56
+ return d
57
+ })
58
+ })
59
+
60
+ const visibleDays = computed(() =>
61
+ currentView.value === 'day' ? [currentDate.value] : weekDays.value,
62
+ )
63
+
64
+ const todayIso = (() => {
65
+ const d = new Date()
66
+ return fmt(d)
67
+ })()
68
+
69
+ function fmt(d: Date) {
70
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
71
+ }
72
+
73
+ const dayFormat = new Intl.DateTimeFormat(props.locale, { weekday: 'short' })
74
+ const dateFormat = new Intl.DateTimeFormat(props.locale, { day: 'numeric' })
75
+
76
+ const headerLabel = computed(() => {
77
+ if (currentView.value === 'day') {
78
+ return new Intl.DateTimeFormat(props.locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
79
+ .format(currentDate.value)
80
+ }
81
+ const start = weekDays.value[0]!
82
+ const end = weekDays.value[6]!
83
+ const f = new Intl.DateTimeFormat(props.locale, { day: 'numeric', month: 'short' })
84
+ return `${f.format(start)} – ${f.format(end)}, ${end.getFullYear()}`
85
+ })
86
+
87
+ function navigate(delta: number) {
88
+ const d = new Date(currentDate.value)
89
+ if (currentView.value === 'week') d.setDate(d.getDate() + delta * 7)
90
+ else d.setDate(d.getDate() + delta)
91
+ currentDate.value = d
92
+ }
93
+
94
+ function goToday() { currentDate.value = new Date() }
95
+
96
+ function eventsForDayHour(day: Date, hour: number) {
97
+ const dayStr = fmt(day)
98
+ return props.events.filter((ev) => {
99
+ const start = new Date(ev.start)
100
+ const end = new Date(ev.end)
101
+ const evDay = fmt(start)
102
+ if (evDay !== dayStr) return false
103
+ const evStartHour = start.getHours()
104
+ const evEndHour = end.getHours() + (end.getMinutes() > 0 ? 1 : 0)
105
+ return hour >= evStartHour && hour < evEndHour
106
+ })
107
+ }
108
+
109
+ function isEventStart(ev: SchedulerEvent, hour: number) {
110
+ return new Date(ev.start).getHours() === hour
111
+ }
112
+
113
+ function eventDuration(ev: SchedulerEvent) {
114
+ const start = new Date(ev.start)
115
+ const end = new Date(ev.end)
116
+ return Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 3600000))
117
+ }
118
+
119
+ function timeLabel(ev: SchedulerEvent) {
120
+ const f = new Intl.DateTimeFormat(props.locale, { hour: '2-digit', minute: '2-digit' })
121
+ return `${f.format(new Date(ev.start))} – ${f.format(new Date(ev.end))}`
122
+ }
123
+
124
+ const eventColors: Record<string, string> = {
125
+ primary: 'bg-primary-container text-on-primary-container border-primary/30',
126
+ secondary: 'bg-secondary-container text-on-secondary-container border-secondary/30',
127
+ tertiary: 'bg-tertiary-container text-on-tertiary-container border-tertiary/30',
128
+ error: 'bg-error-container text-on-error-container border-error/30',
129
+ success: 'bg-success-container text-on-success-container border-success/30',
130
+ }
131
+ </script>
132
+
133
+ <template>
134
+ <div class="flex flex-col overflow-hidden rounded-lg border border-outline-variant">
135
+ <!-- Header -->
136
+ <div class="flex items-center justify-between border-b border-outline-variant bg-surface-container px-4 py-3">
137
+ <div class="flex items-center gap-1">
138
+ <MIconButton icon="chevron_left" label="Anterior" :size="36" @click="navigate(-1)" />
139
+ <MIconButton icon="chevron_right" label="Siguiente" :size="36" @click="navigate(1)" />
140
+ <button
141
+ type="button"
142
+ class="ml-2 cursor-pointer rounded-full border border-outline px-3 py-1 text-label-medium text-on-surface transition-colors hover:bg-on-surface/8"
143
+ @click="goToday"
144
+ >
145
+ Hoy
146
+ </button>
147
+ </div>
148
+
149
+ <h3 class="text-title-medium font-medium capitalize text-on-surface">{{ headerLabel }}</h3>
150
+
151
+ <div class="flex rounded-full bg-surface-container-high p-0.5">
152
+ <button
153
+ v-for="v in (['day', 'week'] as const)"
154
+ :key="v"
155
+ type="button"
156
+ class="cursor-pointer rounded-full px-3 py-1 text-label-medium transition-all duration-150"
157
+ :class="currentView === v ? 'bg-secondary-container text-on-secondary-container shadow-elevation-1' : 'text-on-surface-variant hover:bg-on-surface/8'"
158
+ @click="currentView = v"
159
+ >
160
+ {{ v === 'day' ? 'Día' : 'Semana' }}
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ <!-- Grid -->
166
+ <div class="overflow-auto">
167
+ <table class="w-full border-collapse">
168
+ <!-- Day headers -->
169
+ <thead>
170
+ <tr>
171
+ <th class="sticky top-0 z-10 w-16 border-b border-r border-outline-variant bg-surface-container p-2" />
172
+ <th
173
+ v-for="day in visibleDays"
174
+ :key="fmt(day)"
175
+ class="sticky top-0 z-10 border-b border-r border-outline-variant bg-surface-container px-2 py-2 text-center last:border-r-0"
176
+ :class="fmt(day) === todayIso ? 'bg-primary-container/30' : ''"
177
+ >
178
+ <div class="text-label-small uppercase text-on-surface-variant">{{ dayFormat.format(day) }}</div>
179
+ <div
180
+ class="mx-auto mt-0.5 flex h-8 w-8 items-center justify-center rounded-full text-title-medium"
181
+ :class="fmt(day) === todayIso ? 'bg-primary text-on-primary font-medium' : 'text-on-surface'"
182
+ >
183
+ {{ dateFormat.format(day) }}
184
+ </div>
185
+ </th>
186
+ </tr>
187
+ </thead>
188
+
189
+ <tbody>
190
+ <tr v-for="hour in hours" :key="hour">
191
+ <!-- Hour label -->
192
+ <td class="w-16 border-r border-b border-outline-variant/50 p-0 pr-2 text-right align-top">
193
+ <span class="relative -top-2.5 text-label-small text-on-surface-variant">
194
+ {{ String(hour).padStart(2, '0') }}:00
195
+ </span>
196
+ </td>
197
+
198
+ <!-- Day cells -->
199
+ <td
200
+ v-for="day in visibleDays"
201
+ :key="fmt(day)"
202
+ class="relative h-14 border-r border-b border-outline-variant/50 p-0 last:border-r-0"
203
+ :class="fmt(day) === todayIso ? 'bg-primary-container/[0.05]' : ''"
204
+ @click="emit('slotClick', { date: fmt(day), hour })"
205
+ >
206
+ <template v-for="ev in eventsForDayHour(day, hour)" :key="ev.id">
207
+ <button
208
+ v-if="isEventStart(ev, hour)"
209
+ type="button"
210
+ class="absolute inset-x-0.5 top-0.5 z-[5] cursor-pointer overflow-hidden rounded border-l-[3px] px-2 py-1 text-left transition-opacity hover:opacity-90"
211
+ :class="eventColors[ev.color ?? 'primary']"
212
+ :style="{ height: `calc(${eventDuration(ev) * 100}% - 4px)` }"
213
+ @click.stop="emit('eventClick', ev)"
214
+ >
215
+ <p class="truncate text-label-small font-medium">{{ ev.title }}</p>
216
+ <p class="truncate text-label-small opacity-70">{{ timeLabel(ev) }}</p>
217
+ </button>
218
+ </template>
219
+ </td>
220
+ </tr>
221
+ </tbody>
222
+ </table>
223
+ </div>
224
+ </div>
225
+ </template>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import MIcon from './MIcon.vue'
3
+
4
+ export interface SegmentedOption {
5
+ value: string | number
6
+ label: string
7
+ icon?: string
8
+ disabled?: boolean
9
+ }
10
+
11
+ withDefaults(defineProps<{
12
+ modelValue: string | number | (string | number)[]
13
+ options: SegmentedOption[]
14
+ multiSelect?: boolean
15
+ density?: 'default' | 'comfortable' | 'compact'
16
+ color?: 'primary' | 'secondary' | 'tertiary'
17
+ }>(), { multiSelect: false, density: 'default', color: 'primary' })
18
+
19
+ const emit = defineEmits<{ 'update:modelValue': [string | number | (string | number)[]] }>()
20
+
21
+ function isSelected(value: string | number, modelValue: string | number | (string | number)[]) {
22
+ return Array.isArray(modelValue) ? modelValue.includes(value) : modelValue === value
23
+ }
24
+
25
+ function toggle(opt: SegmentedOption, modelValue: string | number | (string | number)[], multi: boolean) {
26
+ if (opt.disabled) return
27
+ if (multi) {
28
+ const arr = Array.isArray(modelValue) ? [...modelValue] : [modelValue]
29
+ const idx = arr.indexOf(opt.value)
30
+ if (idx >= 0) arr.splice(idx, 1)
31
+ else arr.push(opt.value)
32
+ emit('update:modelValue', arr)
33
+ } else {
34
+ emit('update:modelValue', opt.value)
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <div
41
+ class="inline-flex overflow-hidden rounded-full border border-outline"
42
+ role="group"
43
+ >
44
+ <button
45
+ v-for="(opt, i) in options"
46
+ :key="opt.value"
47
+ type="button"
48
+ class="relative inline-flex items-center justify-center gap-2 text-label-large font-medium transition-[background-color,color] duration-150 outline-none before:pointer-events-none before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity before:duration-150 enabled:hover:before:opacity-[0.08] enabled:active:before:opacity-[0.12]"
49
+ :class="[
50
+ density === 'compact' ? 'h-8 px-3' : density === 'comfortable' ? 'h-10 px-4' : 'h-10 px-6',
51
+ i > 0 ? 'border-l border-outline' : '',
52
+ opt.disabled ? 'cursor-not-allowed opacity-[0.38]' : 'cursor-pointer',
53
+ isSelected(opt.value, modelValue)
54
+ ? color === 'secondary'
55
+ ? 'bg-secondary-container text-on-secondary-container'
56
+ : color === 'tertiary'
57
+ ? 'bg-tertiary-container text-on-tertiary-container'
58
+ : 'bg-secondary-container text-on-secondary-container'
59
+ : 'text-on-surface',
60
+ ]"
61
+ :disabled="opt.disabled"
62
+ :aria-pressed="isSelected(opt.value, modelValue)"
63
+ @click="toggle(opt, modelValue, multiSelect)"
64
+ >
65
+ <MIcon
66
+ v-if="isSelected(opt.value, modelValue)"
67
+ name="check"
68
+ :size="18"
69
+ class="transition-transform duration-150"
70
+ />
71
+ <MIcon v-else-if="opt.icon" :name="opt.icon" :size="18" />
72
+ <span>{{ opt.label }}</span>
73
+ </button>
74
+ </div>
75
+ </template>