@mushinjs/ui 0.0.0-stage → 0.2.0-next.20261112165327

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -3
  3. package/dist/preset.d.ts +7 -0
  4. package/dist/preset.js +55 -0
  5. package/dist/preset.js.map +1 -0
  6. package/package.json +35 -3
  7. package/src/components/Alert.vue +75 -0
  8. package/src/components/Avatar.vue +45 -0
  9. package/src/components/Button.vue +175 -0
  10. package/src/components/Card.vue +52 -0
  11. package/src/components/Checkbox.vue +70 -0
  12. package/src/components/DataTable.vue +244 -0
  13. package/src/components/Drawer.vue +81 -0
  14. package/src/components/Dropdown.vue +116 -0
  15. package/src/components/Empty.vue +30 -0
  16. package/src/components/Form.vue +35 -0
  17. package/src/components/FormItem.vue +87 -0
  18. package/src/components/Image.vue +66 -0
  19. package/src/components/Input.vue +205 -0
  20. package/src/components/KeyValueInput.vue +60 -0
  21. package/src/components/Modal.vue +129 -0
  22. package/src/components/NumberInput.vue +113 -0
  23. package/src/components/Pagination.vue +97 -0
  24. package/src/components/Popover.vue +62 -0
  25. package/src/components/Progress.vue +76 -0
  26. package/src/components/RadioButton.vue +45 -0
  27. package/src/components/RadioGroup.vue +68 -0
  28. package/src/components/Select.vue +291 -0
  29. package/src/components/Slider.vue +103 -0
  30. package/src/components/Spin.vue +49 -0
  31. package/src/components/TabPane.vue +22 -0
  32. package/src/components/Tabs.vue +133 -0
  33. package/src/components/Tag.vue +78 -0
  34. package/src/components/TagsInput.vue +42 -0
  35. package/src/components/TimeInput.vue +71 -0
  36. package/src/components/Toggle.vue +151 -0
  37. package/src/components/Tooltip.vue +63 -0
  38. package/src/components/UiProvider.vue +156 -0
  39. package/src/components/form.ts +10 -0
  40. package/src/components/radio.ts +7 -0
  41. package/src/components/tabs.ts +10 -0
  42. package/src/config.ts +61 -0
  43. package/src/feedback.ts +120 -0
  44. package/src/index.ts +42 -0
  45. package/src/preset.ts +34 -0
  46. package/src/theme.css +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Setobox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @mushinjs/ui
