@bluerobotics/bluevue 0.1.1 → 0.2.1

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 (91) hide show
  1. package/README.md +97 -18
  2. package/bin/create-blueos-extension.mjs +72 -0
  3. package/dist/bluevue.js +2464 -778
  4. package/dist/components/BlueApp.vue.d.ts +50 -0
  5. package/dist/components/BlueBanner.vue.d.ts +49 -0
  6. package/dist/components/BlueBannerGroup.vue.d.ts +20 -0
  7. package/dist/components/BlueButton.vue.d.ts +59 -0
  8. package/dist/components/BlueCard.vue.d.ts +45 -0
  9. package/dist/components/BlueCheckbox.vue.d.ts +34 -0
  10. package/dist/components/BlueChip.vue.d.ts +47 -0
  11. package/dist/components/BlueConfirmDialog.vue.d.ts +193 -0
  12. package/dist/components/BlueDialog.vue.d.ts +60 -0
  13. package/dist/components/BlueFileDrop.vue.d.ts +32 -0
  14. package/dist/components/BlueIcon.vue.d.ts +26 -0
  15. package/dist/components/BlueInput.vue.d.ts +1 -0
  16. package/dist/components/BlueProgressBar.vue.d.ts +28 -0
  17. package/dist/components/BlueRadioGroup.vue.d.ts +40 -0
  18. package/dist/components/BlueSection.vue.d.ts +36 -0
  19. package/dist/components/BlueSnackbar.vue.d.ts +19 -0
  20. package/dist/components/BlueSpinner.vue.d.ts +25 -0
  21. package/dist/components/BlueStat.vue.d.ts +21 -0
  22. package/dist/components/BlueStepsDialog.vue.d.ts +62 -0
  23. package/dist/components/BlueTable.vue.d.ts +62 -0
  24. package/dist/components/BlueTabs.vue.d.ts +37 -0
  25. package/dist/components/BlueTextarea.vue.d.ts +40 -0
  26. package/dist/components/BlueTooltip.vue.d.ts +44 -0
  27. package/dist/components/BlueWindRose.vue.d.ts +37 -0
  28. package/dist/composables/useBlueLoading.d.ts +22 -0
  29. package/dist/composables/useBlueOs.d.ts +39 -0
  30. package/dist/composables/useBlueSnackbar.d.ts +30 -0
  31. package/dist/index.d.ts +30 -0
  32. package/dist/services/blueos.d.ts +37 -0
  33. package/dist/style.css +1 -1
  34. package/dist/types/banner.d.ts +16 -0
  35. package/dist/utils/files.d.ts +13 -0
  36. package/dist/utils/theme.d.ts +22 -0
  37. package/package.json +8 -2
  38. package/src/components/BlueApp.vue +84 -0
  39. package/src/components/BlueBanner.vue +196 -0
  40. package/src/components/BlueBannerGroup.vue +47 -0
  41. package/src/components/BlueButton.vue +134 -0
  42. package/src/components/BlueButtonGroup.vue +28 -26
  43. package/src/components/BlueCard.vue +62 -0
  44. package/src/components/BlueCheckbox.vue +100 -0
  45. package/src/components/BlueChip.vue +77 -0
  46. package/src/components/BlueConfirmDialog.vue +73 -0
  47. package/src/components/BlueDialog.vue +135 -0
  48. package/src/components/BlueFileDrop.vue +94 -0
  49. package/src/components/BlueIcon.vue +37 -0
  50. package/src/components/BlueInput.vue +23 -13
  51. package/src/components/BlueLoadingDialog.vue +22 -53
  52. package/src/components/BlueProgressBar.vue +46 -0
  53. package/src/components/BluePromptDialog.vue +51 -99
  54. package/src/components/BlueRadioGroup.vue +104 -0
  55. package/src/components/BlueSection.vue +33 -0
  56. package/src/components/BlueSelect.vue +13 -11
  57. package/src/components/BlueSlider.vue +1 -3
  58. package/src/components/BlueSnackbar.vue +79 -0
  59. package/src/components/BlueSpinner.vue +37 -0
  60. package/src/components/BlueStat.vue +26 -0
  61. package/src/components/BlueStepsDialog.vue +180 -0
  62. package/src/components/BlueSwitch.vue +18 -15
  63. package/src/components/BlueTable.vue +154 -0
  64. package/src/components/BlueTabs.vue +77 -0
  65. package/src/components/BlueTextarea.vue +102 -0
  66. package/src/components/BlueTooltip.vue +88 -0
  67. package/src/components/BlueWindRose.vue +269 -0
  68. package/src/composables/useBlueLoading.ts +38 -0
  69. package/src/composables/useBlueOs.ts +154 -0
  70. package/src/composables/useBlueSnackbar.ts +89 -0
  71. package/src/index.ts +40 -0
  72. package/src/services/blueos.ts +95 -0
  73. package/src/styles/bluevue.css +144 -0
  74. package/src/types/banner.ts +17 -0
  75. package/src/utils/files.ts +44 -0
  76. package/src/utils/theme.ts +37 -0
  77. package/templates/extension/.github/workflows/deploy.yml +23 -0
  78. package/templates/extension/Dockerfile +56 -0
  79. package/templates/extension/README.md +35 -0
  80. package/templates/extension/backend/main.py +45 -0
  81. package/templates/extension/backend/requirements.txt +2 -0
  82. package/templates/extension/frontend/.eslintrc.cjs +20 -0
  83. package/templates/extension/frontend/env.d.ts +7 -0
  84. package/templates/extension/frontend/index.html +12 -0
  85. package/templates/extension/frontend/package.json +30 -0
  86. package/templates/extension/frontend/src/App.vue +51 -0
  87. package/templates/extension/frontend/src/main.ts +8 -0
  88. package/templates/extension/frontend/src/styles/global.css +11 -0
  89. package/templates/extension/frontend/tsconfig.json +25 -0
  90. package/templates/extension/frontend/tsconfig.node.json +11 -0
  91. package/templates/extension/frontend/vite.config.ts +24 -0
