@bluerobotics/bluevue 0.1.1 → 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
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
<div v-else />
|
|
20
20
|
<slot name="insetElement" />
|
|
21
21
|
|
|
22
|
-
<
|
|
22
|
+
<BlueTooltip
|
|
23
23
|
v-if="infoTooltip"
|
|
24
|
-
|
|
24
|
+
:text="infoTooltip"
|
|
25
|
+
:theme="theme"
|
|
26
|
+
class="items-center shrink-0 mr-2 ml-auto"
|
|
25
27
|
>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
+
<BlueIcon
|
|
29
|
+
name="mdi-information-outline"
|
|
30
|
+
:size="16"
|
|
31
|
+
:label="infoTooltip"
|
|
32
|
+
class="opacity-60 cursor-help"
|
|
28
33
|
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
29
34
|
/>
|
|
30
|
-
|
|
31
|
-
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]"
|
|
32
|
-
>
|
|
33
|
-
{{ infoTooltip }}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
35
|
+
</BlueTooltip>
|
|
36
36
|
|
|
37
37
|
<div class="flex flex-col items-end min-w-0 shrink-[1000]">
|
|
38
38
|
<!-- The inset shadow darkens a band along the top edge, which the eye takes for the rim of
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
>
|
|
56
56
|
<!-- Always a text input, even for numbers: a native number input reports a half-typed
|
|
57
57
|
value ('-', '-27.') as an empty string, which would wipe the model mid-keystroke. -->
|
|
58
|
+
<div
|
|
59
|
+
v-if="$slots.prepend"
|
|
60
|
+
class="flex shrink-0 items-center pl-2"
|
|
61
|
+
>
|
|
62
|
+
<slot name="prepend" />
|
|
63
|
+
</div>
|
|
58
64
|
<input
|
|
59
65
|
:id="name"
|
|
60
66
|
ref="inputRef"
|
|
@@ -65,8 +71,9 @@
|
|
|
65
71
|
:disabled="disabled"
|
|
66
72
|
:autofocus="autofocus"
|
|
67
73
|
:inputmode="type === 'number' ? 'decimal' : undefined"
|
|
68
|
-
class="w-full min-w-0 bg-transparent
|
|
74
|
+
class="w-full min-w-0 bg-transparent text-sm font-medium outline-none"
|
|
69
75
|
:class="[
|
|
76
|
+
$slots.prepend ? 'pl-1' : 'pl-4',
|
|
70
77
|
suffix ? 'pr-1' : 'pr-4',
|
|
71
78
|
theme === 'dark' ? 'text-white placeholder:text-[#ffffff44]' : 'text-black placeholder:text-[#00000055]',
|
|
72
79
|
]"
|
|
@@ -108,6 +115,9 @@
|
|
|
108
115
|
<script setup lang="ts">
|
|
109
116
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
110
117
|
|
|
118
|
+
import BlueIcon from './BlueIcon.vue'
|
|
119
|
+
import BlueTooltip from './BlueTooltip.vue'
|
|
120
|
+
|
|
111
121
|
const props = defineProps<{
|
|
112
122
|
/**
|
|
113
123
|
* Model value for v-model. A 'number' field emits numbers and never emits an empty value:
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { onMounted, ref, watch } from 'vue'
|
|
3
|
-
|
|
4
2
|
import logo from '../assets/br-logo-white.svg'
|
|
3
|
+
import BlueDialog from './BlueDialog.vue'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* A blocking overlay for an operation that is under way, such as a vehicle restarting. It is up
|
|
8
7
|
* for exactly as long as `modelValue` says it is, and unless it is dismissible there is no way
|
|
9
8
|
* out of it.
|
|
10
9
|
*/
|
|
11
|
-
|
|
10
|
+
defineProps<{
|
|
12
11
|
modelValue: boolean
|
|
13
12
|
/** What the operation is, shown under the propeller. */
|
|
14
13
|
message?: string
|
|
@@ -19,58 +18,28 @@ const props = defineProps<{
|
|
|
19
18
|
const emit = defineEmits<{
|
|
20
19
|
(event: 'update:modelValue', value: boolean): void
|
|
21
20
|
}>()
|
|
22
|
-
|
|
23
|
-
const dialogRef = ref<HTMLDialogElement | null>(null)
|
|
24
|
-
|
|
25
|
-
const sync = (show: boolean): void => {
|
|
26
|
-
const el = dialogRef.value
|
|
27
|
-
if (!el) return
|
|
28
|
-
if (show && !el.open) el.showModal()
|
|
29
|
-
if (!show && el.open) el.close()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const close = (): void => emit('update:modelValue', false)
|
|
33
|
-
|
|
34
|
-
// Escape closes a dismissible overlay through the same path as the button, so the caller's
|
|
35
|
-
// state follows the dialog instead of thinking it is still up.
|
|
36
|
-
const onCancel = (event: Event): void => {
|
|
37
|
-
event.preventDefault()
|
|
38
|
-
if (props.dismissible) close()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
onMounted(() => sync(props.modelValue))
|
|
42
|
-
watch(() => props.modelValue, sync)
|
|
43
21
|
</script>
|
|
44
22
|
|
|
45
23
|
<template>
|
|
46
|
-
<!--
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
24
|
+
<!-- Persistent unless the wait is dismissible: an operation the user cannot interrupt must not
|
|
25
|
+
end up running under a live page. -->
|
|
26
|
+
<BlueDialog
|
|
27
|
+
:model-value="modelValue"
|
|
28
|
+
:persistent="!dismissible"
|
|
29
|
+
width="320px"
|
|
30
|
+
body-class="flex flex-col items-center justify-center px-6 pt-7 pb-5"
|
|
31
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
52
32
|
>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
alt=""
|
|
66
|
-
class="bluevue-loading__icon mb-6 h-[100px] w-[100px]"
|
|
67
|
-
>
|
|
68
|
-
<span
|
|
69
|
-
v-if="message"
|
|
70
|
-
class="mt-[15px] text-center text-base text-white"
|
|
71
|
-
>
|
|
72
|
-
{{ message }}
|
|
73
|
-
</span>
|
|
74
|
-
</div>
|
|
75
|
-
</dialog>
|
|
33
|
+
<img
|
|
34
|
+
:src="logo"
|
|
35
|
+
alt=""
|
|
36
|
+
class="bluevue-loading__icon mb-6 h-[100px] w-[100px]"
|
|
37
|
+
>
|
|
38
|
+
<span
|
|
39
|
+
v-if="message"
|
|
40
|
+
class="mt-[15px] text-center text-base text-white"
|
|
41
|
+
>
|
|
42
|
+
{{ message }}
|
|
43
|
+
</span>
|
|
44
|
+
</BlueDialog>
|
|
76
45
|
</template>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How far along something is. Give it a value for work whose size is known, and set
|
|
6
|
+
* `indeterminate` for work that is under way but cannot say how much of it is left.
|
|
7
|
+
*/
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
/** Percentage complete, clamped to 0-100. */
|
|
10
|
+
modelValue?: number
|
|
11
|
+
/** Sweeps instead of filling, for progress that cannot be measured. */
|
|
12
|
+
indeterminate?: boolean
|
|
13
|
+
/** Bar thickness in pixels (default 4). */
|
|
14
|
+
height?: number
|
|
15
|
+
/** The fill (default the primary token). */
|
|
16
|
+
color?: string
|
|
17
|
+
theme?: 'light' | 'dark'
|
|
18
|
+
/** What is progressing, read out by assistive technology. */
|
|
19
|
+
label?: string
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
// A value outside the range is a caller's arithmetic, not a bar that should paint past its track.
|
|
23
|
+
const percent = computed(() => Math.min(100, Math.max(0, props.modelValue ?? 0)))
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div
|
|
28
|
+
class="bluevue-progress w-full overflow-hidden rounded-full"
|
|
29
|
+
:class="theme === 'light' ? 'bg-[#00000022]' : 'bg-[#ffffff1a]'"
|
|
30
|
+
:style="{ height: `${height ?? 4}px` }"
|
|
31
|
+
role="progressbar"
|
|
32
|
+
:aria-label="label"
|
|
33
|
+
:aria-valuenow="indeterminate ? undefined : percent"
|
|
34
|
+
aria-valuemin="0"
|
|
35
|
+
aria-valuemax="100"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
class="h-full rounded-full"
|
|
39
|
+
:class="indeterminate ? 'bluevue-progress__sweep' : 'transition-[width] duration-200 ease-out'"
|
|
40
|
+
:style="{
|
|
41
|
+
width: indeterminate ? undefined : `${percent}%`,
|
|
42
|
+
backgroundColor: color || 'var(--bluevue-primary)',
|
|
43
|
+
}"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
3
|
|
|
4
|
+
import BlueButton from './BlueButton.vue'
|
|
5
|
+
import BlueDialog from './BlueDialog.vue'
|
|
4
6
|
import BlueInput from './BlueInput.vue'
|
|
5
7
|
|
|
6
8
|
/**
|
|
@@ -31,121 +33,71 @@ const emit = defineEmits<{
|
|
|
31
33
|
|
|
32
34
|
const name = ref(props.initial ?? '')
|
|
33
35
|
const notes = ref(props.notesInitial ?? '')
|
|
34
|
-
const dialogRef = ref<
|
|
36
|
+
const dialogRef = ref<InstanceType<typeof BlueDialog> | null>(null)
|
|
35
37
|
|
|
36
38
|
// Reset on open rather than on close, so the fields are right before the dialog animates in.
|
|
37
39
|
watch(
|
|
38
40
|
() => props.modelValue,
|
|
39
41
|
(open) => {
|
|
40
|
-
if (open)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
const el = dialogRef.value
|
|
45
|
-
if (!el) return
|
|
46
|
-
if (open && !el.open) el.showModal()
|
|
47
|
-
if (!open && el.open) el.close()
|
|
42
|
+
if (!open) return
|
|
43
|
+
name.value = props.initial ?? ''
|
|
44
|
+
notes.value = props.notesInitial ?? ''
|
|
48
45
|
}
|
|
49
46
|
)
|
|
50
47
|
|
|
51
|
-
onMounted(() => {
|
|
52
|
-
if (props.modelValue) dialogRef.value?.showModal()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
function close(): void {
|
|
56
|
-
// Every way out of the dialog closes the element, and its close event is what reports back,
|
|
57
|
-
// so Escape and the backdrop need no separate handling.
|
|
58
|
-
dialogRef.value?.close()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
48
|
function confirm(): void {
|
|
62
49
|
const trimmed = name.value.trim()
|
|
63
50
|
if (!trimmed) return
|
|
64
|
-
close()
|
|
51
|
+
dialogRef.value?.close()
|
|
65
52
|
emit('confirm', trimmed, notes.value.trim())
|
|
66
53
|
}
|
|
67
|
-
|
|
68
|
-
// A click landing on the dialog itself is a click on the backdrop, since the panel covers the
|
|
69
|
-
// whole of the element's own box.
|
|
70
|
-
function onBackdropClick(event: MouseEvent): void {
|
|
71
|
-
if (event.target === dialogRef.value) close()
|
|
72
|
-
}
|
|
73
54
|
</script>
|
|
74
55
|
|
|
75
56
|
<template>
|
|
76
|
-
<
|
|
57
|
+
<BlueDialog
|
|
77
58
|
ref="dialogRef"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
:model-value="modelValue"
|
|
60
|
+
:title="title"
|
|
61
|
+
:subtitle="subtitle"
|
|
62
|
+
:icon="icon || 'mdi-content-save-outline'"
|
|
63
|
+
body-class="flex flex-col gap-3 px-5 py-4"
|
|
64
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
81
65
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
type="button"
|
|
101
|
-
class="cursor-pointer text-[#ffffffaa] transition-colors hover:text-white"
|
|
102
|
-
title="Close"
|
|
103
|
-
@click="close"
|
|
104
|
-
>
|
|
105
|
-
<span class="mdi mdi-close text-[20px] leading-none" />
|
|
106
|
-
</button>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<div class="flex flex-col gap-3 border-t border-[#ffffff0d] px-5 py-4">
|
|
110
|
-
<BlueInput
|
|
111
|
-
v-model="name"
|
|
112
|
-
name="bluevue-prompt-name"
|
|
113
|
-
:label="label"
|
|
114
|
-
theme="dark"
|
|
115
|
-
width="380px"
|
|
116
|
-
autofocus
|
|
117
|
-
@keydown.enter="confirm"
|
|
118
|
-
/>
|
|
119
|
-
<BlueInput
|
|
120
|
-
v-if="notesLabel"
|
|
121
|
-
v-model="notes"
|
|
122
|
-
name="bluevue-prompt-notes"
|
|
123
|
-
:label="notesLabel"
|
|
124
|
-
theme="dark"
|
|
125
|
-
width="380px"
|
|
126
|
-
@keydown.enter="confirm"
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
66
|
+
<BlueInput
|
|
67
|
+
v-model="name"
|
|
68
|
+
name="bluevue-prompt-name"
|
|
69
|
+
:label="label"
|
|
70
|
+
theme="dark"
|
|
71
|
+
width="380px"
|
|
72
|
+
autofocus
|
|
73
|
+
@keydown.enter="confirm"
|
|
74
|
+
/>
|
|
75
|
+
<BlueInput
|
|
76
|
+
v-if="notesLabel"
|
|
77
|
+
v-model="notes"
|
|
78
|
+
name="bluevue-prompt-notes"
|
|
79
|
+
:label="notesLabel"
|
|
80
|
+
theme="dark"
|
|
81
|
+
width="380px"
|
|
82
|
+
@keydown.enter="confirm"
|
|
83
|
+
/>
|
|
129
84
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</dialog>
|
|
85
|
+
<template #footer>
|
|
86
|
+
<BlueButton
|
|
87
|
+
density="compact"
|
|
88
|
+
@click="dialogRef?.close()"
|
|
89
|
+
>
|
|
90
|
+
Cancel
|
|
91
|
+
</BlueButton>
|
|
92
|
+
<span class="flex-1 text-center text-xs text-[#ffffff88]">{{ hint }}</span>
|
|
93
|
+
<BlueButton
|
|
94
|
+
variant="filled"
|
|
95
|
+
density="compact"
|
|
96
|
+
:disabled="!name.trim()"
|
|
97
|
+
@click="confirm"
|
|
98
|
+
>
|
|
99
|
+
{{ confirmLabel || 'Save' }}
|
|
100
|
+
</BlueButton>
|
|
101
|
+
</template>
|
|
102
|
+
</BlueDialog>
|
|
151
103
|
</template>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BlueIcon from './BlueIcon.vue'
|
|
3
|
+
import BlueTooltip from './BlueTooltip.vue'
|
|
4
|
+
|
|
5
|
+
/** One of the choices in the group. */
|
|
6
|
+
export interface BlueRadioItem {
|
|
7
|
+
label: string
|
|
8
|
+
value: string | number
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
/** Why the choice is what it sounds like, or why it is out of reach. */
|
|
11
|
+
hint?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One choice out of a few, each spelled out. Where a BlueButtonGroup packs the options into a
|
|
16
|
+
* track, this gives every one of them a line of its own, which is what a choice needing an
|
|
17
|
+
* explanation apiece wants.
|
|
18
|
+
*/
|
|
19
|
+
defineProps<{
|
|
20
|
+
modelValue: string | number | null
|
|
21
|
+
items: BlueRadioItem[]
|
|
22
|
+
/** Name of the group, which is what makes the choices exclusive. */
|
|
23
|
+
name: string
|
|
24
|
+
/** Label above the choices. */
|
|
25
|
+
label?: string
|
|
26
|
+
/** Lays the choices out along a row instead of down a column. */
|
|
27
|
+
inline?: boolean
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
theme?: 'light' | 'dark'
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(event: 'update:modelValue', value: string | number): void
|
|
34
|
+
}>()
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<fieldset
|
|
39
|
+
class="w-full min-w-0"
|
|
40
|
+
:class="disabled ? 'opacity-30' : ''"
|
|
41
|
+
>
|
|
42
|
+
<legend
|
|
43
|
+
v-if="label"
|
|
44
|
+
class="mb-2 text-[13px]"
|
|
45
|
+
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
46
|
+
>
|
|
47
|
+
{{ label }}
|
|
48
|
+
</legend>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
class="flex"
|
|
52
|
+
:class="inline ? 'flex-wrap items-center gap-x-5 gap-y-2' : 'flex-col gap-2'"
|
|
53
|
+
>
|
|
54
|
+
<label
|
|
55
|
+
v-for="item in items"
|
|
56
|
+
:key="item.value"
|
|
57
|
+
class="flex items-center gap-2"
|
|
58
|
+
:class="[
|
|
59
|
+
disabled || item.disabled ? 'cursor-not-allowed opacity-30' : 'cursor-pointer',
|
|
60
|
+
theme === 'dark' ? 'text-white' : 'text-black',
|
|
61
|
+
]"
|
|
62
|
+
>
|
|
63
|
+
<input
|
|
64
|
+
type="radio"
|
|
65
|
+
class="peer absolute h-0 w-0 opacity-0"
|
|
66
|
+
:name="name"
|
|
67
|
+
:value="item.value"
|
|
68
|
+
:checked="modelValue === item.value"
|
|
69
|
+
:disabled="disabled || item.disabled"
|
|
70
|
+
@change="emit('update:modelValue', item.value)"
|
|
71
|
+
>
|
|
72
|
+
<!-- The dot is drawn rather than left to the platform, which paints a radio in its own
|
|
73
|
+
light colours whatever the surface around it is. -->
|
|
74
|
+
<span
|
|
75
|
+
class="flex h-[16px] w-[16px] shrink-0 items-center justify-center rounded-full 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)]"
|
|
76
|
+
:class="modelValue === item.value
|
|
77
|
+
? 'border-[var(--bluevue-primary)]'
|
|
78
|
+
: theme === 'dark' ? 'border-[#ffffff44]' : 'border-[#00000044]'"
|
|
79
|
+
>
|
|
80
|
+
<span
|
|
81
|
+
v-if="modelValue === item.value"
|
|
82
|
+
class="h-[8px] w-[8px] rounded-full bg-[var(--bluevue-primary)]"
|
|
83
|
+
/>
|
|
84
|
+
</span>
|
|
85
|
+
|
|
86
|
+
<span class="min-w-0 truncate text-[13px]">{{ item.label }}</span>
|
|
87
|
+
|
|
88
|
+
<BlueTooltip
|
|
89
|
+
v-if="item.hint"
|
|
90
|
+
:text="item.hint"
|
|
91
|
+
:theme="theme"
|
|
92
|
+
class="items-center"
|
|
93
|
+
>
|
|
94
|
+
<BlueIcon
|
|
95
|
+
name="mdi-information-outline"
|
|
96
|
+
:size="14"
|
|
97
|
+
:label="item.hint"
|
|
98
|
+
class="opacity-60"
|
|
99
|
+
/>
|
|
100
|
+
</BlueTooltip>
|
|
101
|
+
</label>
|
|
102
|
+
</div>
|
|
103
|
+
</fieldset>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A subject inside a panel: a quiet heading over a stack of rows. Smaller than a
|
|
4
|
+
* BlueExpansiblePanel, which is a section of the page rather than a part of one, and not foldable.
|
|
5
|
+
*/
|
|
6
|
+
defineProps<{
|
|
7
|
+
title: string
|
|
8
|
+
/** Space between the rows, in pixels (default 12). */
|
|
9
|
+
gap?: number
|
|
10
|
+
}>()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<section>
|
|
15
|
+
<div class="mb-2 flex items-center gap-2">
|
|
16
|
+
<span class="text-[13px] uppercase tracking-wide text-[#ffffff88]">{{ title }}</span>
|
|
17
|
+
<!-- Notes about the subject sit on its heading line, where they are read before the rows
|
|
18
|
+
they qualify rather than after them. The rest of the row is theirs. -->
|
|
19
|
+
<div
|
|
20
|
+
v-if="$slots.notes"
|
|
21
|
+
class="min-w-0 flex-1"
|
|
22
|
+
>
|
|
23
|
+
<slot name="notes" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div
|
|
27
|
+
class="flex flex-col"
|
|
28
|
+
:style="{ gap: `${gap ?? 12}px` }"
|
|
29
|
+
>
|
|
30
|
+
<slot />
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
</template>
|
|
@@ -18,20 +18,20 @@
|
|
|
18
18
|
<div v-else />
|
|
19
19
|
<slot name="insetElement" />
|
|
20
20
|
|
|
21
|
-
<
|
|
21
|
+
<BlueTooltip
|
|
22
22
|
v-if="infoTooltip"
|
|
23
|
-
|
|
23
|
+
:text="infoTooltip"
|
|
24
|
+
:theme="theme"
|
|
25
|
+
class="items-center shrink-0 mr-2 ml-auto"
|
|
24
26
|
>
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
+
<BlueIcon
|
|
28
|
+
name="mdi-information-outline"
|
|
29
|
+
:size="16"
|
|
30
|
+
:label="infoTooltip"
|
|
31
|
+
class="opacity-60 cursor-help"
|
|
27
32
|
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
28
33
|
/>
|
|
29
|
-
|
|
30
|
-
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]"
|
|
31
|
-
>
|
|
32
|
-
{{ infoTooltip }}
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
34
|
+
</BlueTooltip>
|
|
35
35
|
|
|
36
36
|
<div class="flex flex-col items-end min-w-0 shrink-[1000]">
|
|
37
37
|
<button
|
|
@@ -112,6 +112,8 @@
|
|
|
112
112
|
import { computed, ref, watch } from 'vue'
|
|
113
113
|
|
|
114
114
|
import { useBluePopover } from '../composables/useBluePopover'
|
|
115
|
+
import BlueIcon from './BlueIcon.vue'
|
|
116
|
+
import BlueTooltip from './BlueTooltip.vue'
|
|
115
117
|
|
|
116
118
|
/**
|
|
117
119
|
* Option in the select
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
:class="[theme === 'dark' ? 'text-white' : 'text-black', disabled ? 'opacity-30' : '']"
|
|
14
14
|
>{{ label }}</label>
|
|
15
15
|
</div>
|
|
16
|
-
|
|
17
|
-
only ellipsizes the label once the track is down to its floor. -->
|
|
18
|
-
<div class="flex justify-between items-center min-w-0 shrink-[1000]">
|
|
16
|
+
<div class="flex w-1/2 min-w-0 items-center justify-between">
|
|
19
17
|
<div
|
|
20
18
|
name="slider-track"
|
|
21
19
|
class="relative overflow-visible rounded-[6px] bluevue-elevation-1 min-w-[140px] max-w-full"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { type BlueNoticeSeverity, useBlueSnackbar } from '../composables/useBlueSnackbar'
|
|
5
|
+
import BlueButton from './BlueButton.vue'
|
|
6
|
+
import BlueIcon from './BlueIcon.vue'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Puts the application's notices on screen, newest nearest the edge. Mount it once, at the root;
|
|
10
|
+
* BlueApp already does. Raising a notice is useBlueSnackbar's job, from wherever it happened.
|
|
11
|
+
*/
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
/** Which corner they gather in (default 'bottom-right'). */
|
|
14
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const { notices, dismiss } = useBlueSnackbar()
|
|
18
|
+
|
|
19
|
+
const LOOK: Record<BlueNoticeSeverity, { icon: string; color: string }> = {
|
|
20
|
+
success: { icon: 'mdi-check-circle-outline', color: '#66BB6A' },
|
|
21
|
+
error: { icon: 'mdi-alert-circle-outline', color: 'var(--bluevue-error)' },
|
|
22
|
+
warning: { icon: 'mdi-alert-outline', color: '#FFB74D' },
|
|
23
|
+
info: { icon: 'mdi-information-outline', color: '#42A5F5' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const corner = computed(() => {
|
|
27
|
+
switch (props.position) {
|
|
28
|
+
case 'bottom-left':
|
|
29
|
+
return 'bottom-4 left-4 items-start'
|
|
30
|
+
case 'top-right':
|
|
31
|
+
return 'top-4 right-4 items-end'
|
|
32
|
+
case 'top-left':
|
|
33
|
+
return 'top-4 left-4 items-start'
|
|
34
|
+
default:
|
|
35
|
+
return 'bottom-4 right-4 items-end'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Newest nearest the edge the stack grows from, so the one that just arrived is where the eye
|
|
40
|
+
// already is.
|
|
41
|
+
const stacked = computed(() => (props.position?.startsWith('top') ? [...notices.value].reverse() : notices.value))
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div
|
|
46
|
+
class="pointer-events-none fixed z-[2000] flex max-w-[min(90vw,420px)] flex-col gap-2"
|
|
47
|
+
:class="corner"
|
|
48
|
+
role="status"
|
|
49
|
+
aria-live="polite"
|
|
50
|
+
>
|
|
51
|
+
<TransitionGroup name="bluevue-notice">
|
|
52
|
+
<div
|
|
53
|
+
v-for="notice in stacked"
|
|
54
|
+
:key="notice.id"
|
|
55
|
+
class="bluevue-panel pointer-events-auto flex items-center gap-3 rounded-[6px] py-2 pl-3 pr-2 text-white"
|
|
56
|
+
>
|
|
57
|
+
<BlueIcon
|
|
58
|
+
:name="LOOK[notice.severity].icon"
|
|
59
|
+
:size="18"
|
|
60
|
+
:color="LOOK[notice.severity].color"
|
|
61
|
+
/>
|
|
62
|
+
<span class="min-w-0 flex-1 text-[13px] leading-snug">
|
|
63
|
+
{{ notice.text }}
|
|
64
|
+
<span
|
|
65
|
+
v-if="notice.repeats > 1"
|
|
66
|
+
class="text-[#ffffff88]"
|
|
67
|
+
>(×{{ notice.repeats }})</span>
|
|
68
|
+
</span>
|
|
69
|
+
<BlueButton
|
|
70
|
+
variant="icon"
|
|
71
|
+
density="compact"
|
|
72
|
+
icon="mdi-close"
|
|
73
|
+
tooltip="Dismiss"
|
|
74
|
+
@click="dismiss(notice.id)"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</TransitionGroup>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|