@getcortexlabs/mavis-web 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (44) hide show
  1. package/README.md +3 -3
  2. package/dist/appearance.d.ts +4 -0
  3. package/dist/appearance.d.ts.map +1 -0
  4. package/dist/appearance.js +23 -0
  5. package/dist/appearance.js.map +1 -0
  6. package/dist/components/MButton.d.ts +25 -7
  7. package/dist/components/MButton.d.ts.map +1 -1
  8. package/dist/components/MButton.js +30 -3
  9. package/dist/components/MButton.js.map +1 -1
  10. package/dist/components/MIcon.d.ts +55 -0
  11. package/dist/components/MIcon.d.ts.map +1 -0
  12. package/dist/components/MIcon.js +31 -0
  13. package/dist/components/MIcon.js.map +1 -0
  14. package/dist/components/MInput.d.ts +31 -13
  15. package/dist/components/MInput.d.ts.map +1 -1
  16. package/dist/components/MInput.js +87 -65
  17. package/dist/components/MInput.js.map +1 -1
  18. package/dist/components/MScreen.d.ts +9 -0
  19. package/dist/components/MScreen.d.ts.map +1 -1
  20. package/dist/components/MScreen.js +17 -0
  21. package/dist/components/MScreen.js.map +1 -1
  22. package/dist/components/MSheet.d.ts +96 -0
  23. package/dist/components/MSheet.d.ts.map +1 -0
  24. package/dist/components/MSheet.js +238 -0
  25. package/dist/components/MSheet.js.map +1 -0
  26. package/dist/index.d.ts +8 -6
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +11 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/style.d.ts.map +1 -1
  31. package/dist/style.js +24 -9
  32. package/dist/style.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/appearance.ts +23 -0
  35. package/src/components/MButton.ts +41 -3
  36. package/src/components/MIcon.ts +39 -0
  37. package/src/components/MInput.ts +88 -64
  38. package/src/components/MScreen.ts +14 -0
  39. package/src/components/MSheet.ts +268 -0
  40. package/src/fonts/LICENSE +45 -0
  41. package/src/fonts/lucide.woff2 +0 -0
  42. package/src/index.ts +37 -6
  43. package/src/style.css +87 -0
  44. package/src/style.ts +20 -9
@@ -0,0 +1,39 @@
1
+ import { defineComponent, h, mergeProps } from 'vue'
2
+ import { iconProps, useMavisTheme } from '@getcortexlabs/mavis-core/vue'
3
+ import { iconFontFamily, iconGlyph, resolveIconSize } from '@getcortexlabs/mavis-core'
4
+ import { webAttrs } from '../style.js'
5
+
6
+ export const MIcon = defineComponent({
7
+ name: 'MIcon',
8
+ inheritAttrs: false,
9
+ props: iconProps,
10
+ setup(props, { attrs }) {
11
+ const { theme } = useMavisTheme()
12
+ return () => {
13
+ const size = `${resolveIconSize(props.size)}px`
14
+ return h(
15
+ 'span',
16
+ mergeProps(
17
+ {
18
+ class: 'm-icon',
19
+ style: {
20
+ fontFamily: iconFontFamily,
21
+ fontSize: size,
22
+ width: size,
23
+ height: size,
24
+ lineHeight: 1,
25
+ color: theme.value.colors[props.tone],
26
+ display: 'inline-flex',
27
+ alignItems: 'center',
28
+ justifyContent: 'center',
29
+ flexShrink: 0,
30
+ },
31
+ },
32
+ webAttrs({ ...attrs, id: props.id }),
33
+ props.label ? { role: 'img', 'aria-label': props.label } : { 'aria-hidden': 'true' },
34
+ ),
35
+ iconGlyph(props.name),
36
+ )
37
+ }
38
+ },
39
+ })
@@ -19,9 +19,11 @@ import {
19
19
  nonNegative,
20
20
  resolveDimension,
21
21
  validateInput,
22
+ iconSizes,
22
23
  type InputHandle,
23
24
  } from '@getcortexlabs/mavis-core'
24
25
  import { length, webAttrs } from '../style.js'
26
+ import { MIcon } from './MIcon.js'
25
27
 