@@ -11,7 +11,7 @@
11
11
  :for="name"
12
12
  class="block truncate text-start mr-6"
13
13
  :title="label"
14
- :class="theme === 'dark' ? 'text-white' : 'text-black'"
14
+ :class="[theme === 'dark' ? 'text-white' : 'text-black', disabled ? 'opacity-30' : '']"
15
15
  >
16
16
  {{ label }}
17
17
  </label>
@@ -19,20 +19,20 @@
19
19
  <div v-else />
20
20
  <slot name="insetElement" />
21
21
 
22
- <div
22
+ <BlueTooltip
23
23
  v-if="infoTooltip"
24
- class="relative group inline-flex items-center shrink-0 mr-2 ml-auto"
24
+ :text="infoTooltip"
25
+ :theme="theme"
26
+ class="items-center shrink-0 mr-2 ml-auto"
25
27
  >
26
- <span
27
- class="mdi mdi-information-outline text-[16px] opacity-60 cursor-help"
28
- :class="theme === 'dark' ? 'text-white' : 'text-black'"
28
+ <BlueIcon
29
+ name="mdi-information-outline"
30
+ :size="16"
31
+ :label="infoTooltip"
32
+ class="cursor-help"
33
+ :class="[disabled ? 'opacity-30' : 'opacity-60', theme === 'dark' ? 'text-white' : 'text-black']"
29
34
  />
30
- <div
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 pl-4 text-sm font-medium outline-none"
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
- const props = defineProps<{
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
- <!-- Escape is swallowed unless the wait is dismissible: an operation the user cannot
47
- interrupt must not end up running under a live page. -->
48
- <dialog
49
- ref="dialogRef"
50
- class="bluevue-dialog"
51
- @cancel="onCancel"
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
- <div class="bluevue-panel relative flex w-[320px] flex-col items-center justify-center rounded-lg px-6 pt-7 pb-5">
54
- <button
55
- v-if="dismissible"
56
- type="button"
57
- class="absolute right-3 top-3 cursor-pointer text-[#ffffffaa] transition-colors hover:text-white"
58
- title="Close"
59
- @click="close"
60
- >
61
- <span class="mdi mdi-close text-[20px] leading-none" />
62
- </button>
63
- <img
64
- :src="logo"
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-[5px] 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 { onMounted, ref, watch } from 'vue'
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<HTMLDialogElement | null>(null)
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
- name.value = props.initial ?? ''
42
- notes.value = props.notesInitial ?? ''
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
- <dialog
57
+ <BlueDialog
77
58
  ref="dialogRef"
78
- class="bluevue-dialog"
79
- @close="emit('update:modelValue', false)"
80
- @click="onBackdropClick"
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
- <div class="bluevue-panel w-[624px] max-w-full rounded-lg text-white">
83
- <div class="flex items-start gap-3 px-5 pt-4 pb-3">
84
- <span
85
- class="mdi mt-[2px] shrink-0 text-[22px] leading-none text-[#42A5F5]"
86
- :class="icon || 'mdi-content-save-outline'"
87
- />
88
- <div class="flex-1 min-w-0">
89
- <div class="text-base leading-tight truncate">
90
- {{ title }}
91
- </div>
92
- <div
93
- v-if="subtitle"
94
- class="text-xs text-[#ffffff88] mt-1 leading-relaxed"
95
- >
96
- {{ subtitle }}
97
- </div>
98
- </div>
99
- <button
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
- <div class="flex items-center justify-between gap-3 border-t border-[#ffffff0d] px-5 py-3">
131
- <button
132
- type="button"
133
- class="h-7 shrink-0 cursor-pointer rounded-[4px] bg-[#ffffff11] px-3 text-[13px] font-medium uppercase tracking-wide transition-colors hover:bg-[#ffffff22]"
134
- @click="close"
135
- >
136
- Cancel
137
- </button>
138
- <span class="flex-1 text-center text-xs text-[#ffffff88]">{{ hint }}</span>
139
- <button
140
- type="button"
141
- :disabled="!name.trim()"
142
- class="h-7 shrink-0 rounded-[4px] bg-[var(--bluevue-primary)] px-3 text-[13px] font-medium uppercase tracking-wide transition-colors hover:brightness-125 disabled:cursor-not-allowed disabled:bg-[#9e9e9e80] disabled:text-[#afafaf] disabled:opacity-30 disabled:hover:brightness-100"
143
- :class="name.trim() ? 'cursor-pointer' : ''"
144
- @click="confirm"
145
- >
146
- {{ confirmLabel || 'Save' }}
147
- </button>
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
- <div
21
+ <BlueTooltip
22
22
  v-if="infoTooltip"
23
- class="relative group inline-flex items-center shrink-0 mr-2 ml-auto"
23
+ :text="infoTooltip"
24
+ :theme="theme"
25
+ class="items-center shrink-0 mr-2 ml-auto"
24
26
  >
25
- <span
26
- class="mdi mdi-information-outline text-[16px] opacity-60 cursor-help"
27
- :class="theme === 'dark' ? 'text-white' : 'text-black'"
27
+ <BlueIcon
28
+ name="mdi-information-outline"
29
+ :size="16"
30
+ :label="infoTooltip"
31
+ class="cursor-help"
32
+ :class="[disabled ? 'opacity-30' : 'opacity-60', theme === 'dark' ? 'text-white' : 'text-black']"
28
33
  />
29
- <div
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
- <!-- Shrinks far more eagerly than the label, so a tight row narrows the track first and
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>