@bluerobotics/bluevue 0.1.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/bluevue.js +1176 -0
- package/dist/components/BlueButtonGroup.vue.d.ts +101 -0
- package/dist/components/BlueExpansiblePanel.vue.d.ts +38 -0
- package/dist/components/BlueInput.vue.d.ts +70 -0
- package/dist/components/BlueLoadingDialog.vue.d.ts +27 -0
- package/dist/components/BlueMenu.vue.d.ts +57 -0
- package/dist/components/BluePromptDialog.vue.d.ts +37 -0
- package/dist/components/BlueSelect.vue.d.ts +76 -0
- package/dist/components/BlueSlider.vue.d.ts +51 -0
- package/dist/components/BlueSwitch.vue.d.ts +39 -0
- package/dist/composables/useBluePopover.d.ts +38 -0
- package/dist/index.d.ts +10 -0
- package/dist/style.css +2 -0
- package/package.json +64 -0
- package/src/assets/br-logo-white.svg +1 -0
- package/src/components/BlueButtonGroup.vue +382 -0
- package/src/components/BlueExpansiblePanel.vue +72 -0
- package/src/components/BlueInput.vue +247 -0
- package/src/components/BlueLoadingDialog.vue +76 -0
- package/src/components/BlueMenu.vue +119 -0
- package/src/components/BluePromptDialog.vue +151 -0
- package/src/components/BlueSelect.vue +236 -0
- package/src/components/BlueSlider.vue +464 -0
- package/src/components/BlueSwitch.vue +117 -0
- package/src/composables/useBluePopover.ts +153 -0
- package/src/index.ts +11 -0
- package/src/styles/bluevue.css +124 -0
- package/src/styles/bundled.css +9 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex w-full justify-between items-center">
|
|
3
|
+
<!-- shrink-0 keeps the label at its natural width: letting flex shrink it by the fraction
|
|
4
|
+
of a pixel that rounding introduces is enough for Chromium to ellipsize a label that
|
|
5
|
+
fits. The cap is what makes an outsized label ellipsize instead of eating the row. -->
|
|
6
|
+
<div
|
|
7
|
+
v-if="label"
|
|
8
|
+
class="min-w-0 max-w-[45%] shrink-0"
|
|
9
|
+
>
|
|
10
|
+
<label
|
|
11
|
+
class="block truncate text-start mr-6"
|
|
12
|
+
:title="label"
|
|
13
|
+
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
14
|
+
>{{ label }}</label>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex items-center min-w-0 shrink-[1000]">
|
|
17
|
+
<div
|
|
18
|
+
v-if="infoTooltip"
|
|
19
|
+
class="relative group inline-flex items-center shrink-0 mr-2"
|
|
20
|
+
>
|
|
21
|
+
<span
|
|
22
|
+
class="mdi mdi-information-outline text-[16px] opacity-60 cursor-help"
|
|
23
|
+
:class="theme === 'dark' ? 'text-white' : 'text-black'"
|
|
24
|
+
/>
|
|
25
|
+
<div
|
|
26
|
+
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]"
|
|
27
|
+
>
|
|
28
|
+
{{ infoTooltip }}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<!-- Both labels are laid out rather than drawn over: two equal columns take their width
|
|
32
|
+
from the longer of the two, so the track grows to whatever it has to say and the knob
|
|
33
|
+
covering either side has the same room. -->
|
|
34
|
+
<div
|
|
35
|
+
name="switch-track"
|
|
36
|
+
class="relative grid grid-cols-2 rounded-[8px] bluevue-elevation-1 cursor-pointer overflow-hidden"
|
|
37
|
+
:class="[theme === 'dark' ? 'bg-[#464646AA]' : 'bg-[#00000011]', disabled ? 'opacity-30 cursor-not-allowed' : '']"
|
|
38
|
+
:style="{ minWidth: width || '75px', height: height || '30px' }"
|
|
39
|
+
@click="toggleSwitch"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
class="absolute top-[4px] bottom-[4px] w-[calc(50%-4px)] rounded-[8px] bluevue-elevation-1 transition-all duration-300"
|
|
43
|
+
:style="{
|
|
44
|
+
left: modelValue ? 'calc(50% + 2px)' : '2px',
|
|
45
|
+
backgroundColor: modelValue ? color || 'var(--bluevue-primary)' : '#777777',
|
|
46
|
+
}"
|
|
47
|
+
/>
|
|
48
|
+
<!-- 6px, of which the knob's 2px inset takes the first two: what is left is the 4px the
|
|
49
|
+
label keeps from the knob's edge. -->
|
|
50
|
+
<span
|
|
51
|
+
class="relative flex items-center justify-center px-[6px] text-[14px] whitespace-nowrap pointer-events-none transition-opacity duration-300"
|
|
52
|
+
:class="labelClass(!modelValue)"
|
|
53
|
+
>
|
|
54
|
+
{{ labelOff || 'Off' }}
|
|
55
|
+
</span>
|
|
56
|
+
<span
|
|
57
|
+
class="relative flex items-center justify-center px-[6px] text-[14px] whitespace-nowrap pointer-events-none transition-opacity duration-300"
|
|
58
|
+
:class="labelClass(modelValue)"
|
|
59
|
+
>
|
|
60
|
+
{{ labelOn || 'On' }}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
import { ref, watch } from 'vue'
|
|
69
|
+
|
|
70
|
+
const props = defineProps<{
|
|
71
|
+
/** Color of the switch's knob when it is on. */
|
|
72
|
+
color?: string
|
|
73
|
+
/** The current value of the switch. */
|
|
74
|
+
modelValue: boolean | null
|
|
75
|
+
/** Whether the switch is disabled. */
|
|
76
|
+
disabled?: boolean
|
|
77
|
+
/** Height of the switch container. */
|
|
78
|
+
height?: string
|
|
79
|
+
/** Label on the left side of the component. */
|
|
80
|
+
label?: string
|
|
81
|
+
/** Optional info tooltip shown via an info icon next to the control. */
|
|
82
|
+
infoTooltip?: string
|
|
83
|
+
/** Custom text for the switch when it is on. */
|
|
84
|
+
labelOn?: string
|
|
85
|
+
/** Custom text for the switch when it is off. */
|
|
86
|
+
labelOff?: string
|
|
87
|
+
/** Name of the component's container. */
|
|
88
|
+
name: string
|
|
89
|
+
/** Theme of the component. */
|
|
90
|
+
theme?: 'light' | 'dark'
|
|
91
|
+
/** Minimum width of the container. */
|
|
92
|
+
width?: string
|
|
93
|
+
}>()
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits<{
|
|
96
|
+
(e: 'update:modelValue', value: boolean): void
|
|
97
|
+
}>()
|
|
98
|
+
|
|
99
|
+
const modelValue = ref(props.modelValue || false)
|
|
100
|
+
|
|
101
|
+
// The label the knob sits under is white against its fill; the other one stays as a dim
|
|
102
|
+
// reminder of what the far position says.
|
|
103
|
+
const labelClass = (active: boolean): string[] =>
|
|
104
|
+
active ? ['text-white'] : ['opacity-20', props.theme === 'dark' ? 'text-white' : 'text-black']
|
|
105
|
+
|
|
106
|
+
const toggleSwitch = (): void => {
|
|
107
|
+
if (props.disabled) return
|
|
108
|
+
modelValue.value = !modelValue.value
|
|
109
|
+
emit('update:modelValue', modelValue.value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
watch(
|
|
113
|
+
() => props.modelValue,
|
|
114
|
+
(v) => (modelValue.value = v ?? false),
|
|
115
|
+
{ immediate: true }
|
|
116
|
+
)
|
|
117
|
+
</script>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { autoUpdate, flip, offset, type Placement, shift, size, useFloating, type VirtualElement } from '@floating-ui/vue'
|
|
2
|
+
import { computed, type CSSProperties, nextTick, ref, type Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface BluePopoverOptions {
|
|
5
|
+
/** Where the menu hangs off its anchor (default 'bottom-end'). */
|
|
6
|
+
placement?: Placement
|
|
7
|
+
/** Give the menu at least its anchor's width, for a select whose list should line up with it. */
|
|
8
|
+
matchAnchorWidth?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BluePopover {
|
|
12
|
+
/** Goes on the element the menu hangs off, when it opens from a control rather than a pointer. */
|
|
13
|
+
anchorRef: Ref<HTMLElement | null>
|
|
14
|
+
/** Goes on the element carrying the `popover` attribute. */
|
|
15
|
+
popoverRef: Ref<HTMLElement | null>
|
|
16
|
+
/** The popover's id, for the `popovertarget` the activator is bound to. */
|
|
17
|
+
popoverId: string
|
|
18
|
+
/** Bind onto an activator `<button>` to let the browser own the open and close. */
|
|
19
|
+
activatorProps: { popovertarget: string }
|
|
20
|
+
isOpen: Ref<boolean>
|
|
21
|
+
/** False until the menu has been placed, so it can be held invisible for that first frame. */
|
|
22
|
+
isPositioned: Readonly<Ref<boolean>>
|
|
23
|
+
floatingStyles: Readonly<Ref<CSSProperties>>
|
|
24
|
+
/** Opens the menu, at a viewport position when one is given instead of an anchor element. */
|
|
25
|
+
show: (pointerTarget?: [number, number]) => Promise<void>
|
|
26
|
+
hide: () => void
|
|
27
|
+
/** Bind to the popover's `toggle` event, so a dismissal by the browser is reflected back. */
|
|
28
|
+
onToggle: (event: Event) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let popoverCount = 0
|
|
32
|
+
|
|
33
|
+
// The browser dismisses an auto popover on the pointerup whose pointerdown landed before the
|
|
34
|
+
// popover existed, which is every menu summoned mid-gesture: a right-click, a long press. Knowing
|
|
35
|
+
// a button is still held is what lets such a menu wait for the release instead of being closed by
|
|
36
|
+
// it. Tracked once for all popovers, and only from the first one that is created, so importing
|
|
37
|
+
// the composable touches no document.
|
|
38
|
+
let pointerIsDown = false
|
|
39
|
+
let pointerTracked = false
|
|
40
|
+
|
|
41
|
+
const trackPointerState = (): void => {
|
|
42
|
+
if (pointerTracked || typeof document === 'undefined') return
|
|
43
|
+
pointerTracked = true
|
|
44
|
+
const release = (): void => { pointerIsDown = false }
|
|
45
|
+
document.addEventListener('pointerdown', () => { pointerIsDown = true }, true)
|
|
46
|
+
document.addEventListener('pointerup', release, true)
|
|
47
|
+
document.addEventListener('pointercancel', release, true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const afterPointerRelease = (): Promise<void> =>
|
|
51
|
+
new Promise((resolve) => {
|
|
52
|
+
const done = (): void => {
|
|
53
|
+
document.removeEventListener('pointerup', done)
|
|
54
|
+
document.removeEventListener('pointercancel', done)
|
|
55
|
+
// Resolved a task later, so the browser has already had its turn at dismissing on this
|
|
56
|
+
// release and the menu opens into the quiet after it.
|
|
57
|
+
setTimeout(resolve)
|
|
58
|
+
}
|
|
59
|
+
document.addEventListener('pointerup', done)
|
|
60
|
+
document.addEventListener('pointercancel', done)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A menu that opens in the top layer, so it is never clipped by the panel it belongs to and
|
|
65
|
+
* needs no z-index of its own. The native popover dismisses it on outside click and on Escape;
|
|
66
|
+
* floating-ui keeps it beside its anchor and inside the viewport.
|
|
67
|
+
* @param {BluePopoverOptions} options Placement, and whether to match the anchor's width.
|
|
68
|
+
* @returns {BluePopover} Refs to bind, open state, and imperative open and close.
|
|
69
|
+
*/
|
|
70
|
+
export function useBluePopover(options: BluePopoverOptions = {}): BluePopover {
|
|
71
|
+
trackPointerState()
|
|
72
|
+
|
|
73
|
+
const anchorRef = ref<HTMLElement | null>(null)
|
|
74
|
+
const popoverRef = ref<HTMLElement | null>(null)
|
|
75
|
+
const isOpen = ref(false)
|
|
76
|
+
const popoverId = `bluevue-popover-${++popoverCount}`
|
|
77
|
+
|
|
78
|
+
// Set only when the menu is opened at a pointer position, as a right-click or long-press does.
|
|
79
|
+
const pointerTarget = ref<[number, number] | null>(null)
|
|
80
|
+
|
|
81
|
+
const virtualAnchor = computed<VirtualElement | null>(() => {
|
|
82
|
+
if (!pointerTarget.value) return null
|
|
83
|
+
const [x, y] = pointerTarget.value
|
|
84
|
+
return { getBoundingClientRect: () => new DOMRect(x, y, 0, 0) }
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Null while closed, so scroll and resize are only observed for a menu that is on screen.
|
|
88
|
+
const reference = computed(() => (isOpen.value ? virtualAnchor.value ?? anchorRef.value : null))
|
|
89
|
+
|
|
90
|
+
const { floatingStyles, isPositioned } = useFloating(reference, popoverRef, {
|
|
91
|
+
// Stated so isPositioned drops back to false on close, and a second open is held invisible
|
|
92
|
+
// until it has been placed rather than showing for a frame at wherever it last was.
|
|
93
|
+
open: isOpen,
|
|
94
|
+
placement: options.placement ?? 'bottom-end',
|
|
95
|
+
strategy: 'fixed',
|
|
96
|
+
whileElementsMounted: autoUpdate,
|
|
97
|
+
middleware: [
|
|
98
|
+
offset(4),
|
|
99
|
+
flip({ padding: 8 }),
|
|
100
|
+
shift({ padding: 8 }),
|
|
101
|
+
...(options.matchAnchorWidth
|
|
102
|
+
? [size({
|
|
103
|
+
apply: ({ rects, elements }) => {
|
|
104
|
+
elements.floating.style.minWidth = `${rects.reference.width}px`
|
|
105
|
+
},
|
|
106
|
+
})]
|
|
107
|
+
: []),
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Bumped by every open and close, so an open that waited out a pointer release can tell it is
|
|
112
|
+
// still the one wanted and does not arrive after something already asked for the opposite.
|
|
113
|
+
let request = 0
|
|
114
|
+
|
|
115
|
+
const show = async (target?: [number, number]): Promise<void> => {
|
|
116
|
+
const ticket = ++request
|
|
117
|
+
pointerTarget.value = target ?? null
|
|
118
|
+
await nextTick()
|
|
119
|
+
if (pointerIsDown) await afterPointerRelease()
|
|
120
|
+
if (ticket !== request) return
|
|
121
|
+
const el = popoverRef.value
|
|
122
|
+
if (!el || el.matches(':popover-open')) return
|
|
123
|
+
// Opening it is what sets isOpen, through the toggle event the browser fires.
|
|
124
|
+
el.showPopover()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hide = (): void => {
|
|
128
|
+
request += 1
|
|
129
|
+
const el = popoverRef.value
|
|
130
|
+
if (el?.matches(':popover-open')) el.hidePopover()
|
|
131
|
+
else isOpen.value = false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const onToggle = (event: Event): void => {
|
|
135
|
+
isOpen.value = (event as ToggleEvent).newState === 'open'
|
|
136
|
+
// A menu placed at a pointer keeps its target only while it is up, so the next open off an
|
|
137
|
+
// activator is not still anchored to the last right-click.
|
|
138
|
+
if (!isOpen.value) pointerTarget.value = null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
anchorRef,
|
|
143
|
+
popoverRef,
|
|
144
|
+
popoverId,
|
|
145
|
+
activatorProps: { popovertarget: popoverId },
|
|
146
|
+
isOpen,
|
|
147
|
+
isPositioned,
|
|
148
|
+
floatingStyles,
|
|
149
|
+
show,
|
|
150
|
+
hide,
|
|
151
|
+
onToggle,
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as BlueButtonGroup } from './components/BlueButtonGroup.vue'
|
|
2
|
+
export { default as BlueExpansiblePanel } from './components/BlueExpansiblePanel.vue'
|
|
3
|
+
export { default as BlueInput } from './components/BlueInput.vue'
|
|
4
|
+
export { default as BlueLoadingDialog } from './components/BlueLoadingDialog.vue'
|
|
5
|
+
export { default as BlueMenu, type BlueMenuItem } from './components/BlueMenu.vue'
|
|
6
|
+
export { default as BluePromptDialog } from './components/BluePromptDialog.vue'
|
|
7
|
+
export { default as BlueSelect } from './components/BlueSelect.vue'
|
|
8
|
+
export { default as BlueSlider } from './components/BlueSlider.vue'
|
|
9
|
+
export { default as BlueSwitch } from './components/BlueSwitch.vue'
|
|
10
|
+
|
|
11
|
+
export { useBluePopover } from './composables/useBluePopover'
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* Tokens and component classes: everything the controls need that is not a Tailwind utility.
|
|
2
|
+
Import this on its own when your app compiles the utilities itself (see the README), or get
|
|
3
|
+
it folded into dist/style.css when it does not. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--bluevue-primary: #0B5087;
|
|
7
|
+
--bluevue-error: #CF6679;
|
|
8
|
+
--bluevue-surface: #363636;
|
|
9
|
+
--bluevue-accent: #6699CC;
|
|
10
|
+
--bluevue-panel-bg: rgba(30, 30, 30, 0.96);
|
|
11
|
+
--bluevue-hairline: rgba(255, 255, 255, 0.08);
|
|
12
|
+
/* Material's three-part shadow, at the two depths the controls use: a resting control, and
|
|
13
|
+
one raised above its neighbours (a selected segment, an open menu). */
|
|
14
|
+
--bluevue-elevation-1: 0 2px 1px -1px #0003, 0 1px 1px 0 #00000024, 0 1px 3px 0 #0000001f;
|
|
15
|
+
--bluevue-elevation-5: 0 3px 5px -1px #0003, 0 5px 8px 0 #00000024, 0 1px 14px 0 #0000001f;
|
|
16
|
+
/* The opposite of an elevation: a well cut into the surface, for somewhere a value is typed
|
|
17
|
+
rather than somewhere that is pressed. The light line along the bottom edge is what sells
|
|
18
|
+
the depth, by reading as the lit far wall of the recess. */
|
|
19
|
+
--bluevue-inset-1: inset 0 1px 2px #00000056, inset 0 0.5px 0.5px #0000003a, inset 0 -0.5px 0 #ffffff0b;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bluevue-elevation-1 {
|
|
23
|
+
box-shadow: var(--bluevue-elevation-1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.bluevue-elevation-5 {
|
|
27
|
+
box-shadow: var(--bluevue-elevation-5);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bluevue-inset-1 {
|
|
31
|
+
box-shadow: var(--bluevue-inset-1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* The surface the dialogs are built on: a frosted near-black card with a hairline edge. */
|
|
35
|
+
.bluevue-panel {
|
|
36
|
+
background-color: var(--bluevue-panel-bg);
|
|
37
|
+
backdrop-filter: blur(8px);
|
|
38
|
+
-webkit-backdrop-filter: blur(8px);
|
|
39
|
+
border: 1px solid var(--bluevue-hairline);
|
|
40
|
+
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2), 0 8px 12px 6px rgba(0, 0, 0, 0.15);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* A modal <dialog> carries the browser's own centring, borders and padding. Strip them so the
|
|
44
|
+
panel inside is the whole of what shows, and dim the page behind it. */
|
|
45
|
+
.bluevue-dialog {
|
|
46
|
+
margin: auto;
|
|
47
|
+
border: none;
|
|
48
|
+
padding: 0;
|
|
49
|
+
background: transparent;
|
|
50
|
+
max-width: min(92vw, 624px);
|
|
51
|
+
max-height: 92vh;
|
|
52
|
+
overflow: visible;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* showModal() moves focus into the dialog, and with nothing focusable inside it the dialog
|
|
56
|
+
itself takes it and the browser rings the whole panel. The ring belongs on controls, not on
|
|
57
|
+
the surface holding them, and the controls inside keep their own. */
|
|
58
|
+
.bluevue-dialog:focus,
|
|
59
|
+
.bluevue-dialog:focus-visible,
|
|
60
|
+
.bluevue-popover:focus,
|
|
61
|
+
.bluevue-popover:focus-visible {
|
|
62
|
+
outline: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.bluevue-dialog::backdrop {
|
|
66
|
+
background: rgba(0, 0, 0, 0.88);
|
|
67
|
+
backdrop-filter: blur(8px);
|
|
68
|
+
-webkit-backdrop-filter: blur(8px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* A popover is promoted to the top layer, so it needs no z-index and is not clipped by the
|
|
72
|
+
panel it opens from. Only the browser's default centring and chrome have to go, since
|
|
73
|
+
floating-ui writes the real position. */
|
|
74
|
+
.bluevue-popover {
|
|
75
|
+
position: fixed;
|
|
76
|
+
margin: 0;
|
|
77
|
+
border: none;
|
|
78
|
+
padding: 0;
|
|
79
|
+
background: transparent;
|
|
80
|
+
overflow: visible;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* BlueExpansiblePanel's open and close. */
|
|
84
|
+
.bluevue-expand-enter-active,
|
|
85
|
+
.bluevue-expand-leave-active {
|
|
86
|
+
transition: max-height 0.3s ease;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.bluevue-expand-enter-from,
|
|
91
|
+
.bluevue-expand-leave-to {
|
|
92
|
+
max-height: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.bluevue-expand-enter-to,
|
|
96
|
+
.bluevue-expand-leave-from {
|
|
97
|
+
max-height: 500px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* BlueLoadingDialog's mark is a propeller, so it turns like one: a creep of 80 degrees over two
|
|
101
|
+
seconds, then the balance of four turns over the next two, carrying it round to where it
|
|
102
|
+
started. */
|
|
103
|
+
.bluevue-loading__icon {
|
|
104
|
+
animation: bluevue-prop 4s infinite;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes bluevue-prop {
|
|
108
|
+
/* Each segment carries its own easing, which is what gives the blade its weight: it takes a
|
|
109
|
+
moment to get moving, settles at the end of the creep, then winds up into the spins and
|
|
110
|
+
bleeds the speed off again rather than stopping dead. The cycle ends on a whole turn, so
|
|
111
|
+
the loop restarts from the orientation it began at and the seam is invisible. */
|
|
112
|
+
0% {
|
|
113
|
+
transform: rotate(0deg);
|
|
114
|
+
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
|
115
|
+
}
|
|
116
|
+
/* 80deg at the halfway mark, leaving the other two seconds for the 1360deg that follow. */
|
|
117
|
+
50% {
|
|
118
|
+
transform: rotate(80deg);
|
|
119
|
+
animation-timing-function: cubic-bezier(0.35, 0, 0.45, 1);
|
|
120
|
+
}
|
|
121
|
+
100% {
|
|
122
|
+
transform: rotate(1440deg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Source for the ready-made dist/style.css, for apps that do not run Tailwind themselves.
|
|
2
|
+
Theme and utilities only: Preflight is deliberately left out, because a component library
|
|
3
|
+
has no business resetting the base styles of the app that imports it. */
|
|
4
|
+
@import "tailwindcss/theme.css" layer(theme);
|
|
5
|
+
@import "tailwindcss/utilities.css" layer(utilities);
|
|
6
|
+
|
|
7
|
+
@source "../components";
|
|
8
|
+
|
|
9
|
+
@import "./bluevue.css";
|