@bluerobotics/bluevue 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -18
- package/bin/create-blueos-extension.mjs +72 -0
- package/dist/bluevue.js +2464 -778
- package/dist/components/BlueApp.vue.d.ts +50 -0
- package/dist/components/BlueBanner.vue.d.ts +49 -0
- package/dist/components/BlueBannerGroup.vue.d.ts +20 -0
- package/dist/components/BlueButton.vue.d.ts +59 -0
- package/dist/components/BlueCard.vue.d.ts +45 -0
- package/dist/components/BlueCheckbox.vue.d.ts +34 -0
- package/dist/components/BlueChip.vue.d.ts +47 -0
- package/dist/components/BlueConfirmDialog.vue.d.ts +193 -0
- package/dist/components/BlueDialog.vue.d.ts +60 -0
- package/dist/components/BlueFileDrop.vue.d.ts +32 -0
- package/dist/components/BlueIcon.vue.d.ts +26 -0
- package/dist/components/BlueInput.vue.d.ts +1 -0
- package/dist/components/BlueProgressBar.vue.d.ts +28 -0
- package/dist/components/BlueRadioGroup.vue.d.ts +40 -0
- package/dist/components/BlueSection.vue.d.ts +36 -0
- package/dist/components/BlueSnackbar.vue.d.ts +19 -0
- package/dist/components/BlueSpinner.vue.d.ts +25 -0
- package/dist/components/BlueStat.vue.d.ts +21 -0
- package/dist/components/BlueStepsDialog.vue.d.ts +62 -0
- package/dist/components/BlueTable.vue.d.ts +62 -0
- package/dist/components/BlueTabs.vue.d.ts +37 -0
- package/dist/components/BlueTextarea.vue.d.ts +40 -0
- package/dist/components/BlueTooltip.vue.d.ts +44 -0
- package/dist/components/BlueWindRose.vue.d.ts +37 -0
- package/dist/composables/useBlueLoading.d.ts +22 -0
- package/dist/composables/useBlueOs.d.ts +39 -0
- package/dist/composables/useBlueSnackbar.d.ts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/services/blueos.d.ts +37 -0
- package/dist/style.css +1 -1
- package/dist/types/banner.d.ts +16 -0
- package/dist/utils/files.d.ts +13 -0
- package/dist/utils/theme.d.ts +22 -0
- package/package.json +8 -2
- package/src/components/BlueApp.vue +84 -0
- package/src/components/BlueBanner.vue +196 -0
- package/src/components/BlueBannerGroup.vue +47 -0
- package/src/components/BlueButton.vue +134 -0
- package/src/components/BlueButtonGroup.vue +26 -24
- package/src/components/BlueCard.vue +62 -0
- package/src/components/BlueCheckbox.vue +100 -0
- package/src/components/BlueChip.vue +77 -0
- package/src/components/BlueConfirmDialog.vue +73 -0
- package/src/components/BlueDialog.vue +135 -0
- package/src/components/BlueFileDrop.vue +94 -0
- package/src/components/BlueIcon.vue +37 -0
- package/src/components/BlueInput.vue +21 -11
- package/src/components/BlueLoadingDialog.vue +22 -53
- package/src/components/BlueProgressBar.vue +46 -0
- package/src/components/BluePromptDialog.vue +51 -99
- package/src/components/BlueRadioGroup.vue +104 -0
- package/src/components/BlueSection.vue +33 -0
- package/src/components/BlueSelect.vue +12 -10
- package/src/components/BlueSlider.vue +1 -3
- package/src/components/BlueSnackbar.vue +79 -0
- package/src/components/BlueSpinner.vue +37 -0
- package/src/components/BlueStat.vue +26 -0
- package/src/components/BlueStepsDialog.vue +180 -0
- package/src/components/BlueSwitch.vue +16 -13
- package/src/components/BlueTable.vue +154 -0
- package/src/components/BlueTabs.vue +77 -0
- package/src/components/BlueTextarea.vue +101 -0
- package/src/components/BlueTooltip.vue +88 -0
- package/src/components/BlueWindRose.vue +267 -0
- package/src/composables/useBlueLoading.ts +38 -0
- package/src/composables/useBlueOs.ts +154 -0
- package/src/composables/useBlueSnackbar.ts +89 -0
- package/src/index.ts +40 -0
- package/src/services/blueos.ts +95 -0
- package/src/styles/bluevue.css +136 -0
- package/src/types/banner.ts +17 -0
- package/src/utils/files.ts +44 -0
- package/src/utils/theme.ts +37 -0
- package/templates/extension/.github/workflows/deploy.yml +23 -0
- package/templates/extension/Dockerfile +56 -0
- package/templates/extension/README.md +35 -0
- package/templates/extension/backend/main.py +45 -0
- package/templates/extension/backend/requirements.txt +2 -0
- package/templates/extension/frontend/.eslintrc.cjs +20 -0
- package/templates/extension/frontend/env.d.ts +7 -0
- package/templates/extension/frontend/index.html +12 -0
- package/templates/extension/frontend/package.json +30 -0
- package/templates/extension/frontend/src/App.vue +51 -0
- package/templates/extension/frontend/src/main.ts +8 -0
- package/templates/extension/frontend/src/styles/global.css +11 -0
- package/templates/extension/frontend/tsconfig.json +25 -0
- package/templates/extension/frontend/tsconfig.node.json +11 -0
- package/templates/extension/frontend/vite.config.ts +24 -0
|
@@ -22,20 +22,20 @@
|
|
|
22
22
|
<!-- Shrinks far more eagerly than the label, so a tight row ellipsizes the button names
|
|
23
23
|
before it starts eating into the label. -->
|
|
24
24
|
<div class="flex items-center min-w-0 shrink-[1000]">
|
|
25
|
-
<
|
|
25
|
+
<BlueTooltip
|
|
26
26
|
v-if="infoTooltip"
|
|
27
|
-
|
|
27
|
+
:text="infoTooltip"
|
|
28
|
+
:theme="theme"
|
|
29
|
+
class="items-center shrink-0 mr-2"
|
|
28
30
|
>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
+
<BlueIcon
|
|
32
|
+
name="mdi-information-outline"
|
|
33
|
+
:size="16"
|
|
34
|
+
:label="infoTooltip"
|
|
35
|
+
class="opacity-60 cursor-help"
|
|
31
36
|
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
32
37
|
/>
|
|
33
|
-
|
|
34
|
-
class="absolute bottom-full mb-1 right-0 hidden group-hover:block w-max max-w-[280px] px-2 py-1 text-xs rounded bg-gray-700 text-white z-[1000]"
|
|
35
|
-
>
|
|
36
|
-
{{ infoTooltip }}
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
38
|
+
</BlueTooltip>
|
|
39
39
|
<div
|
|
40
40
|
ref="trackRef"
|
|
41
41
|
class="relative flex min-w-0 justify-end overflow-hidden rounded-[6px] bluevue-elevation-1 z-[666]"
|
|
@@ -76,21 +76,21 @@
|
|
|
76
76
|
@pointerleave="cancelLongPress"
|
|
77
77
|
@pointercancel="cancelLongPress"
|
|
78
78
|
>
|
|
79
|
-
<
|
|
80
|
-
|
|
79
|
+
<BlueTooltip
|
|
80
|
+
:text="btn.tooltip || ''"
|
|
81
|
+
:theme="theme"
|
|
82
|
+
class="min-w-0"
|
|
83
|
+
>
|
|
84
|
+
<!-- A span rather than a paragraph: this is one line inside a button, and a
|
|
85
|
+
paragraph brings margins of its own wherever the host page has no reset. -->
|
|
86
|
+
<span
|
|
81
87
|
data-bbg-label
|
|
82
|
-
class="truncate"
|
|
88
|
+
class="block truncate"
|
|
83
89
|
:class="[density === 'compact' ? 'text-[10px]' : 'text-xs', isWide ? 'max-w-[150px]' : '']"
|
|
84
90
|
>
|
|
85
91
|
{{ btn.name }}
|
|
86
|
-
</
|
|
87
|
-
|
|
88
|
-
v-if="btn.tooltip"
|
|
89
|
-
class="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 hidden group-hover:block whitespace-no-wrap px-2 py-1 text-xs rounded bg-gray-700 text-white"
|
|
90
|
-
>
|
|
91
|
-
{{ btn.tooltip }}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
92
|
+
</span>
|
|
93
|
+
</BlueTooltip>
|
|
94
94
|
</button>
|
|
95
95
|
<div
|
|
96
96
|
v-if="idx < buttonItems.length - 1"
|
|
@@ -156,6 +156,8 @@
|
|
|
156
156
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
157
157
|
|
|
158
158
|
import { useBluePopover } from '../composables/useBluePopover'
|
|
159
|
+
import BlueIcon from './BlueIcon.vue'
|
|
160
|
+
import BlueTooltip from './BlueTooltip.vue'
|
|
159
161
|
|
|
160
162
|
/**
|
|
161
163
|
* One button in the group
|
|
@@ -244,9 +246,9 @@ let longPressFired = false
|
|
|
244
246
|
// The inset each density gives a button name, paired with the total width it costs so the
|
|
245
247
|
// measurements below can reason about the group without reading it back from the DOM.
|
|
246
248
|
const DENSITY_PADDING = {
|
|
247
|
-
compact: { class: 'px-
|
|
248
|
-
regular: { class: 'px-
|
|
249
|
-
comfortable: { class: 'px-
|
|
249
|
+
compact: { class: 'px-[11px]', total: 22 },
|
|
250
|
+
regular: { class: 'px-[15px]', total: 30 },
|
|
251
|
+
comfortable: { class: 'px-[19px]', total: 38 },
|
|
250
252
|
} as const
|
|
251
253
|
|
|
252
254
|
// Once the buttons' natural width would exceed this, cap each label so long names
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BlueIcon from './BlueIcon.vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The sheet an extension's page is laid out on: a titled bar over a body, raised off the backdrop.
|
|
6
|
+
* BlueApp puts one in the middle of the page for you; reach for it directly when a page needs more
|
|
7
|
+
* than one.
|
|
8
|
+
*/
|
|
9
|
+
defineProps<{
|
|
10
|
+
title?: string
|
|
11
|
+
/** An image beside the title, such as the extension's own mark. */
|
|
12
|
+
logo?: string
|
|
13
|
+
/** An mdi name beside the title, for a card with no mark of its own. */
|
|
14
|
+
icon?: string
|
|
15
|
+
/** How wide the card is allowed to get (default '615px'). */
|
|
16
|
+
width?: string
|
|
17
|
+
/** Replaces the body's own inset, for a body that lays itself out. */
|
|
18
|
+
bodyClass?: string
|
|
19
|
+
}>()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<section
|
|
24
|
+
class="bluevue-elevation-5 mx-auto w-full rounded-[8px] bg-[var(--bluevue-surface)] text-white"
|
|
25
|
+
:style="{ maxWidth: width || '615px' }"
|
|
26
|
+
>
|
|
27
|
+
<!-- Relative, so the bar's own shadow paints over the body below it rather than under it. -->
|
|
28
|
+
<header
|
|
29
|
+
v-if="title || $slots.header || $slots.actions"
|
|
30
|
+
class="bluevue-elevation-1 relative flex items-center gap-5 rounded-t-[8px] bg-[#15151577] px-5 py-3"
|
|
31
|
+
>
|
|
32
|
+
<slot name="header">
|
|
33
|
+
<img
|
|
34
|
+
v-if="logo"
|
|
35
|
+
:src="logo"
|
|
36
|
+
alt=""
|
|
37
|
+
class="h-6 w-6 shrink-0"
|
|
38
|
+
>
|
|
39
|
+
<BlueIcon
|
|
40
|
+
v-else-if="icon"
|
|
41
|
+
:name="icon"
|
|
42
|
+
:size="24"
|
|
43
|
+
/>
|
|
44
|
+
<span class="truncate text-lg font-medium">{{ title }}</span>
|
|
45
|
+
</slot>
|
|
46
|
+
<div class="ml-auto flex shrink-0 items-center gap-1">
|
|
47
|
+
<slot name="actions" />
|
|
48
|
+
</div>
|
|
49
|
+
</header>
|
|
50
|
+
|
|
51
|
+
<div :class="bodyClass || 'px-5 py-4'">
|
|
52
|
+
<slot />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<footer
|
|
56
|
+
v-if="$slots.footer"
|
|
57
|
+
class="flex items-center justify-between gap-3 border-t border-[#ffffff0d] px-5 py-3"
|
|
58
|
+
>
|
|
59
|
+
<slot name="footer" />
|
|
60
|
+
</footer>
|
|
61
|
+
</section>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
import BlueIcon from './BlueIcon.vue'
|
|
5
|
+
import BlueTooltip from './BlueTooltip.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A plain yes or no, laid out as the other controls are: what it is on the left, the box on the
|
|
9
|
+
* right. A native checkbox drives it, so it is reached and toggled from the keyboard as one.
|
|
10
|
+
*/
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
modelValue: boolean | null
|
|
13
|
+
/** Label on the left of the row. */
|
|
14
|
+
label?: string
|
|
15
|
+
/** Name and id, tying the label to the box. */
|
|
16
|
+
name: string
|
|
17
|
+
/** Neither on nor off, for a box standing for several things that disagree. */
|
|
18
|
+
indeterminate?: boolean
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
/** The fill when it is on (default the primary token). */
|
|
21
|
+
color?: string
|
|
22
|
+
/** A hint behind an info icon beside the box. */
|
|
23
|
+
infoTooltip?: string
|
|
24
|
+
theme?: 'light' | 'dark'
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{
|
|
28
|
+
(event: 'update:modelValue', value: boolean): void
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const checked = computed(() => props.modelValue === true)
|
|
32
|
+
|
|
33
|
+
const toggle = (): void => {
|
|
34
|
+
if (props.disabled) return
|
|
35
|
+
// An indeterminate box is answering for several things at once, and the answer it takes is yes.
|
|
36
|
+
emit('update:modelValue', props.indeterminate ? true : !checked.value)
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div class="flex w-full items-center justify-between">
|
|
42
|
+
<label
|
|
43
|
+
v-if="label"
|
|
44
|
+
:for="name"
|
|
45
|
+
class="min-w-0 max-w-[45%] shrink-0 truncate text-start mr-6"
|
|
46
|
+
:title="label"
|
|
47
|
+
:class="[theme === 'dark' ? 'text-white' : 'text-black', disabled ? 'opacity-30' : 'cursor-pointer']"
|
|
48
|
+
>{{ label }}</label>
|
|
49
|
+
<div v-else />
|
|
50
|
+
|
|
51
|
+
<div class="ml-auto flex shrink-0 items-center">
|
|
52
|
+
<BlueTooltip
|
|
53
|
+
v-if="infoTooltip"
|
|
54
|
+
:text="infoTooltip"
|
|
55
|
+
:theme="theme"
|
|
56
|
+
class="items-center mr-2"
|
|
57
|
+
>
|
|
58
|
+
<BlueIcon
|
|
59
|
+
name="mdi-information-outline"
|
|
60
|
+
:size="16"
|
|
61
|
+
:label="infoTooltip"
|
|
62
|
+
class="opacity-60 cursor-help"
|
|
63
|
+
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
64
|
+
/>
|
|
65
|
+
</BlueTooltip>
|
|
66
|
+
|
|
67
|
+
<!-- The native box is the control; the square beside it is what is actually seen, and the
|
|
68
|
+
two are one element to the pointer and to the keyboard because the label wraps both. -->
|
|
69
|
+
<label
|
|
70
|
+
class="relative inline-flex items-center"
|
|
71
|
+
:class="disabled ? 'opacity-30' : 'cursor-pointer'"
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
74
|
+
:id="name"
|
|
75
|
+
type="checkbox"
|
|
76
|
+
class="peer absolute h-0 w-0 opacity-0"
|
|
77
|
+
:name="name"
|
|
78
|
+
:checked="checked"
|
|
79
|
+
:disabled="disabled"
|
|
80
|
+
:aria-checked="indeterminate ? 'mixed' : checked"
|
|
81
|
+
@change="toggle"
|
|
82
|
+
>
|
|
83
|
+
<span
|
|
84
|
+
class="bluevue-elevation-1 flex h-[18px] w-[18px] items-center justify-center rounded-[4px] border transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--bluevue-accent)]"
|
|
85
|
+
:class="[
|
|
86
|
+
checked || indeterminate ? 'border-transparent' : theme === 'dark' ? 'border-[#ffffff44] bg-[#00000022]' : 'border-[#00000044] bg-white',
|
|
87
|
+
]"
|
|
88
|
+
:style="{ backgroundColor: checked || indeterminate ? color || 'var(--bluevue-primary)' : undefined }"
|
|
89
|
+
>
|
|
90
|
+
<BlueIcon
|
|
91
|
+
v-if="checked || indeterminate"
|
|
92
|
+
:name="indeterminate ? 'mdi-minus' : 'mdi-check'"
|
|
93
|
+
:size="14"
|
|
94
|
+
color="#ffffff"
|
|
95
|
+
/>
|
|
96
|
+
</span>
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
import BlueIcon from './BlueIcon.vue'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A small standing label: a state, a tag, a count. It says what something is rather than doing
|
|
8
|
+
* anything, so anything that acts on a press belongs in a BlueButton instead.
|
|
9
|
+
*/
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
/** The text. A default slot takes its place when given. */
|
|
12
|
+
label?: string
|
|
13
|
+
/** 'tonal' (default) is a fill of the colour at low alpha, 'filled' the colour itself. */
|
|
14
|
+
variant?: 'tonal' | 'filled' | 'outlined'
|
|
15
|
+
/** Any CSS colour, or one of the standard meanings. */
|
|
16
|
+
color?: 'success' | 'error' | 'warning' | 'info' | 'neutral' | string
|
|
17
|
+
/** An mdi name before the text. */
|
|
18
|
+
icon?: string
|
|
19
|
+
/** Offers a cross that emits close. */
|
|
20
|
+
closable?: boolean
|
|
21
|
+
size?: 'small' | 'regular'
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
defineEmits<{
|
|
25
|
+
(event: 'close'): void
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const MEANING: Record<string, string> = {
|
|
29
|
+
success: '#66BB6A',
|
|
30
|
+
error: '#EF5350',
|
|
31
|
+
warning: '#FFB74D',
|
|
32
|
+
info: '#42A5F5',
|
|
33
|
+
neutral: '#90A4AE',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tone = computed(() => MEANING[props.color ?? 'neutral'] ?? props.color ?? MEANING.neutral)
|
|
37
|
+
|
|
38
|
+
const filled = computed(() => props.variant === 'filled')
|
|
39
|
+
|
|
40
|
+
// A tonal chip is the tone at a tenth of itself, which reads as a tint of the surface rather than
|
|
41
|
+
// as something sitting on top of it. Colour-mix rather than an alpha suffix, since the tone may
|
|
42
|
+
// arrive as any CSS colour, including a variable.
|
|
43
|
+
const look = computed(() => ({
|
|
44
|
+
color: filled.value ? '#ffffff' : tone.value,
|
|
45
|
+
backgroundColor: filled.value ? tone.value : props.variant === 'outlined' ? 'transparent' : `color-mix(in srgb, ${tone.value} 18%, transparent)`,
|
|
46
|
+
borderColor: props.variant === 'outlined' ? tone.value : 'transparent',
|
|
47
|
+
}))
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<span
|
|
52
|
+
class="inline-flex max-w-full shrink-0 items-center gap-1 whitespace-nowrap rounded-full border"
|
|
53
|
+
:class="size === 'small' ? 'h-[18px] px-2 text-[11px]' : 'h-[22px] px-[10px] text-[12px]'"
|
|
54
|
+
:style="look"
|
|
55
|
+
>
|
|
56
|
+
<BlueIcon
|
|
57
|
+
v-if="icon"
|
|
58
|
+
:name="icon"
|
|
59
|
+
:size="size === 'small' ? 12 : 14"
|
|
60
|
+
/>
|
|
61
|
+
<span class="min-w-0 truncate">
|
|
62
|
+
<slot>{{ label }}</slot>
|
|
63
|
+
</span>
|
|
64
|
+
<button
|
|
65
|
+
v-if="closable"
|
|
66
|
+
type="button"
|
|
67
|
+
class="-mr-1 cursor-pointer opacity-70 transition-opacity hover:opacity-100"
|
|
68
|
+
:aria-label="`Remove ${label}`"
|
|
69
|
+
@click="$emit('close')"
|
|
70
|
+
>
|
|
71
|
+
<BlueIcon
|
|
72
|
+
name="mdi-close"
|
|
73
|
+
:size="size === 'small' ? 12 : 14"
|
|
74
|
+
/>
|
|
75
|
+
</button>
|
|
76
|
+
</span>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import BlueButton from './BlueButton.vue'
|
|
5
|
+
import BlueDialog from './BlueDialog.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Asks before something that cannot be taken back. The question is the title, what it will cost
|
|
9
|
+
* is the message, and the committing button says what it does rather than saying yes.
|
|
10
|
+
*/
|
|
11
|
+
defineProps<{
|
|
12
|
+
modelValue: boolean
|
|
13
|
+
title: string
|
|
14
|
+
/** What confirming will do, and to what. */
|
|
15
|
+
message?: string
|
|
16
|
+
/** Names the action rather than agreeing with the question: 'Delete', 'Restart', 'Overwrite'. */
|
|
17
|
+
confirmLabel?: string
|
|
18
|
+
cancelLabel?: string
|
|
19
|
+
/** Colours the committing button as a warning, for anything destructive. */
|
|
20
|
+
danger?: boolean
|
|
21
|
+
icon?: string
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
(event: 'update:modelValue', value: boolean): void
|
|
26
|
+
(event: 'confirm'): void
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const dialogRef = ref<InstanceType<typeof BlueDialog> | null>(null)
|
|
30
|
+
|
|
31
|
+
function confirm(): void {
|
|
32
|
+
dialogRef.value?.close()
|
|
33
|
+
emit('confirm')
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<BlueDialog
|
|
39
|
+
ref="dialogRef"
|
|
40
|
+
:model-value="modelValue"
|
|
41
|
+
:title="title"
|
|
42
|
+
:icon="icon || (danger ? 'mdi-alert-outline' : 'mdi-help-circle-outline')"
|
|
43
|
+
:icon-color="danger ? 'var(--bluevue-error)' : undefined"
|
|
44
|
+
width="440px"
|
|
45
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
46
|
+
>
|
|
47
|
+
<p
|
|
48
|
+
v-if="message"
|
|
49
|
+
class="text-sm leading-relaxed text-[#ffffffcc]"
|
|
50
|
+
>
|
|
51
|
+
{{ message }}
|
|
52
|
+
</p>
|
|
53
|
+
<slot />
|
|
54
|
+
|
|
55
|
+
<template #footer>
|
|
56
|
+
<BlueButton
|
|
57
|
+
variant="text"
|
|
58
|
+
density="compact"
|
|
59
|
+
@click="dialogRef?.close()"
|
|
60
|
+
>
|
|
61
|
+
{{ cancelLabel || 'Cancel' }}
|
|
62
|
+
</BlueButton>
|
|
63
|
+
<BlueButton
|
|
64
|
+
variant="filled"
|
|
65
|
+
density="compact"
|
|
66
|
+
:color="danger ? 'var(--bluevue-error)' : undefined"
|
|
67
|
+
@click="confirm"
|
|
68
|
+
>
|
|
69
|
+
{{ confirmLabel || 'Confirm' }}
|
|
70
|
+
</BlueButton>
|
|
71
|
+
</template>
|
|
72
|
+
</BlueDialog>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, useSlots, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import BlueButton from './BlueButton.vue'
|
|
5
|
+
import BlueIcon from './BlueIcon.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The surface every dialog in the kit is built on: a frosted panel in the browser's top layer,
|
|
9
|
+
* with an optional header, a body and an optional footer. Being a native modal, it dims the page
|
|
10
|
+
* itself, traps focus, and closes on Escape and on the backdrop unless it is told not to.
|
|
11
|
+
*/
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
modelValue: boolean
|
|
14
|
+
/** The heading. Without one, and without a header slot, the dialog is only its body. */
|
|
15
|
+
title?: string
|
|
16
|
+
/** An mdi name beside the title. */
|
|
17
|
+
icon?: string
|
|
18
|
+
iconColor?: string
|
|
19
|
+
/** A line under the title, for what the dialog is about to act on. */
|
|
20
|
+
subtitle?: string
|
|
21
|
+
/**
|
|
22
|
+
* Takes away Escape, the backdrop and the close button. For an operation that must not end up
|
|
23
|
+
* running under a live page.
|
|
24
|
+
*/
|
|
25
|
+
persistent?: boolean
|
|
26
|
+
/** Panel width (default '624px'). It never exceeds the viewport. */
|
|
27
|
+
width?: string
|
|
28
|
+
/** Replaces the body's own inset, for a dialog laying its contents out differently. */
|
|
29
|
+
bodyClass?: string
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(event: 'update:modelValue', value: boolean): void
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
const slots = useSlots()
|
|
37
|
+
const dialogRef = ref<HTMLDialogElement | null>(null)
|
|
38
|
+
|
|
39
|
+
const hasHeader = computed(() => Boolean(props.title || slots.header))
|
|
40
|
+
|
|
41
|
+
const sync = (show: boolean): void => {
|
|
42
|
+
const el = dialogRef.value
|
|
43
|
+
if (!el) return
|
|
44
|
+
if (show && !el.open) el.showModal()
|
|
45
|
+
if (!show && el.open) el.close()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onMounted(() => sync(props.modelValue))
|
|
49
|
+
watch(() => props.modelValue, sync)
|
|
50
|
+
|
|
51
|
+
// Every way out closes the element, and its close event is what reports back, so Escape and the
|
|
52
|
+
// backdrop need no separate answer.
|
|
53
|
+
const close = (): void => dialogRef.value?.close()
|
|
54
|
+
|
|
55
|
+
const onCancel = (event: Event): void => {
|
|
56
|
+
if (props.persistent) event.preventDefault()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A click landing on the dialog itself landed on the backdrop, since the panel covers the whole of
|
|
60
|
+
// the element's own box.
|
|
61
|
+
const onBackdropClick = (event: MouseEvent): void => {
|
|
62
|
+
if (!props.persistent && event.target === dialogRef.value) close()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
defineExpose({ close })
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<dialog
|
|
70
|
+
ref="dialogRef"
|
|
71
|
+
class="bluevue-dialog"
|
|
72
|
+
@close="emit('update:modelValue', false)"
|
|
73
|
+
@cancel="onCancel"
|
|
74
|
+
@click="onBackdropClick"
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
class="bluevue-panel relative max-w-full rounded-lg text-white"
|
|
78
|
+
:style="{ width: width || '624px' }"
|
|
79
|
+
>
|
|
80
|
+
<header
|
|
81
|
+
v-if="hasHeader"
|
|
82
|
+
class="flex items-start gap-3 px-5 pt-4 pb-3"
|
|
83
|
+
>
|
|
84
|
+
<slot name="header">
|
|
85
|
+
<BlueIcon
|
|
86
|
+
v-if="icon"
|
|
87
|
+
:name="icon"
|
|
88
|
+
:size="22"
|
|
89
|
+
:color="iconColor || '#42A5F5'"
|
|
90
|
+
class="mt-[2px]"
|
|
91
|
+
/>
|
|
92
|
+
<div class="min-w-0 flex-1">
|
|
93
|
+
<div class="truncate text-base leading-tight">
|
|
94
|
+
{{ title }}
|
|
95
|
+
</div>
|
|
96
|
+
<div
|
|
97
|
+
v-if="subtitle"
|
|
98
|
+
class="mt-1 text-xs leading-relaxed text-[#ffffff88]"
|
|
99
|
+
>
|
|
100
|
+
{{ subtitle }}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</slot>
|
|
104
|
+
<BlueButton
|
|
105
|
+
v-if="!persistent"
|
|
106
|
+
variant="icon"
|
|
107
|
+
icon="mdi-close"
|
|
108
|
+
tooltip="Close"
|
|
109
|
+
@click="close"
|
|
110
|
+
/>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<!-- With no header to hold it, the way out sits over the corner of the panel. -->
|
|
114
|
+
<BlueButton
|
|
115
|
+
v-else-if="!persistent"
|
|
116
|
+
variant="icon"
|
|
117
|
+
icon="mdi-close"
|
|
118
|
+
tooltip="Close"
|
|
119
|
+
class="absolute right-3 top-3 z-10"
|
|
120
|
+
@click="close"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<div :class="[bodyClass || 'px-5 py-4', hasHeader ? 'border-t border-[#ffffff0d]' : '']">
|
|
124
|
+
<slot />
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<footer
|
|
128
|
+
v-if="slots.footer"
|
|
129
|
+
class="flex items-center justify-between gap-3 border-t border-[#ffffff0d] px-5 py-3"
|
|
130
|
+
>
|
|
131
|
+
<slot name="footer" />
|
|
132
|
+
</footer>
|
|
133
|
+
</div>
|
|
134
|
+
</dialog>
|
|
135
|
+
</template>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import BlueIcon from './BlueIcon.vue'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A place to drop a file on, which is also a place to press for the file picker. It hands the
|
|
8
|
+
* files over and keeps none of them, so what is done with one, and whether it is any good, stays
|
|
9
|
+
* with the consumer.
|
|
10
|
+
*/
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
/** What the picker offers, as the accept attribute takes it (e.g. '.json,.param'). */
|
|
13
|
+
accept?: string
|
|
14
|
+
/** Takes more than one file at a time. */
|
|
15
|
+
multiple?: boolean
|
|
16
|
+
/** The line under the invitation, usually naming the formats that are understood. */
|
|
17
|
+
hint?: string
|
|
18
|
+
/** The invitation itself. */
|
|
19
|
+
label?: string
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
theme?: 'light' | 'dark'
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
(event: 'files', files: File[]): void
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const input = ref<HTMLInputElement>()
|
|
29
|
+
const hovering = ref(false)
|
|
30
|
+
// Dragging over a child fires leave on the parent, so the crossings are counted rather than
|
|
31
|
+
// treated as one, and the zone only cools down when the pointer has really left it.
|
|
32
|
+
const depth = ref(0)
|
|
33
|
+
|
|
34
|
+
const take = (files: FileList | null): void => {
|
|
35
|
+
if (props.disabled || !files?.length) return
|
|
36
|
+
emit('files', Array.from(files))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const onDrop = (event: DragEvent): void => {
|
|
40
|
+
depth.value = 0
|
|
41
|
+
hovering.value = false
|
|
42
|
+
take(event.dataTransfer?.files ?? null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onPick = (event: Event): void => {
|
|
46
|
+
const element = event.target as HTMLInputElement
|
|
47
|
+
take(element.files)
|
|
48
|
+
// Cleared so picking the same file twice in a row is still an event the second time.
|
|
49
|
+
element.value = ''
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div
|
|
55
|
+
class="w-full"
|
|
56
|
+
:class="disabled ? 'pointer-events-none opacity-30' : ''"
|
|
57
|
+
>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
class="flex w-full cursor-pointer flex-col items-center justify-center gap-1 rounded-[8px] border border-dashed px-4 py-6 text-center transition-colors"
|
|
61
|
+
:class="[
|
|
62
|
+
hovering
|
|
63
|
+
? 'border-[var(--bluevue-accent)] bg-[var(--bluevue-accent)]/10'
|
|
64
|
+
: theme === 'light' ? 'border-[#00000033] hover:bg-[#00000006]' : 'border-[#ffffff33] hover:bg-[#ffffff08]',
|
|
65
|
+
theme === 'light' ? 'text-black' : 'text-white',
|
|
66
|
+
]"
|
|
67
|
+
@click="input?.click()"
|
|
68
|
+
@dragenter.prevent="depth += 1; hovering = true"
|
|
69
|
+
@dragover.prevent
|
|
70
|
+
@dragleave.prevent="depth -= 1; hovering = depth > 0"
|
|
71
|
+
@drop.prevent="onDrop"
|
|
72
|
+
>
|
|
73
|
+
<BlueIcon
|
|
74
|
+
name="mdi-tray-arrow-up"
|
|
75
|
+
:size="24"
|
|
76
|
+
class="opacity-70"
|
|
77
|
+
/>
|
|
78
|
+
<span class="text-[13px]">{{ label ?? 'Drop a file here, or press to choose one' }}</span>
|
|
79
|
+
<span
|
|
80
|
+
v-if="hint"
|
|
81
|
+
class="text-[11px] opacity-60"
|
|
82
|
+
>{{ hint }}</span>
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<input
|
|
86
|
+
ref="input"
|
|
87
|
+
type="file"
|
|
88
|
+
class="hidden"
|
|
89
|
+
:accept="accept"
|
|
90
|
+
:multiple="multiple"
|
|
91
|
+
@change="onPick"
|
|
92
|
+
>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Material Design Icon in a square box, so a row of them lines up whichever glyphs it carries
|
|
6
|
+
* and however wide each one happens to advance.
|
|
7
|
+
*/
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
/** Icon name, with or without the `mdi-` prefix. */
|
|
10
|
+
name: string
|
|
11
|
+
/** Side of the box, in pixels when given as a number (default 20). */
|
|
12
|
+
size?: number | string
|
|
13
|
+
color?: string
|
|
14
|
+
/** Turns, for something under way. */
|
|
15
|
+
spin?: boolean
|
|
16
|
+
/** Read out in place of the icon. Without one the icon is hidden from assistive technology. */
|
|
17
|
+
label?: string
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const iconClass = computed(() => (props.name.startsWith('mdi-') ? props.name : `mdi-${props.name}`))
|
|
21
|
+
|
|
22
|
+
const box = computed(() => {
|
|
23
|
+
const size = props.size ?? 20
|
|
24
|
+
return typeof size === 'number' ? `${size}px` : size
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<span
|
|
30
|
+
class="mdi bluevue-icon"
|
|
31
|
+
:class="[iconClass, spin ? 'bluevue-icon--spin' : '']"
|
|
32
|
+
:style="{ width: box, height: box, fontSize: box, color }"
|
|
33
|
+
:role="label ? 'img' : undefined"
|
|
34
|
+
:aria-label="label"
|
|
35
|
+
:aria-hidden="label ? undefined : 'true'"
|
|
36
|
+
/>
|
|
37
|
+
</template>
|