@mushinjs/ui 0.0.0-stage → 0.2.0-next.20261113103537
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/preset.d.ts +7 -0
- package/dist/preset.js +55 -0
- package/dist/preset.js.map +1 -0
- package/package.json +35 -3
- package/src/components/Alert.vue +75 -0
- package/src/components/Avatar.vue +45 -0
- package/src/components/Button.vue +175 -0
- package/src/components/Card.vue +52 -0
- package/src/components/Checkbox.vue +70 -0
- package/src/components/DataTable.vue +244 -0
- package/src/components/Drawer.vue +81 -0
- package/src/components/Dropdown.vue +116 -0
- package/src/components/Empty.vue +30 -0
- package/src/components/Form.vue +35 -0
- package/src/components/FormItem.vue +87 -0
- package/src/components/Image.vue +66 -0
- package/src/components/Input.vue +205 -0
- package/src/components/KeyValueInput.vue +60 -0
- package/src/components/Modal.vue +129 -0
- package/src/components/NumberInput.vue +113 -0
- package/src/components/Pagination.vue +97 -0
- package/src/components/Popover.vue +62 -0
- package/src/components/Progress.vue +76 -0
- package/src/components/RadioButton.vue +45 -0
- package/src/components/RadioGroup.vue +68 -0
- package/src/components/Select.vue +291 -0
- package/src/components/Slider.vue +103 -0
- package/src/components/Spin.vue +49 -0
- package/src/components/TabPane.vue +22 -0
- package/src/components/Tabs.vue +133 -0
- package/src/components/Tag.vue +78 -0
- package/src/components/TagsInput.vue +42 -0
- package/src/components/TimeInput.vue +71 -0
- package/src/components/Toggle.vue +151 -0
- package/src/components/Tooltip.vue +63 -0
- package/src/components/UiProvider.vue +156 -0
- package/src/components/form.ts +10 -0
- package/src/components/radio.ts +7 -0
- package/src/components/tabs.ts +10 -0
- package/src/config.ts +61 -0
- package/src/feedback.ts +120 -0
- package/src/index.ts +42 -0
- package/src/preset.ts +34 -0
- 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
|
-
#
|
|
1
|
+
# @mushinjs/ui
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/preset.d.ts
ADDED
|
@@ -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'\n\n/** A colour from theme.css, with its soft fill when it has one. */\nconst tone = (name: string) => ({ DEFAULT: `var(--${name})`, soft: `var(--${name}-soft)` })\n\n/**\n * The Mushin.js UnoCSS preset: theme.css tokens as colours (`bg-s1`, `text-fg2`, `border-line`,\n * `bg-primary-soft`, `text-bad`…), so utilities follow the light and dark themes on their own.\n * Use it next to presetWind3 and presetIcons; the repository's uno.config.ts shows how.\n */\nexport function presetMushin(): Preset {\n return {\n name: '@mushinjs/ui',\n theme: {\n colors: {\n bg: 'var(--bg)',\n s1: 'var(--s1)',\n s2: 'var(--s2)',\n s3: 'var(--s3)',\n line: { DEFAULT: 'var(--line)', strong: 'var(--line-strong)' },\n fg: { DEFAULT: 'var(--fg)', 2: 'var(--fg2)', 3: 'var(--fg3)' },\n primary: { ...tone('primary'), hover: 'var(--primary-hover)', fg: 'var(--primary-fg)' },\n ok: tone('ok'),\n warn: tone('warn'),\n bad: tone('bad'),\n info: tone('info'),\n idle: 'var(--idle)',\n },\n borderRadius: { DEFAULT: 'var(--radius-sm)', card: 'var(--radius)' },\n boxShadow: { DEFAULT: 'var(--shadow)', pop: 'var(--shadow-pop)' },\n fontFamily: { mono: 'var(--mono)' },\n },\n }\n}\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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-next.20261113103537",
|
|
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>
|