26
28
  export const MInput = defineComponent({
27
29
  name: 'MInput',
@@ -98,6 +100,22 @@ export const MInput = defineComponent({
98
100
  const message = props.error || props.helperText
99
101
  const text = props.type === 'text' || props.type === 'search'
100
102
  const border = props.error ? colors.danger : focused.value ? colors.primary : colors.border
103
+ const inset = 16 + iconSizes.md + 10
104
+ const icon = (name: typeof props.icon, trailing: boolean) =>
105
+ name
106
+ ? h(MIcon, {
107
+ name,
108
+ size: 'md',
109
+ tone: props.error ? 'danger' : 'muted',
110
+ style: {
111
+ position: 'absolute',
112
+ [trailing ? 'right' : 'left']: '16px',
113
+ top: props.multiline ? '16px' : '50%',
114
+ transform: props.multiline ? undefined : 'translateY(-50%)',
115
+ pointerEvents: 'none',
116
+ },
117
+ })
118
+ : null
101
119
  return h(
102
120
  'div',
103
121
  mergeProps(
@@ -131,70 +149,76 @@ export const MInput = defineComponent({
131
149
  },
132
150
  props.label,
133
151
  ),
134
- h(props.multiline ? 'textarea' : 'input', {
135
- ref: editor,
136
- id,
137
- class: 'm-input-editor',
138
- type: props.multiline ? undefined : props.type,
139
- rows: props.multiline ? props.rows : undefined,
140
- value: props.modelValue,
141
- placeholder: props.placeholder,
142
- disabled: props.disabled,
143
- readonly: props.readonly,
144
- maxlength: props.maxLength,
145
- autocapitalize: props.autocapitalize ?? (text ? 'sentences' : 'none'),
146
- autocorrect: (props.autocorrect ?? text) ? 'on' : 'off',
147
- spellcheck: props.autocorrect ?? text,
148
- enterkeyhint: props.multiline ? 'enter' : props.returnKey,
149
- 'aria-invalid': props.error ? 'true' : undefined,
150
- 'aria-describedby': message ? `${id}-message` : undefined,
151
- style: {
152
- boxSizing: 'border-box',
153
- width: '100%',
154
- minWidth: 0,
155
- minHeight: `${props.multiline ? props.rows * typography.body.fontSize * 1.5 + 30 : control.md.height}px`,
156
- margin: 0,
157
- padding: '14px 16px',
158
- border: `1px solid ${border}`,
159
- borderRadius: `${radius.md}px`,
160
- outline: 'none',
161
- boxShadow: focused.value ? `0 0 0 2px ${border}` : 'none',
162
- fontFamily: 'inherit',
163
- fontSize: `${typography.body.fontSize}px`,
164
- lineHeight: '1.5',
165
- backgroundColor: props.readonly ? colors.elevated : colors.surface,
166
- color: colors.text,
167
- caretColor: colors.primary,
168
- resize: 'none',
169
- overflow: props.multiline ? 'hidden' : undefined,
170
- },
171
- onInput: (event: Event) => {
172
- if (props.disabled || props.readonly) return
173
- const value = (event.target as HTMLInputElement).value
174
- if (value !== props.modelValue) emit('update:modelValue', value)
175
- resize()
176
- },
177
- onFocus: () => {
178
- focused.value = true
179
- emit('focus')
180
- },
181
- onBlur: () => {
182
- focused.value = false
183
- emit('blur')
184
- },
185
- onKeydown: (event: KeyboardEvent) => {
186
- if (
187
- event.key !== 'Enter' ||
188
- event.isComposing ||
189
- event.keyCode === 229 ||
190
- props.multiline
191
- )
192
- return
193
- event.preventDefault()
194
- if (!props.disabled && !props.readonly)
195
- emit('submit', (event.target as HTMLInputElement).value)
196
- },
197
- }),
152
+ h('div', { class: 'm-input-field', style: { position: 'relative', display: 'flex' } }, [
153
+ h(props.multiline ? 'textarea' : 'input', {
154
+ ref: editor,
155
+ id,
156
+ class: 'm-input-editor',
157
+ type: props.multiline ? undefined : props.type,
158
+ rows: props.multiline ? props.rows : undefined,
159
+ value: props.modelValue,
160
+ placeholder: props.placeholder,
161
+ disabled: props.disabled,
162
+ readonly: props.readonly,
163
+ maxlength: props.maxLength,
164
+ autocapitalize: props.autocapitalize ?? (text ? 'sentences' : 'none'),
165
+ autocorrect: (props.autocorrect ?? text) ? 'on' : 'off',
166
+ spellcheck: props.autocorrect ?? text,
167
+ enterkeyhint: props.multiline ? 'enter' : props.returnKey,
168
+ 'aria-invalid': props.error ? 'true' : undefined,
169
+ 'aria-describedby': message ? `${id}-message` : undefined,
170
+ style: {
171
+ boxSizing: 'border-box',
172
+ width: '100%',
173
+ minWidth: 0,
174
+ minHeight: `${props.multiline ? props.rows * typography.body.fontSize * 1.5 + 30 : control.md.height}px`,
175
+ margin: 0,
176
+ padding: '14px 16px',
177
+ paddingLeft: props.icon ? `${inset}px` : undefined,
178
+ paddingRight: props.trailingIcon ? `${inset}px` : undefined,
179
+ border: `1px solid ${border}`,
180
+ borderRadius: `${radius.md}px`,
181
+ outline: 'none',
182
+ boxShadow: focused.value ? `0 0 0 2px ${border}` : 'none',
183
+ fontFamily: 'inherit',
184
+ fontSize: `${typography.body.fontSize}px`,
185
+ lineHeight: '1.5',
186
+ backgroundColor: props.readonly ? colors.elevated : colors.surface,
187
+ color: colors.text,
188
+ caretColor: colors.primary,
189
+ resize: 'none',
190
+ overflow: props.multiline ? 'hidden' : undefined,
191
+ },
192
+ onInput: (event: Event) => {
193
+ if (props.disabled || props.readonly) return
194
+ const value = (event.target as HTMLInputElement).value
195
+ if (value !== props.modelValue) emit('update:modelValue', value)
196
+ resize()
197
+ },
198
+ onFocus: () => {
199
+ focused.value = true
200
+ emit('focus')
201
+ },
202
+ onBlur: () => {
203
+ focused.value = false
204
+ emit('blur')
205
+ },
206
+ onKeydown: (event: KeyboardEvent) => {
207
+ if (
208
+ event.key !== 'Enter' ||
209
+ event.isComposing ||
210
+ event.keyCode === 229 ||
211
+ props.multiline
212
+ )
213
+ return
214
+ event.preventDefault()
215
+ if (!props.disabled && !props.readonly)
216
+ emit('submit', (event.target as HTMLInputElement).value)
217
+ },
218
+ }),
219
+ icon(props.icon, false),
220
+ icon(props.trailingIcon, true),
221
+ ]),
198
222
  message
199
223
  ? h(
200
224
  'p',
@@ -16,6 +16,19 @@ export const MScreen = defineComponent({
16
16
  const { theme } = useMavisTheme()
17
17
  const element = useScreenViewport(() => props.scroll)
18
18
  provide(layoutDirection, 'column')
19
+ // Browsers already move focus away on a tap outside the editor; a drag keeps the keyboard.
20
+ const releaseKeyboard = (event: TouchEvent) => {
21
+ if (!props.dismissKeyboard || !props.scroll) return
22
+ const screen = element.value
23
+ const active = document.activeElement
24
+ if (!(active instanceof HTMLElement) || !screen || active === screen) return
25
+ if (!screen.contains(active) || active.contains(event.target as Node)) return
26
+ const editor =
27
+ active instanceof HTMLInputElement ||
28
+ active instanceof HTMLTextAreaElement ||
29
+ active.isContentEditable
30
+ if (editor) active.blur()
31
+ }
19
32
  return () =>
20
33
  h(
21
34
  'main',
@@ -24,6 +37,7 @@ export const MScreen = defineComponent({
24
37
  ref: element,
25
38
  class: 'm-screen',
26
39
  tabindex: props.scroll ? 0 : -1,
40
+ onTouchmovePassive: releaseKeyboard,
27
41
  style: {
28
42
  display: 'flex',
29
43
  flexDirection: 'column',
@@ -0,0 +1,268 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ nextTick,
5
+ onBeforeUnmount,
6
+ onMounted,
7
+ provide,
8
+ ref,
9
+ useId,
10
+ watch,
11
+ } from 'vue'
12
+ import {
13
+ sheetProps,
14
+ sheetEmits,
15
+ useMavisTheme,
16
+ layoutDirection,
17
+ } from '@getcortexlabs/mavis-core/vue'
18
+ import { checkSheetName, resolveSpace, type SheetCloseReason } from '@getcortexlabs/mavis-core'
19
+ import { MRow, MColumn, MBox } from './layout.js'
20
+ import { MText } from './MText.js'
21
+ import { MButton } from './MButton.js'
22
+
23
+ /** The longest duration in a `transition-duration` list, in milliseconds. */
24
+ function longestDuration(value: string) {
25
+ return Math.max(
26
+ 0,
27
+ ...value.split(',').map((part) => {
28
+ const trimmed = part.trim()
29
+ const number = Number.parseFloat(trimmed)
30
+ if (!Number.isFinite(number)) return 0
31
+ return trimmed.endsWith('ms') ? number : number * 1000
32
+ }),
33
+ )
34
+ }
35
+
36
+ export const MSheet = defineComponent({
37
+ name: 'MSheet',
38
+ inheritAttrs: false,
39
+ props: sheetProps,
40
+ emits: sheetEmits,
41
+ setup(props, { slots, emit }) {
42
+ const { theme } = useMavisTheme()
43
+ provide(layoutDirection, 'column')
44
+ const titleId = `m-sheet-${useId()}`
45
+ const dialog = ref<HTMLDialogElement>()
46
+ const presented = ref(false)
47
+ const shown = ref(false)
48
+ let closing: SheetCloseReason | undefined
49
+ let generation = 0
50
+ let timer: ReturnType<typeof setTimeout> | undefined
51
+ let outside = false
52
+ let opener: HTMLElement | undefined
53
+
54
+ const announce = (reason: SheetCloseReason) => {
55
+ closing = reason
56
+ emit('update:open', false)
57
+ emit('close', reason)
58
+ }
59
+ const request = (reason: SheetCloseReason) => {
60
+ if (props.open && !closing) announce(reason)
61
+ }
62
+
63
+ const focusable = (element: HTMLElement) =>
64
+ [
65
+ ...element.querySelectorAll<HTMLElement>(
66
+ 'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
67
+ ),
68
+ ].filter((control) => !control.closest('[inert]'))
69
+ // Engines differ on which control a modal dialog focuses first; start at the first control,
70
+ // or the panel itself, unless the content asked for focus with `autofocus`.
71
+ function focusInitial(element: HTMLDialogElement) {
72
+ if (typeof document === 'undefined') return
73
+ const active = document.activeElement
74
+ if (active && active !== element && element.contains(active)) return
75
+ const panel = element.querySelector<HTMLElement>('.m-sheet-panel')
76
+ if (panel) panel.tabIndex = -1
77
+ ;(focusable(element)[0] ?? panel)?.focus({ preventScroll: true })
78
+ }
79
+ // A modal dialog makes the page inert, yet Tab can still leave for the browser's own controls.
80
+ function trapTab(event: KeyboardEvent) {
81
+ const element = dialog.value
82
+ if (event.key !== 'Tab' || !element) return
83
+ const controls = focusable(element)
84
+ const first = controls[0]
85
+ const last = controls[controls.length - 1]
86
+ if (!first || !last) return
87
+ const active = document.activeElement
88
+ if (event.shiftKey && (active === first || active === element)) {
89
+ event.preventDefault()
90
+ last.focus()
91
+ } else if (!event.shiftKey && active === last) {
92
+ event.preventDefault()
93
+ first.focus()
94
+ }
95
+ }
96
+ // The dialog restores focus when it closes; some engines leave it on the body instead.
97
+ function restoreFocus() {
98
+ const target = opener
99
+ opener = undefined
100
+ const active = typeof document === 'undefined' ? null : document.activeElement
101
+ if (target?.isConnected && (!active || active === document.body))
102
+ target.focus({ preventScroll: true })
103
+ }
104
+ function present() {
105
+ const current = ++generation
106
+ clearTimeout(timer)
107
+ presented.value = true
108
+ if (typeof document !== 'undefined' && document.activeElement instanceof HTMLElement)
109
+ opener = document.activeElement
110
+ nextTick(() => {
111
+ const element = dialog.value
112
+ if (generation !== current || !element) return
113
+ if (typeof element.showModal === 'function' && !element.open) element.showModal()
114
+ focusInitial(element)
115
+ // Resolve the offscreen position before the open class so the entrance transitions.
116
+ element.getBoundingClientRect?.()
117
+ shown.value = true
118
+ })
119
+ }
120
+ function leave() {
121
+ const element = dialog.value
122
+ const current = ++generation
123
+ shown.value = false
124
+ let release = () => {}
125
+ const finish = () => {
126
+ if (generation !== current) return
127
+ clearTimeout(timer)
128
+ release()
129
+ if (element?.open) element.close()
130
+ presented.value = false
131
+ closing = undefined
132
+ restoreFocus()
133
+ }
134
+ const duration =
135
+ element && typeof getComputedStyle === 'function'
136
+ ? longestDuration(getComputedStyle(element).transitionDuration)
137
+ : 0
138
+ if (!duration || !element) {
139
+ finish()
140
+ return
141
+ }
142
+ const ended = (event: Event) => {
143
+ if (event.target === element) finish()
144
+ }
145
+ element.addEventListener('transitionend', ended)
146
+ release = () => element.removeEventListener('transitionend', ended)
147
+ timer = setTimeout(finish, duration + 100)
148
+ }
149
+
150
+ watch(
151
+ () => props.open,
152
+ (open) => {
153
+ if (open) {
154
+ closing = undefined
155
+ present()
156
+ } else if (presented.value) {
157
+ if (!closing) {
158
+ closing = 'program'
159
+ emit('close', 'program')
160
+ }
161
+ leave()
162
+ }
163
+ },
164
+ )
165
+ onMounted(() => {
166
+ if (props.open) present()
167
+ })
168
+ onBeforeUnmount(() => {
169
+ generation++
170
+ clearTimeout(timer)
171
+ })
172
+
173
+ return () => {
174
+ checkSheetName(props.title, props.accessibilityLabel)
175
+ const { colors, radius, space } = theme.value
176
+ const header =
177
+ props.title || props.dismissible
178
+ ? h(
179
+ MRow,
180
+ { gap: 'sm', justify: 'between' },
181
+ {
182
+ default: () => [
183
+ props.title
184
+ ? h(MText, {
185
+ as: 'h2',
186
+ variant: 'heading',
187
+ grow: 1,
188
+ id: titleId,
189
+ text: props.title,
190
+ })
191
+ : h(MBox, { grow: 1 }),
192
+ props.dismissible
193
+ ? h(MButton, {
194
+ variant: 'ghost',
195
+ size: 'sm',
196
+ icon: 'x',
197
+ shrink: 0,
198
+ accessibilityLabel: props.closeLabel,
199
+ onClick: () => request('button'),
200
+ })
201
+ : null,
202
+ ],
203
+ },
204
+ )
205
+ : null
206
+ return h(
207
+ 'dialog',
208
+ {
209
+ ref: dialog,
210
+ id: props.id,
211
+ class: ['m-sheet', { 'm-sheet-open': shown.value, 'm-sheet-full': props.fullscreen }],
212
+ 'aria-labelledby': props.title ? titleId : undefined,
213
+ 'aria-label': props.title ? undefined : props.accessibilityLabel,
214
+ style: {
215
+ '--m-sheet-surface': colors.surface,
216
+ '--m-sheet-radius': `${radius.lg}px`,
217
+ '--m-focus': colors.primary,
218
+ color: colors.text,
219
+ colorScheme: theme.value.appearance,
220
+ },
221
+ onCancel: (event: Event) => {
222
+ // The browser would close at once; the sheet leaves through its own transition.
223
+ event.preventDefault()
224
+ if (props.dismissible) request('escape')
225
+ },
226
+ onClose: () => {
227
+ // Closed by the browser itself: keep the bound state in step.
228
+ if (props.open && !closing) announce('escape')
229
+ shown.value = false
230
+ presented.value = false
231
+ restoreFocus()
232
+ },
233
+ onKeydown: trapTab,
234
+ onPointerdown: (event: Event) => {
235
+ outside = event.target === event.currentTarget
236
+ },
237
+ onClick: (event: Event) => {
238
+ if (outside && event.target === event.currentTarget && props.dismissible)
239
+ request('backdrop')
240
+ outside = false
241
+ },
242
+ },
243
+ presented.value
244
+ ? [
245
+ h(
246
+ 'div',
247
+ {
248
+ class: 'm-sheet-panel',
249
+ style: {
250
+ padding: `${resolveSpace(theme.value, props.padding ?? 'lg')}px`,
251
+ gap: `${space.md}px`,
252
+ },
253
+ },
254
+ [
255
+ header,
256
+ h(
257
+ MColumn,
258
+ { class: 'm-sheet-body', shrink: 1, gap: props.gap, align: props.align },
259
+ { default: () => slots.default?.() },
260
+ ),
261
+ ],
262
+ ),
263
+ ]
264
+ : undefined,
265
+ )
266
+ }
267
+ },
268
+ })
@@ -0,0 +1,45 @@
1
+ Lucide icon font 1.65.0
2
+
3
+ ISC License
4
+
5
+ Copyright (c) 2026 Lucide Icons and Contributors
6
+
7
+ Permission to use, copy, modify, and/or distribute this software for any
8
+ purpose with or without fee is hereby granted, provided that the above
9
+ copyright notice and this permission notice appear in all copies.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
+
19
+ ---
20
+
21
+ The following Lucide icons are derived from the Feather project:
22
+
23
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
24
+
25
+ The MIT License (MIT) (for the icons listed above)
26
+
27
+ Copyright (c) 2013-present Cole Bemis
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining a copy
30
+ of this software and associated documentation files (the "Software"), to deal
31
+ in the Software without restriction, including without limitation the rights
32
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
33
+ copies of the Software, and to permit persons to whom the Software is
34
+ furnished to do so, subject to the following conditions:
35
+
36
+ The above copyright notice and this permission notice shall be included in all
37
+ copies or substantial portions of the Software.
38
+
39
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
41
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
42
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
43
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
44
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
45
+ SOFTWARE.
Binary file
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { App } from 'vue'
2
- import { mavis, type MavisTheme } from '@getcortexlabs/mavis-core'
3
- import { createThemeContext, themeKey } from '@getcortexlabs/mavis-core/vue'
2
+ import {
3
+ createThemeContext,
4
+ themeKey,
5
+ type ThemeContextOptions,
6
+ } from '@getcortexlabs/mavis-core/vue'
7
+ import { systemAppearance } from './appearance.js'
4
8
  import { MScreen } from './components/MScreen.js'
5
9
  import { MBox, MRow, MColumn } from './components/layout.js'
6
10
  import { MCard } from './components/MCard.js'
@@ -8,18 +12,43 @@ import { MText } from './components/MText.js'
8
12
  import { MButton } from './components/MButton.js'
9
13
  import { MSlider } from './components/MSlider.js'
10
14
  import { MInput } from './components/MInput.js'
15
+ import { MIcon } from './components/MIcon.js'
11
16
  import { MReveal } from './components/MReveal.js'
17
+ import { MSheet } from './components/MSheet.js'
12
18
 
13
- export { MScreen, MBox, MRow, MColumn, MCard, MText, MButton, MSlider, MInput, MReveal }
19
+ export {
20
+ MScreen,
21
+ MBox,
22
+ MRow,
23
+ MColumn,
24
+ MCard,
25
+ MText,
26
+ MButton,
27
+ MSlider,
28
+ MInput,
29
+ MIcon,
30
+ MReveal,
31
+ MSheet,
32
+ }
14
33
  export { useMavisTheme } from '@getcortexlabs/mavis-core/vue'
15
- export type { MavisThemeContext } from '@getcortexlabs/mavis-core/vue'
34
+ export type {
35
+ MavisThemeContext,
36
+ Appearance,
37
+ AppearanceMode,
38
+ AppearanceSource,
39
+ } from '@getcortexlabs/mavis-core/vue'
16
40
  export type { Align, Justify } from './components/layout.js'
17
41
  export * from '@getcortexlabs/mavis-core'
18
42
 
19
- export function createMavis(options: { theme?: MavisTheme } = {}) {
43
+ export type MavisOptions = Omit<ThemeContextOptions, 'source'>
44
+
45
+ /** Create one plugin per application; `system` appearance follows `prefers-color-scheme`. */
46
+ export function createMavis(options: MavisOptions = {}) {
20
47
  return {
21
48
  install(app: App) {
22
- app.provide(themeKey, createThemeContext(options.theme ?? mavis))
49
+ const context = createThemeContext({ ...options, source: systemAppearance() })
50
+ app.provide(themeKey, context)
51
+ app.onUnmount(() => context.dispose())
23
52
  for (const [name, component] of Object.entries({
24
53
  MScreen,
25
54
  MBox,
@@ -30,7 +59,9 @@ export function createMavis(options: { theme?: MavisTheme } = {}) {
30
59
  MButton,
31
60
  MSlider,
32
61
  MInput,
62
+ MIcon,
33
63
  MReveal,
64
+ MSheet,
34
65
  })) {
35
66
  app.component(name, component)
36
67
  }