@getcortexlabs/mavis-core 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.
package/src/index.ts CHANGED
@@ -16,4 +16,9 @@ export { sliderScale } from './slider.js'
16
16
  export { resolveDimension, resolveRadius, nonNegative } from './layout.js'
17
17
  export type { Dimension } from './layout.js'
18
18
  export { validateInput } from './input.js'
19
+ export { icons, iconFontVersion } from './icons.js'
20
+ export { iconGlyph, iconFontFamily, iconSizes, resolveIconSize, checkIconOnly } from './icon.js'
21
+ export type { IconName, IconSize } from './icon.js'
19
22
  export type { InputHandle } from './input.js'
23
+ export { checkSheetName, sheetCloseReasons } from './sheet.js'
24
+ export type { SheetCloseReason } from './sheet.js'
package/src/sheet.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** Why a sheet closed: the component's own control, a platform dismissal, or `open` set to false. */
2
+ export type SheetCloseReason = 'button' | 'backdrop' | 'escape' | 'back' | 'swipe' | 'program'
3
+
4
+ export const sheetCloseReasons: readonly SheetCloseReason[] = [
5
+ 'button',
6
+ 'backdrop',
7
+ 'escape',
8
+ 'back',
9
+ 'swipe',
10
+ 'program',
11
+ ]
12
+
13
+ export function checkSheetName(title: string | undefined, label: string | undefined) {
14
+ if (!title && !label) {
15
+ throw new RangeError('A sheet needs a title or an accessibilityLabel')
16
+ }
17
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DefineComponent } from '@vue/runtime-core'
2
2
  import type { ComponentProps } from './props.js'
3
+ import type { SheetCloseReason } from '../sheet.js'
3
4
 
4
5
  type Action = { onClick?: () => void }
5
6
  type Model = { 'onUpdate:modelValue'?: (value: number) => void }
@@ -9,6 +10,11 @@ type InputEvents = {
9
10
  onBlur?: () => void
10
11
  onSubmit?: (value: string) => void
11
12
  }
13
+ type SheetEvents = {
14
+ 'onUpdate:open'?: (value: boolean) => void
15
+ onClose?: (reason: SheetCloseReason) => void
16
+ onOpened?: () => void
17
+ }
12
18
 
13
19
  /** Template props come from the same declarations used by both renderers. */
14
20
  declare module '@vue/runtime-core' {
@@ -22,6 +28,8 @@ declare module '@vue/runtime-core' {
22
28
  MButton: DefineComponent<ComponentProps['MButton'] & Action>
23
29
  MSlider: DefineComponent<ComponentProps['MSlider'] & Model>
24
30
  MInput: DefineComponent<ComponentProps['MInput'] & InputEvents>
31
+ MIcon: DefineComponent<ComponentProps['MIcon']>
25
32
  MReveal: DefineComponent<ComponentProps['MReveal']>
33
+ MSheet: DefineComponent<ComponentProps['MSheet'] & SheetEvents>
26
34
  }
27
35
  }
package/src/vue/index.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export { createThemeContext, themeKey, useMavisTheme } from './theme.js'
2
- export type { MavisThemeContext } from './theme.js'
2
+ export type {
3
+ MavisThemeContext,
4
+ Appearance,
5
+ AppearanceMode,
6
+ AppearanceSource,
7
+ ThemeContextOptions,
8
+ } from './theme.js'
3
9
  export { layoutDirection, useDefaultShrink } from './layout-context.js'
4
10
  export { textContent } from './text-content.js'
5
11
  export type {} from './global-components.js'
@@ -14,8 +20,11 @@ export {
14
20
  sliderProps,
15
21
  inputProps,
16
22
  inputEmits,
23
+ iconProps,
17
24
  screenProps,
18
25
  revealProps,
26
+ sheetProps,
27
+ sheetEmits,
19
28
  componentProps,
20
29
  viewAttrs,
21
30
  actionEmits,
package/src/vue/props.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ExtractPropTypes, ExtractPublicPropTypes, PropType } from '@vue/runtime-core'
2
2
  import type { Palette, Spacing, Radius } from '../theme/types.js'
3
3
  import type { Dimension } from '../layout.js'
4
+ import { iconSizes, type IconName, type IconSize } from '../icon.js'
5
+ import { sheetCloseReasons, type SheetCloseReason } from '../sheet.js'
4
6
 