2
+
3
+ The Vue components and design tokens of the Mushin.js dashboard. The dashboard is built on them, and a plugin page can use them to look and behave like the rest of the dashboard in both the light and the dark theme.
4
+
5
+ Most plugins do not need a page. A plugin that declares a config schema gets its settings form from the dashboard's SchemaForm, which is built on these components. Write a page only for what a form cannot show.
6
+
7
+ ## Setup
8
+
9
+ ```ts
10
+ // main.ts
11
+ import '@mushinjs/ui/theme.css'
12
+ import 'virtual:uno.css'
13
+ ```
14
+
15
+ ```vue
16
+ <!-- App.vue: once, around everything -->
17
+ <UiProvider :text="{ close: '关闭', cancel: '取消' }">
18
+ <RouterView />
19
+ </UiProvider>
20
+ ```
21
+
22
+ `UiProvider` draws toasts and confirmation dialogs and gives the components their words (`text`, see `UiText` in `config.ts`; English is the default). Set `data-theme="dark"` or `data-theme="light"` on `<html>` to switch the palette.
23
+
24
+ Build with UnoCSS. Use the repository's `uno.config.ts`, or make your own from the same presets:
25
+
26
+ ```ts
27
+ import { defineConfig, presetIcons, presetWind3 } from 'unocss'
28
+ import { presetMushin } from '@mushinjs/ui/preset'
29
+
30
+ export default defineConfig({
31
+ presets: [
32
+ presetWind3({ dark: { dark: 'html[data-theme="dark"]', light: 'html[data-theme="light"]' } }),
33
+ presetIcons(),
34
+ presetMushin(),
35
+ ],
36
+ })
37
+ ```
38
+
39
+ ## Components
40
+
41
+ Alert, Avatar, Button, Card, Checkbox, DataTable, Drawer, Dropdown, Empty, Form, FormItem, Image, Input, KeyValueInput, Modal, NumberInput, Pagination, Popover, Progress, RadioButton, RadioGroup, Select, Slider, Spin, TabPane, Tabs, Tag, TagsInput, TimeInput, Toggle, Tooltip.
42
+
43
+ The props follow the Naive UI ones the dashboard used before: `value` with `update:value` (so `v-model:value`), `type` (`default`, `primary`, `info`, `success`, `warning`, `error`), `size` (`tiny`, `small`, `medium`, `large`), and `secondary`, `quaternary`, `circle` on buttons. Overlays use Reka UI, which handles focus and the keyboard.
44
+
45
+ Every component passes its attributes through, so `class`, `title`, `aria-*` and `data-agent-target` reach the element. The agent highlights controls by `data-agent-target`; keep it on the controls it points at.
46
+
47
+ Run the dashboard in development and open `/dev/ui` to see every component in its states.
48
+
49
+ ## Toasts and confirmations
50
+
51
+ ```ts
52
+ import { useConfirm, useToast } from '@mushinjs/ui'
53
+
54
+ const toast = useToast()
55
+ const confirm = useConfirm()
56
+
57
+ toast.success('Saved')
58
+ confirm.warning({
59
+ title: 'Delete this flow?',
60
+ content: 'Its runs are kept.',
61
+ positiveText: 'Delete',
62
+ negativeText: 'Cancel',
63
+ onPositiveClick: () => remove(),
64
+ })
65
+ ```
66
+
67
+ `onPositiveClick` may return a promise. The button shows a spinner until the promise settles, and the dialog stays open if it returns `false`.
68
+
69
+ ## Icons
70
+
71
+ Icons are class names from Iconify: `<span class="i-lucide-trash-2" />`, or `icon="i-lucide-trash-2"` on a Button or Tag. Use lucide first. When lucide has no match, use phosphor (`i-ph-…`) or carbon (`i-carbon-…`). Use svg-logos (`i-logos-…`) for brands.
72
+
73
+ Write each class name out in full. UnoCSS finds class names by reading the source, so it never finds one put together at runtime, like `` `i-lucide-${name}` ``. For an icon that depends on data, use a table of full names (`{ keyword: 'i-lucide-terminal' }`).
74
+
75
+ ## Colours and CSS
76
+
77
+ - Take colours only from the tokens in `theme.css`, as CSS variables (`var(--s1)`, `var(--fg2)`, `var(--primary)`) or utilities (`bg-s1`, `text-fg2`, `border-line`, `bg-primary-soft`, `text-bad`). Both themes then work without extra rules.
78
+ - Use Tailwind-style utilities only (presetWind3). Do not use attributify mode or variant groups.
79
+ - The dashboard also runs in Tauri's WebKitGTK on Linux. Do not use native CSS nesting or `@container` queries.
80
+
81
+ ## Plugin pages
82
+
83
+ - Declare `vue` and `@mushinjs/ui` as peer dependencies, so the page uses the dashboard's copies and not a second Vue.
84
+ - Keep the page's own CSS from leaking into the dashboard. Use scoped styles or a class prefix for the plugin, or put the rules in an `@layer`.
85
+ - Do not restyle the shared components from outside (`.mu-button { … }`). If one is missing something, add it to `@mushinjs/ui`.
86
+ - Write the page's words in both English and Simplified Chinese, as the dashboard does.
@@ -0,0 +1,7 @@
1
+ import { Preset } from "unocss";
2
+ /**
3
+ * The Mushin.js UnoCSS preset: theme.css tokens as colours (`bg-s1`, `text-fg2`, `border-line`,
4
+ * `bg-primary-soft`, `text-bad`…), so utilities follow the light and dark themes on their own.
5
+ * Use it next to presetWind3 and presetIcons; the repository's uno.config.ts shows how.
6
+ */
7
+ export declare function presetMushin(): Preset;
package/dist/preset.js ADDED
@@ -0,0 +1,55 @@
1
+ // src/preset.ts
2
+ /** A colour from theme.css, with its soft fill when it has one. */
3
+ const tone = (name) => ({
4
+ DEFAULT: `var(--${name})`,
5
+ soft: `var(--${name}-soft)`
6
+ });
7
+ /**
8
+ * The Mushin.js UnoCSS preset: theme.css tokens as colours (`bg-s1`, `text-fg2`, `border-line`,
9
+ * `bg-primary-soft`, `text-bad`…), so utilities follow the light and dark themes on their own.
10
+ * Use it next to presetWind3 and presetIcons; the repository's uno.config.ts shows how.
11
+ */
12
+ function presetMushin() {
13
+ return {
14
+ name: "@mushinjs/ui",
15
+ theme: {
16
+ colors: {
17
+ bg: "var(--bg)",
18
+ s1: "var(--s1)",
19
+ s2: "var(--s2)",
20
+ s3: "var(--s3)",
21
+ line: {
22
+ DEFAULT: "var(--line)",
23
+ strong: "var(--line-strong)"
24
+ },
25
+ fg: {
26
+ DEFAULT: "var(--fg)",
27
+ 2: "var(--fg2)",
28
+ 3: "var(--fg3)"
29
+ },
30
+ primary: {
31
+ ...tone("primary"),
32
+ hover: "var(--primary-hover)",
33
+ fg: "var(--primary-fg)"
34
+ },
35
+ ok: tone("ok"),
36
+ warn: tone("warn"),
37
+ bad: tone("bad"),
38
+ info: tone("info"),
39
+ idle: "var(--idle)"
40
+ },
41
+ borderRadius: {
42
+ DEFAULT: "var(--radius-sm)",
43
+ card: "var(--radius)"
44
+ },
45
+ boxShadow: {
46
+ DEFAULT: "var(--shadow)",
47
+ pop: "var(--shadow-pop)"
48
+ },
49
+ fontFamily: { mono: "var(--mono)" }
50
+ }
51
+ };
52
+ }
53
+ export { presetMushin };
54
+
55
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.js","names":[],"sources":["../src/preset.ts"],"sourcesContent":["import type { Preset } from 'unocss'\r\n\r\n/** A colour from theme.css, with its soft fill when it has one. */\r\nconst tone = (name: string) => ({ DEFAULT: `var(--${name})`, soft: `var(--${name}-soft)` })\r\n\r\n/**\r\n * The Mushin.js UnoCSS preset: theme.css tokens as colours (`bg-s1`, `text-fg2`, `border-line`,\r\n * `bg-primary-soft`, `text-bad`…), so utilities follow the light and dark themes on their own.\r\n * Use it next to presetWind3 and presetIcons; the repository's uno.config.ts shows how.\r\n */\r\nexport function presetMushin(): Preset {\r\n return {\r\n name: '@mushinjs/ui',\r\n theme: {\r\n colors: {\r\n bg: 'var(--bg)',\r\n s1: 'var(--s1)',\r\n s2: 'var(--s2)',\r\n s3: 'var(--s3)',\r\n line: { DEFAULT: 'var(--line)', strong: 'var(--line-strong)' },\r\n fg: { DEFAULT: 'var(--fg)', 2: 'var(--fg2)', 3: 'var(--fg3)' },\r\n primary: { ...tone('primary'), hover: 'var(--primary-hover)', fg: 'var(--primary-fg)' },\r\n ok: tone('ok'),\r\n warn: tone('warn'),\r\n bad: tone('bad'),\r\n info: tone('info'),\r\n idle: 'var(--idle)',\r\n },\r\n borderRadius: { DEFAULT: 'var(--radius-sm)', card: 'var(--radius)' },\r\n boxShadow: { DEFAULT: 'var(--shadow)', pop: 'var(--shadow-pop)' },\r\n fontFamily: { mono: 'var(--mono)' },\r\n },\r\n }\r\n}\r\n"],"mappings":";;AAGA,MAAM,QAAQ,UAAkB;CAAE,SAAS,SAAS,KAAK;CAAI,MAAM,SAAS,KAAK;AAAQ;;;;;;AAOzF,SAAgB,eAAuB;CACrC,OAAO;EACL,MAAM;EACN,OAAO;GACL,QAAQ;IACN,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,MAAM;KAAE,SAAS;KAAe,QAAQ;IAAqB;IAC7D,IAAI;KAAE,SAAS;KAAa,GAAG;KAAc,GAAG;IAAa;IAC7D,SAAS;KAAE,GAAG,KAAK,SAAS;KAAG,OAAO;KAAwB,IAAI;IAAoB;IACtF,IAAI,KAAK,IAAI;IACb,MAAM,KAAK,MAAM;IACjB,KAAK,KAAK,KAAK;IACf,MAAM,KAAK,MAAM;IACjB,MAAM;GACR;GACA,cAAc;IAAE,SAAS;IAAoB,MAAM;GAAgB;GACnE,WAAW;IAAE,SAAS;IAAiB,KAAK;GAAoB;GAChE,YAAY,EAAE,MAAM,cAAc;EACpC;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@mushinjs/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-next.20261112165327",
4
+ "description": "Vue components and design tokens shared by the Mushin.js dashboard and plugin pages",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mushinjs/mushin.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./preset": {
15
+ "types": "./dist/preset.d.ts",
16
+ "import": "./dist/preset.js"
17
+ },
18
+ "./theme.css": "./src/theme.css",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "dist"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "peerDependencies": {
29
+ "vue": "^3.5.44"
30
+ },
31
+ "dependencies": {
32
+ "reka-ui": "^2.11.0"
33
+ },
34
+ "devDependencies": {
35
+ "unocss": "^66.12.0",
36
+ "vue": "^3.5.44"
37
+ }
6
38
  }
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ /** A note inside a page or panel: information, a warning, an error. */
3
+ import { computed } from 'vue'
4
+ import { type Tone, useText } from '../config'
5
+
6
+ const props = withDefaults(defineProps<{
7
+ type?: Tone
8
+ title?: string
9
+ showIcon?: boolean
10
+ closable?: boolean
11
+ }>(), { type: 'info', showIcon: true })
12
+
13
+ defineEmits<{ close: [] }>()
14
+ const text = useText()
15
+ const icon = computed(() => ({
16
+ default: 'i-lucide-info',
17
+ primary: 'i-lucide-info',
18
+ info: 'i-lucide-info',
19
+ success: 'i-lucide-circle-check',
20
+ warning: 'i-lucide-triangle-alert',
21
+ error: 'i-lucide-circle-x',
22
+ }[props.type]))
23
+ </script>
24
+
25
+ <template>
26
+ <div class="mu-alert" :class="`mu-alert--${type}`" :role="type === 'error' || type === 'warning' ? 'alert' : 'status'">
27
+ <span v-if="showIcon" class="mu-alert__icon" :class="icon" />
28
+ <div class="mu-alert__body">
29
+ <div v-if="title || $slots.header" class="mu-alert__title"><slot name="header">{{ title }}</slot></div>
30
+ <div v-if="$slots.default" class="mu-alert__content"><slot /></div>
31
+ </div>
32
+ <button v-if="closable" type="button" class="mu-alert__close" :aria-label="text.close" @click="$emit('close')">
33
+ <span class="i-lucide-x" />
34
+ </button>
35
+ </div>
36
+ </template>
37
+
38
+ <style>
39
+ .mu-alert {
40
+ --c: var(--info);
41
+ --bg: var(--info-soft);
42
+ display: flex;
43
+ align-items: flex-start;
44
+ gap: 8px;
45
+ box-sizing: border-box;
46
+ padding: 9px 12px;
47
+ border-radius: var(--radius-sm);
48
+ background: var(--bg);
49
+ color: var(--fg);
50
+ font-size: 13px;
51
+ line-height: 1.55;
52
+ }
53
+
54
+ .mu-alert--default { --c: var(--fg2); --bg: var(--s2); }
55
+ .mu-alert--primary { --c: var(--primary); --bg: var(--primary-soft); }
56
+ .mu-alert--success { --c: var(--ok); --bg: var(--ok-soft); }
57
+ .mu-alert--warning { --c: var(--warn); --bg: var(--warn-soft); }
58
+ .mu-alert--error { --c: var(--bad); --bg: var(--bad-soft); }
59
+ .mu-alert__icon { margin-top: 2px; color: var(--c); font-size: 16px; }
60
+ .mu-alert__body { flex: 1; min-width: 0; }
61
+ .mu-alert__title { font-weight: 600; }
62
+ .mu-alert__title + .mu-alert__content { margin-top: 2px; color: var(--fg2); }
63
+
64
+ .mu-alert__close {
65
+ display: inline-flex;
66
+ padding: 2px;
67
+ border: 0;
68
+ border-radius: 4px;
69
+ background: none;
70
+ color: var(--fg3);
71
+ cursor: pointer;
72
+ }
73
+
74
+ .mu-alert__close:hover { color: var(--fg); }
75
+ </style>
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ /** A round picture, or its initials on a colour when there is no picture (or it fails to load). */
3
+ import { ref, watch } from 'vue'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ src?: string
7
+ size?: number
8
+ /** Background behind the initials. */
9
+ color?: string
10
+ /** Rounded square instead of a circle. */
11
+ square?: boolean
12
+ }>(), { size: 32 })
13
+
14
+ const failed = ref(false)
15
+ watch(() => props.src, () => (failed.value = false))
16
+ </script>
17
+
18
+ <template>
19
+ <span
20
+ class="mu-avatar"
21
+ :class="{ 'mu-avatar--square': square }"
22
+ :style="{ width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.42)}px`, background: src && !failed ? undefined : color }"
23
+ >
24
+ <img v-if="src && !failed" :src="src" alt="" @error="failed = true">
25
+ <slot v-else />
26
+ </span>
27
+ </template>
28
+
29
+ <style>
30
+ .mu-avatar {
31
+ display: inline-grid;
32
+ flex: none;
33
+ place-items: center;
34
+ overflow: hidden;
35
+ border-radius: 50%;
36
+ background: var(--s3);
37
+ color: #fff;
38
+ font-weight: 600;
39
+ line-height: 1;
40
+ user-select: none;
41
+ }
42
+
43
+ .mu-avatar--square { border-radius: 25%; }
44
+ .mu-avatar img { width: 100%; height: 100%; object-fit: cover; }
45
+ </style>
@@ -0,0 +1,175 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * A button. `type` picks the colour, the style flags how loud it is: solid (default for colours),
4
+ * `secondary` (soft fill), `tertiary` (outline), `quaternary` (no fill until hovered), `dashed`
5
+ * and `text`. An icon goes in `icon` as a class name (`i-lucide-plus`) or in the #icon slot.
6
+ */
7
+ import { computed, useSlots } from 'vue'
8
+ import type { Size, Tone } from '../config'
9
+
10
+ const props = withDefaults(defineProps<{
11
+ type?: Tone
12
+ size?: Size
13
+ secondary?: boolean
14
+ tertiary?: boolean
15
+ quaternary?: boolean
16
+ dashed?: boolean
17
+ text?: boolean
18
+ circle?: boolean
19
+ round?: boolean
20
+ block?: boolean
21
+ loading?: boolean
22
+ disabled?: boolean
23
+ icon?: string
24
+ iconPlacement?: 'left' | 'right'
25
+ attrType?: 'button' | 'submit' | 'reset'
26
+ /** Shows the button as pressed, for buttons that toggle something. */
27
+ pressed?: boolean
28
+ }>(), { type: 'default', size: 'medium', iconPlacement: 'left', attrType: 'button' })
29
+
30
+ defineEmits<{ click: [event: MouseEvent] }>()
31
+ const slots = useSlots()
32
+
33
+ // Reka triggers pass type="button" through to their child; that is not a colour.
34
+ const TONES = ['default', 'primary', 'info', 'success', 'warning', 'error']
35
+ const tone = computed(() => (TONES.includes(props.type) ? props.type : 'default'))
36
+ const variant = computed(() => {
37
+ if (props.text) return 'text'
38
+ if (props.quaternary) return 'ghost'
39
+ if (props.dashed) return 'dashed'
40
+ if (props.tertiary) return 'outline'
41
+ if (props.secondary) return 'soft'
42
+ return tone.value === 'default' ? 'outline' : 'solid'
43
+ })
44
+ const iconOnly = computed(() => !slots.default)
45
+ </script>
46
+
47
+ <template>
48
+ <button
49
+ :type="attrType"
50
+ class="mu-btn"
51
+ :class="[
52
+ `mu-btn--${size}`,
53
+ `mu-btn--${variant}`,
54
+ `mu-tone--${tone}`,
55
+ { 'mu-btn--circle': circle, 'mu-btn--round': round, 'mu-btn--block': block, 'mu-btn--icon-only': iconOnly, 'mu-btn--pressed': pressed, 'mu-btn--loading': loading },
56
+ ]"
57
+ :disabled="disabled || loading"
58
+ :aria-busy="loading || undefined"
59
+ :aria-pressed="pressed === undefined ? undefined : pressed"
60
+ @click="$emit('click', $event)"
61
+ >
62
+ <span v-if="loading" class="mu-btn__icon i-lucide-loader-circle mu-spin" />
63
+ <template v-else-if="iconPlacement === 'left'">
64
+ <span v-if="slots.icon" class="mu-btn__icon"><slot name="icon" /></span>
65
+ <span v-else-if="icon" class="mu-btn__icon" :class="icon" />
66
+ </template>
67
+ <span v-if="slots.default" class="mu-btn__label"><slot /></span>
68
+ <template v-if="!loading && iconPlacement === 'right'">
69
+ <span v-if="slots.icon" class="mu-btn__icon"><slot name="icon" /></span>
70
+ <span v-else-if="icon" class="mu-btn__icon" :class="icon" />
71
+ </template>
72
+ </button>
73
+ </template>
74
+
75
+ <style>
76
+ .mu-tone--default { --tone: var(--fg); --tone-hover: var(--fg); --tone-soft: var(--s2); --tone-soft-hover: var(--s3); --tone-fg: var(--s1); }
77
+ .mu-tone--primary { --tone: var(--primary); --tone-hover: var(--primary-hover); --tone-soft: var(--primary-soft); --tone-soft-hover: var(--primary-soft-hover); --tone-fg: var(--primary-fg); }
78
+ .mu-tone--info { --tone: var(--info); --tone-hover: var(--info); --tone-soft: var(--info-soft); --tone-soft-hover: var(--info-soft); --tone-fg: #fff; }
79
+ .mu-tone--success { --tone: var(--ok); --tone-hover: var(--ok); --tone-soft: var(--ok-soft); --tone-soft-hover: var(--ok-soft); --tone-fg: #fff; }
80
+ .mu-tone--warning { --tone: var(--warn); --tone-hover: var(--warn); --tone-soft: var(--warn-soft); --tone-soft-hover: var(--warn-soft); --tone-fg: #fff; }
81
+ .mu-tone--error { --tone: var(--bad); --tone-hover: var(--bad); --tone-soft: var(--bad-soft); --tone-soft-hover: var(--bad-soft); --tone-fg: #fff; }
82
+
83
+ .mu-btn {
84
+ --h: 34px;
85
+ display: inline-flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ gap: 6px;
89
+ box-sizing: border-box;
90
+ height: var(--h);
91
+ padding: 0 14px;
92
+ border: 1px solid transparent;
93
+ border-radius: var(--radius-sm);
94
+ background: transparent;
95
+ color: var(--tone);
96
+ font: inherit;
97
+ font-size: 14px;
98
+ font-weight: 500;
99
+ line-height: 1;
100
+ white-space: nowrap;
101
+ cursor: pointer;
102
+ user-select: none;
103
+ transition: background-color 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
104
+ }
105
+
106
+ .mu-btn:focus-visible {
107
+ outline: none;
108
+ box-shadow: 0 0 0 3px var(--focus);
109
+ }
110
+
111
+ .mu-btn:disabled {
112
+ cursor: not-allowed;
113
+ opacity: 0.5;
114
+ }
115
+
116
+ .mu-btn--loading:disabled {
117
+ cursor: progress;
118
+ opacity: 0.75;
119
+ }
120
+
121
+ .mu-btn--tiny { --h: 22px; padding: 0 7px; gap: 4px; font-size: 12px; border-radius: 5px; }
122
+ .mu-btn--small { --h: 28px; padding: 0 10px; gap: 5px; font-size: 13px; }
123
+ .mu-btn--large { --h: 40px; padding: 0 18px; font-size: 15px; }
124
+
125
+ .mu-btn--icon-only { width: var(--h); padding: 0; }
126
+ .mu-btn--circle { border-radius: 999px; }
127
+ .mu-btn--round { border-radius: 999px; }
128
+ .mu-btn--block { display: flex; width: 100%; }
129
+
130
+ .mu-btn__icon {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ font-size: 1.15em;
135
+ }
136
+
137
+ .mu-btn__icon > svg { width: 1em; height: 1em; }
138
+ .mu-btn--tiny .mu-btn__icon { font-size: 13px; }
139
+ .mu-btn__label { overflow: hidden; text-overflow: ellipsis; }
140
+
141
+ .mu-btn--solid { background: var(--tone); color: var(--tone-fg); }
142
+ .mu-btn--solid:not(:disabled):hover { background: var(--tone-hover); }
143
+ .mu-btn--solid:not(:disabled):active { filter: brightness(0.94); }
144
+
145
+ .mu-btn--soft { background: var(--tone-soft); }
146
+ .mu-btn--soft:not(:disabled):hover { background: var(--tone-soft-hover); }
147
+
148
+ .mu-btn--outline { border-color: var(--line-strong); background: var(--s1); }
149
+ .mu-tone--default.mu-btn--outline { color: var(--fg); }
150
+ .mu-btn--outline:not(:disabled):hover { border-color: var(--tone); color: var(--tone); }
151
+ .mu-tone--default.mu-btn--outline:not(:disabled):hover { border-color: var(--primary); color: var(--primary); }
152
+
153
+ .mu-btn--dashed { border: 1px dashed var(--line-strong); }
154
+ .mu-tone--default.mu-btn--dashed { color: var(--fg2); }
155
+ .mu-btn--dashed:not(:disabled):hover { border-color: var(--primary); color: var(--primary); }
156
+
157
+ .mu-btn--ghost { color: var(--tone); }
158
+ .mu-tone--default.mu-btn--ghost { color: var(--fg2); }
159
+ .mu-btn--ghost:not(:disabled):hover { background: var(--tone-soft); }
160
+ .mu-tone--default.mu-btn--ghost:not(:disabled):hover { color: var(--fg); background: var(--s2); }
161
+
162
+ .mu-btn--text { height: auto; padding: 0; border: 0; }
163
+ .mu-btn--text:not(:disabled):hover { opacity: 0.8; }
164
+ .mu-tone--default.mu-btn--text { color: var(--fg2); }
165
+
166
+ /* A toggling button that is on: soft colour fill, in every style but solid. */
167
+ .mu-btn--pressed:not(.mu-btn--solid) { background: var(--tone-soft); border-color: transparent; }
168
+ .mu-tone--default.mu-btn--pressed:not(.mu-btn--solid) { background: var(--primary-soft); color: var(--primary); }
169
+
170
+ .mu-spin { animation: mu-spin 0.8s linear infinite; }
171
+
172
+ @keyframes mu-spin {
173
+ to { transform: rotate(360deg); }
174
+ }
175
+ </style>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ /** A surface with an optional title row (#header-extra on its right) and footer. */
3
+ defineProps<{
4
+ title?: string
5
+ size?: 'small' | 'medium'
6
+ /** Lifts on hover and shows a pointer, for cards that open something. */
7
+ hoverable?: boolean
8
+ /** A sunken card inside another surface. */
9
+ embedded?: boolean
10
+ }>()
11
+ </script>
12
+
13
+ <template>
14
+ <section class="mu-card" :class="[`mu-card--${size ?? 'medium'}`, { 'mu-card--hoverable': hoverable, 'mu-card--embedded': embedded }]">
15
+ <header v-if="title || $slots.header || $slots['header-extra']" class="mu-card__header">
16
+ <div class="mu-card__title"><slot name="header">{{ title }}</slot></div>
17
+ <div v-if="$slots['header-extra']" class="mu-card__extra"><slot name="header-extra" /></div>
18
+ </header>
19
+ <div class="mu-card__body"><slot /></div>
20
+ <footer v-if="$slots.footer" class="mu-card__footer"><slot name="footer" /></footer>
21
+ </section>
22
+ </template>
23
+
24
+ <style>
25
+ .mu-card {
26
+ box-sizing: border-box;
27
+ border: 1px solid var(--line);
28
+ border-radius: var(--radius);
29
+ background: var(--s1);
30
+ color: var(--fg);
31
+ }
32
+
33
+ .mu-card--embedded { border-color: transparent; background: var(--s2); }
34
+ .mu-card--hoverable { cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; }
35
+ .mu-card--hoverable:hover { border-color: var(--line-strong); box-shadow: var(--shadow); }
36
+
37
+ .mu-card__header {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 12px;
41
+ padding: 16px 20px 0;
42
+ }
43
+
44
+ .mu-card__title { flex: 1; min-width: 0; font-size: 15px; font-weight: 600; }
45
+ .mu-card__extra { display: flex; align-items: center; gap: 8px; }
46
+ .mu-card__body { padding: 16px 20px 20px; }
47
+ .mu-card__footer { padding: 0 20px 16px; }
48
+ .mu-card--small .mu-card__header { padding: 12px 14px 0; }
49
+ .mu-card--small .mu-card__title { font-size: 14px; }
50
+ .mu-card--small .mu-card__body { padding: 12px 14px 14px; }
51
+ .mu-card--small .mu-card__footer { padding: 0 14px 12px; }
52
+ </style>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ /** A checkbox with its label as the default slot. */
3
+ import { CheckboxIndicator, CheckboxRoot } from 'reka-ui'
4
+ import type { Size } from '../config'
5
+
6
+ withDefaults(defineProps<{
7
+ checked?: boolean
8
+ indeterminate?: boolean
9
+ disabled?: boolean
10
+ size?: Size
11
+ }>(), { checked: false, size: 'medium' })
12
+
13
+ const emit = defineEmits<{ 'update:checked': [value: boolean] }>()
14
+ </script>
15
+
16
+ <template>
17
+ <label class="mu-check" :class="[`mu-check--${size}`, { 'is-disabled': disabled }]">
18
+ <CheckboxRoot
19
+ class="mu-check__box"
20
+ :model-value="indeterminate ? 'indeterminate' : checked"
21
+ :disabled="disabled"
22
+ @update:model-value="(value) => emit('update:checked', value === true)"
23
+ >
24
+ <CheckboxIndicator class="mu-check__mark">
25
+ <span :class="indeterminate ? 'i-lucide-minus' : 'i-lucide-check'" />
26
+ </CheckboxIndicator>
27
+ </CheckboxRoot>
28
+ <span v-if="$slots.default" class="mu-check__label"><slot /></span>
29
+ </label>
30
+ </template>
31
+
32
+ <style>
33
+ .mu-check {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ color: var(--fg);
38
+ font-size: 14px;
39
+ line-height: 1.4;
40
+ cursor: pointer;
41
+ user-select: none;
42
+ }
43
+
44
+ .mu-check--small,
45
+ .mu-check--tiny { gap: 6px; font-size: 13px; }
46
+ .mu-check.is-disabled { opacity: 0.5; cursor: not-allowed; }
47
+
48
+ .mu-check__box {
49
+ display: inline-flex;
50
+ flex: none;
51
+ align-items: center;
52
+ justify-content: center;
53
+ box-sizing: border-box;
54
+ width: 16px;
55
+ height: 16px;
56
+ padding: 0;
57
+ border: 1.5px solid var(--line-strong);
58
+ border-radius: 4px;
59
+ background: var(--s1);
60
+ color: var(--primary-fg);
61
+ cursor: inherit;
62
+ transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
63
+ }
64
+
65
+ .mu-check:hover .mu-check__box { border-color: var(--primary); }
66
+ .mu-check__box:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus); }
67
+ .mu-check__box[data-state='checked'],
68
+ .mu-check__box[data-state='indeterminate'] { border-color: var(--primary); background: var(--primary); }
69
+ .mu-check__mark { display: inline-flex; font-size: 12px; }
70
+ </style>