5
7
  function choice<T extends string>(values: readonly T[], defaultValue: T) {
6
8
  return {
@@ -77,32 +79,32 @@ export const buttonProps = {
77
79
  disabled: Boolean,
78
80
  loading: Boolean,
79
81
  loadingText: { type: String, default: 'Loading…' },
82
+ icon: { type: String as PropType<IconName>, default: undefined },
83
+ trailingIcon: { type: String as PropType<IconName>, default: undefined },
80
84
  }
85
+ const tones: readonly (keyof Palette)[] = [
86
+ 'background',
87
+ 'surface',
88
+ 'elevated',
89
+ 'text',
90
+ 'muted',
91
+ 'primary',
92
+ 'onPrimary',
93
+ 'primarySoft',
94
+ 'accent',
95
+ 'onAccent',
96
+ 'accentSoft',
97
+ 'border',
98
+ 'success',
99
+ 'danger',
100
+ ]
81
101
  export const textProps = {
82
102
  ...viewProps,
83
103
  ...flexProps,
84
104
  text: { type: String, default: undefined },
85
105
  as: choice(['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const, 'p'),
86
106
  variant: choice(['display', 'title', 'heading', 'body', 'caption', 'overline'] as const, 'body'),
87
- tone: choice<keyof Palette>(
88
- [
89
- 'background',
90
- 'surface',
91
- 'elevated',
92
- 'text',
93
- 'muted',
94
- 'primary',
95
- 'onPrimary',
96
- 'primarySoft',
97
- 'accent',
98
- 'onAccent',
99
- 'accentSoft',
100
- 'border',
101
- 'success',
102
- 'danger',
103
- ],
104
- 'text',
105
- ),
107
+ tone: choice<keyof Palette>(tones, 'text'),
106
108
  align: choice(['left', 'center', 'right'] as const, 'left'),
107
109
  wrap: { type: Boolean, default: true },
108
110
  }
@@ -139,6 +141,8 @@ export const inputProps = {
139
141
  },
140
142
  autocorrect: { type: Boolean, default: undefined },
141
143
  returnKey: choice(['done', 'next', 'go', 'search', 'send'] as const, 'done'),
144
+ icon: { type: String as PropType<IconName>, default: undefined },
145
+ trailingIcon: { type: String as PropType<IconName>, default: undefined },
142
146
  }
143
147
  export const inputEmits = {
144
148
  'update:modelValue': (value: string) => typeof value === 'string',
@@ -146,8 +150,44 @@ export const inputEmits = {
146
150
  blur: () => true,
147
151
  submit: (value: string) => typeof value === 'string',
148
152
  }
149
- export const screenProps = { ...viewProps, scroll: Boolean }
153
+ export const iconProps = {
154
+ id: viewProps.id,
155
+ name: { type: String as PropType<IconName>, required: true as const },
156
+ size: {
157
+ type: [String, Number] as PropType<IconSize>,
158
+ default: 'md',
159
+ validator: (value: IconSize) => typeof value === 'number' || value in iconSizes,
160
+ },
161
+ tone: choice<keyof Palette>(tones, 'text'),
162
+ /** Accessible name. Without it the icon is decorative and hidden from assistive technology. */
163
+ label: { type: String, default: undefined },
164
+ }
165
+ export const screenProps = {
166
+ ...viewProps,
167
+ scroll: Boolean,
168
+ /** Puts the software keyboard away on a tap outside the editor and, with `scroll`, on a drag towards it. */
169
+ dismissKeyboard: { type: Boolean, default: true },
170
+ }
150
171
  export const revealProps = { ...columnProps, show: Boolean }
172
+ export const sheetProps = {
173
+ id: viewProps.id,
174
+ accessibilityLabel: viewProps.accessibilityLabel,
175
+ open: Boolean,
176
+ /** Rendered as the sheet heading and used as its accessible name. */
177
+ title: { type: String, default: undefined },
178
+ /** Allows the scrim, Escape, the back gesture, swiping down and the close button. */
179
+ dismissible: { type: Boolean, default: true },
180
+ fullscreen: Boolean,
181
+ closeLabel: { type: String, default: 'Close' },
182
+ padding: { ...spacingProp, default: 'lg' },
183
+ gap: spacingProp,
184
+ align: columnProps.align,
185
+ }
186
+ export const sheetEmits = {
187
+ 'update:open': (value: boolean) => typeof value === 'boolean',
188
+ close: (reason: SheetCloseReason) => sheetCloseReasons.includes(reason),
189
+ opened: () => true,
190
+ }
151
191
  export const actionEmits = { click: () => true }
152
192
  export const sliderEmits = { 'update:modelValue': (value: number) => Number.isFinite(value) }
153
193
 
@@ -161,7 +201,9 @@ export const componentProps = {
161
201
  MButton: buttonProps,
162
202
  MSlider: sliderProps,
163
203
  MInput: inputProps,
204
+ MIcon: iconProps,
164
205
  MReveal: revealProps,
206
+ MSheet: sheetProps,
165
207
  }
166
208
  export type BoxProps = ExtractPropTypes<typeof boxProps>
167
209
  export type ComponentProps = {
package/src/vue/theme.ts CHANGED
@@ -2,17 +2,68 @@ import { inject, readonly, shallowRef, type InjectionKey } from '@vue/runtime-co
2
2
  import { mavis } from '../theme/themes.js'
3
3
  import type { MavisTheme } from '../theme/types.js'
4
4
 
5
- export function createThemeContext(initial: MavisTheme) {
5
+ export type Appearance = MavisTheme['appearance']
6
+ export type AppearanceMode = Appearance | 'system'
7
+
8
+ /** Platform access to the device appearance; `current` is undefined while it is unknown, such as during SSR. */
9
+ export interface AppearanceSource {
10
+ current(): Appearance | undefined
11
+ observe(listener: (appearance: Appearance) => void): () => void
12
+ }
13
+
14
+ export interface ThemeContextOptions {
15
+ theme?: MavisTheme
16
+ darkTheme?: MavisTheme
17
+ appearance?: AppearanceMode
18
+ source?: AppearanceSource
19
+ }
20
+
21
+ const modes: readonly AppearanceMode[] = ['system', 'light', 'dark']
22
+
23
+ function checkMode(mode: AppearanceMode) {
24
+ if (!modes.includes(mode)) throw new RangeError(`Unknown appearance mode: ${String(mode)}`)
25
+ return mode
26
+ }
27
+
28
+ export function createThemeContext(options: MavisTheme | ThemeContextOptions = {}) {
29
+ const config: ThemeContextOptions = 'colors' in options ? { theme: options } : options
30
+ const initial = config.theme ?? mavis
31
+ const themes: Partial<Record<Appearance, MavisTheme>> = { [initial.appearance]: initial }
32
+ if (config.darkTheme) themes.dark = config.darkTheme
33
+ const appearance = shallowRef(checkMode(config.appearance ?? 'system'))
6
34
  const theme = shallowRef(initial)
35
+ const resolve = () => {
36
+ const wanted =
37
+ appearance.value === 'system'
38
+ ? (config.source?.current() ?? initial.appearance)
39
+ : appearance.value
40
+ theme.value = themes[wanted] ?? themes[wanted === 'dark' ? 'light' : 'dark'] ?? initial
41
+ }
42
+ resolve()
43
+ const stop = config.source?.observe(() => {
44
+ if (appearance.value === 'system') resolve()
45
+ })
7
46
  return {
8
47
  theme: readonly(theme),
48
+ /** The selection: `system` follows the device; `light` and `dark` are explicit overrides. */
49
+ appearance: readonly(appearance),
50
+ setAppearance(mode: AppearanceMode) {
51
+ appearance.value = checkMode(mode)
52
+ resolve()
53
+ },
54
+ /** Registers the theme for its own appearance and pins that appearance. */
9
55
  setTheme(next: MavisTheme) {
10
- theme.value = next
56
+ themes[next.appearance] = next
57
+ appearance.value = next.appearance
58
+ resolve()
59
+ },
60
+ dispose() {
61
+ stop?.()
11
62
  },
12
63
  }
13
64
  }
14
-
15
65
  export type MavisThemeContext = ReturnType<typeof createThemeContext>
66
+
16
67
  export const themeKey: InjectionKey<MavisThemeContext> = Symbol('mavis.theme')
17
68
 
18
69
  /** Without the plugin, individually imported components use the default theme. */