@bagelink/vue 1.15.57 → 1.15.61
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/dist/components/Avatar.vue.d.ts +5 -1
- package/dist/components/Avatar.vue.d.ts.map +1 -1
- package/dist/components/Badge.vue.d.ts +6 -1
- package/dist/components/Badge.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ListItem.vue.d.ts +12 -0
- package/dist/components/ListItem.vue.d.ts.map +1 -1
- package/dist/components/Progress.vue.d.ts +38 -0
- package/dist/components/Progress.vue.d.ts.map +1 -0
- package/dist/components/Swiper.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts +1 -0
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/components/layout/Divider.vue.d.ts +31 -0
- package/dist/components/layout/Divider.vue.d.ts.map +1 -0
- package/dist/components/layout/Layout.vue.d.ts.map +1 -1
- package/dist/components/layout/SidebarNavItem.vue.d.ts +18 -0
- package/dist/components/layout/SidebarNavItem.vue.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useTheme.d.ts +9 -0
- package/dist/composables/useTheme.d.ts.map +1 -1
- package/dist/directives/index.d.ts +2 -0
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/reveal.d.ts +29 -0
- package/dist/directives/reveal.d.ts.map +1 -0
- package/dist/index.cjs +37 -37
- package/dist/index.mjs +8379 -8076
- package/dist/plugins/bagel.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Avatar.vue +20 -4
- package/src/components/Badge.vue +32 -2
- package/src/components/Dropdown.vue +7 -1
- package/src/components/ListItem.vue +151 -76
- package/src/components/Progress.vue +80 -0
- package/src/components/Swiper.vue +19 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppSidebar.vue +11 -15
- package/src/components/layout/Divider.vue +64 -0
- package/src/components/layout/Layout.vue +17 -4
- package/src/components/layout/SidebarNavItem.vue +186 -0
- package/src/components/layout/index.ts +1 -0
- package/src/composables/index.ts +1 -1
- package/src/composables/useTheme.ts +31 -5
- package/src/directives/index.ts +2 -0
- package/src/directives/reveal.ts +78 -0
- package/src/plugins/bagel.ts +2 -1
- package/src/styles/appearance.css +11 -0
- package/src/styles/bagel.css +1 -0
- package/src/styles/buttons.css +74 -0
- package/src/styles/dark.css +94 -2
- package/src/styles/layout.css +65 -13
- package/src/styles/motion.css +91 -0
- package/src/utils/index.ts +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
defineOptions({ name: 'BglDivider' })
|
|
3
|
+
import { useSlots } from 'vue'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hairline separator in the theme border color.
|
|
7
|
+
* - Horizontal by default; `vertical` for inline groups (toolbars, breadcrumbs).
|
|
8
|
+
* - Default slot (or `label`) renders centered text with a line on each side
|
|
9
|
+
* (horizontal only) — e.g. an "OR" divider.
|
|
10
|
+
* - `size` sets the length of a vertical divider (maps to --divider-size).
|
|
11
|
+
*/
|
|
12
|
+
const {
|
|
13
|
+
vertical = false,
|
|
14
|
+
label = '',
|
|
15
|
+
size,
|
|
16
|
+
class: className = '',
|
|
17
|
+
} = defineProps<{
|
|
18
|
+
vertical?: boolean
|
|
19
|
+
label?: string
|
|
20
|
+
size?: string
|
|
21
|
+
class?: string
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const slots = useSlots()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<!-- vertical: short hairline, no label support -->
|
|
29
|
+
<div
|
|
30
|
+
v-if="vertical"
|
|
31
|
+
class="divider-v"
|
|
32
|
+
:class="className"
|
|
33
|
+
:style="size ? { '--divider-size': size } : undefined"
|
|
34
|
+
role="separator"
|
|
35
|
+
aria-orientation="vertical"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<!-- horizontal with label/slot: line — content — line -->
|
|
39
|
+
<div v-else-if="label || slots.default" class="bgl-divider-labeled" :class="className" role="separator">
|
|
40
|
+
<span class="divider bgl-divider-line" />
|
|
41
|
+
<span class="bgl-divider-label"><slot>{{ label }}</slot></span>
|
|
42
|
+
<span class="divider bgl-divider-line" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- plain horizontal hairline -->
|
|
46
|
+
<div v-else class="divider" :class="className" role="separator" />
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
.bgl-divider-labeled {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 0.75rem;
|
|
54
|
+
}
|
|
55
|
+
.bgl-divider-line {
|
|
56
|
+
flex: 1;
|
|
57
|
+
}
|
|
58
|
+
.bgl-divider-label {
|
|
59
|
+
font-size: 0.8125rem;
|
|
60
|
+
color: var(--bgl-text-soft, var(--bgl-gray));
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -22,19 +22,32 @@ const props = withDefaults(defineProps<LayoutProrps>(), {
|
|
|
22
22
|
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// Bare `fr` tracks won't shrink below their content's intrinsic size, which
|
|
26
|
+
// causes overflow when a child (table, long text, nested grid) is wider than its
|
|
27
|
+
// share. Wrapping in `minmax(0, …)` makes the track shrinkable — the behaviour
|
|
28
|
+
// you almost always want. Authors can opt out by passing an explicit minmax().
|
|
29
|
+
function normalizeTrack(track: string): string {
|
|
30
|
+
const t = track.trim()
|
|
31
|
+
if (/^\d*\.?\d+fr$/.test(t)) { return `minmax(0, ${t})` }
|
|
32
|
+
return t
|
|
33
|
+
}
|
|
34
|
+
function buildTemplate(tracks: string[]): string {
|
|
35
|
+
return tracks.length > 0 ? tracks.map(normalizeTrack).join(' ') : 'auto'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const gridTemplateRows = computed(() => buildTemplate(props.rows))
|
|
26
39
|
const gapSize = computed(() => `${props.gap}rem`)
|
|
27
40
|
const mGapSize = computed(() => props.mGap !== undefined ? `${props.mGap}rem` : gapSize.value)
|
|
28
41
|
|
|
29
42
|
const mGridTemplateRows = computed(() => {
|
|
30
|
-
if (props.mRows?.length) { return props.mRows
|
|
43
|
+
if (props.mRows?.length) { return buildTemplate(props.mRows) }
|
|
31
44
|
return gridTemplateRows.value
|
|
32
45
|
})
|
|
33
46
|
|
|
34
|
-
const gridTemplateColumns = computed(() => (props.columns
|
|
47
|
+
const gridTemplateColumns = computed(() => buildTemplate(props.columns))
|
|
35
48
|
|
|
36
49
|
const mGridTemplateColumns = computed(() => {
|
|
37
|
-
if (props.mColumns?.length) { return props.mColumns
|
|
50
|
+
if (props.mColumns?.length) { return buildTemplate(props.mColumns) }
|
|
38
51
|
return gridTemplateColumns.value
|
|
39
52
|
})
|
|
40
53
|
</script>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { NavLink } from '@bagelink/vue'
|
|
3
|
+
import { Btn, Icon, Dropdown } from '@bagelink/vue'
|
|
4
|
+
import { computed, ref, watch } from 'vue'
|
|
5
|
+
import { useRoute } from 'vue-router'
|
|
6
|
+
import { resolveI18n } from '../../i18n'
|
|
7
|
+
|
|
8
|
+
interface LinkWithAction extends NavLink {
|
|
9
|
+
activeRoutes?: string[]
|
|
10
|
+
children?: LinkWithAction[]
|
|
11
|
+
action?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<{
|
|
15
|
+
link: LinkWithAction
|
|
16
|
+
/** sidebar is expanded (true) or collapsed to icons (false) */
|
|
17
|
+
open: boolean
|
|
18
|
+
isMobile: boolean
|
|
19
|
+
bgColor: string
|
|
20
|
+
textColor: string
|
|
21
|
+
activeColor: string
|
|
22
|
+
}>(), {})
|
|
23
|
+
|
|
24
|
+
const route = useRoute()
|
|
25
|
+
|
|
26
|
+
const hasChildren = computed(() => !!props.link.children?.length)
|
|
27
|
+
|
|
28
|
+
// active detection (mirrors AppSidebar logic, recursive for parents)
|
|
29
|
+
function linkActive(link: LinkWithAction): boolean {
|
|
30
|
+
const linkPath = link.to
|
|
31
|
+
if (link.activeRoutes?.length) {
|
|
32
|
+
return link.activeRoutes.some(p =>
|
|
33
|
+
p === '/' ? route.path === p : route.path === p || route.path.startsWith(`${p}/`))
|
|
34
|
+
}
|
|
35
|
+
if (!linkPath) return false
|
|
36
|
+
if (linkPath === '/') return route.path === linkPath
|
|
37
|
+
return route.path === linkPath || route.path.startsWith(`${linkPath}/`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const childActive = computed(() => props.link.children?.some(linkActive) ?? false)
|
|
41
|
+
const selfActive = computed(() => linkActive(props.link))
|
|
42
|
+
const isActive = computed(() => selfActive.value || (hasChildren.value && childActive.value))
|
|
43
|
+
|
|
44
|
+
// expanded/collapsed group state — auto-open when a child is active
|
|
45
|
+
const expanded = ref(childActive.value)
|
|
46
|
+
watch(childActive, v => { if (v) expanded.value = true })
|
|
47
|
+
|
|
48
|
+
function onParentClick() {
|
|
49
|
+
if (hasChildren.value) expanded.value = !expanded.value
|
|
50
|
+
else props.link.action?.()
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<!-- LEAF link -->
|
|
56
|
+
<Btn
|
|
57
|
+
v-if="!hasChildren"
|
|
58
|
+
:title="!open && !isMobile ? resolveI18n(link.label) : ''"
|
|
59
|
+
fullWidth alignTxt="start" class="flex-shrink-0 px-075"
|
|
60
|
+
:class="{ 'nav-btn-active': selfActive }"
|
|
61
|
+
:style="{
|
|
62
|
+
backgroundColor: selfActive ? activeColor : bgColor,
|
|
63
|
+
color: selfActive ? 'white' : textColor,
|
|
64
|
+
}"
|
|
65
|
+
:to="link.to || '/'" @click="link.action"
|
|
66
|
+
>
|
|
67
|
+
<Icon :name="link.icon" size="1.2" />
|
|
68
|
+
<span class="nav-text">{{ resolveI18n(link.label) }}</span>
|
|
69
|
+
</Btn>
|
|
70
|
+
|
|
71
|
+
<!-- PARENT with children, EXPANDED sidebar -->
|
|
72
|
+
<div v-else-if="open || isMobile" class="sidebar-group w100p">
|
|
73
|
+
<Btn
|
|
74
|
+
fullWidth alignTxt="start" class="flex-shrink-0 px-075 sidebar-group-toggle"
|
|
75
|
+
:class="{ 'nav-btn-active': isActive }"
|
|
76
|
+
:style="{
|
|
77
|
+
backgroundColor: isActive ? activeColor : bgColor,
|
|
78
|
+
color: isActive ? 'white' : textColor,
|
|
79
|
+
}"
|
|
80
|
+
@click="onParentClick"
|
|
81
|
+
>
|
|
82
|
+
<Icon :name="link.icon" size="1.2" />
|
|
83
|
+
<span class="nav-text flex-grow">{{ resolveI18n(link.label) }}</span>
|
|
84
|
+
<Icon name="expand_more" size="1.1" class="nav-text sidebar-chevron" :class="{ 'sidebar-chevron-open': expanded }" />
|
|
85
|
+
</Btn>
|
|
86
|
+
<div class="sidebar-children" :class="{ 'sidebar-children-open': expanded }">
|
|
87
|
+
<Btn
|
|
88
|
+
v-for="child in link.children" :key="child.to || child.label"
|
|
89
|
+
fullWidth alignTxt="start" class="flex-shrink-0 ps-2 pe-075 sidebar-child"
|
|
90
|
+
:class="{ 'nav-btn-active': linkActive(child) }"
|
|
91
|
+
:style="{
|
|
92
|
+
backgroundColor: linkActive(child) ? activeColor : 'transparent',
|
|
93
|
+
color: linkActive(child) ? 'white' : textColor,
|
|
94
|
+
}"
|
|
95
|
+
:to="child.to || '/'" @click="child.action"
|
|
96
|
+
>
|
|
97
|
+
<Icon v-if="child.icon" :name="child.icon" size="1.05" class="opacity-7" />
|
|
98
|
+
<span class="nav-text">{{ resolveI18n(child.label) }}</span>
|
|
99
|
+
</Btn>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- PARENT with children, COLLAPSED sidebar → hover popover flyout -->
|
|
104
|
+
<Dropdown
|
|
105
|
+
v-else
|
|
106
|
+
placement="right-start"
|
|
107
|
+
:triggers="['hover']"
|
|
108
|
+
card
|
|
109
|
+
class="w100p sidebar-flyout-trigger"
|
|
110
|
+
>
|
|
111
|
+
<template #trigger>
|
|
112
|
+
<Btn
|
|
113
|
+
fullWidth alignTxt="start" class="flex-shrink-0 px-075"
|
|
114
|
+
:class="{ 'nav-btn-active': isActive }"
|
|
115
|
+
:style="{
|
|
116
|
+
backgroundColor: isActive ? activeColor : bgColor,
|
|
117
|
+
color: isActive ? 'white' : textColor,
|
|
118
|
+
}"
|
|
119
|
+
>
|
|
120
|
+
<Icon :name="link.icon" size="1.2" />
|
|
121
|
+
<span class="nav-text">{{ resolveI18n(link.label) }}</span>
|
|
122
|
+
</Btn>
|
|
123
|
+
</template>
|
|
124
|
+
<!-- flyout content -->
|
|
125
|
+
<div class="sidebar-flyout p-05">
|
|
126
|
+
<p class="sidebar-flyout-label">{{ resolveI18n(link.label) }}</p>
|
|
127
|
+
<Btn
|
|
128
|
+
v-for="child in link.children" :key="child.to || child.label"
|
|
129
|
+
fullWidth alignTxt="start" thin class="flex-shrink-0 px-075 sidebar-flyout-item"
|
|
130
|
+
:class="{ 'nav-btn-active': linkActive(child) }"
|
|
131
|
+
:style="{
|
|
132
|
+
backgroundColor: linkActive(child) ? activeColor : 'transparent',
|
|
133
|
+
color: linkActive(child) ? 'white' : 'var(--bgl-text-color)',
|
|
134
|
+
}"
|
|
135
|
+
:to="child.to || '/'" @click="child.action"
|
|
136
|
+
>
|
|
137
|
+
<Icon v-if="child.icon" :name="child.icon" size="1.05" class="opacity-7" />
|
|
138
|
+
<span>{{ resolveI18n(child.label) }}</span>
|
|
139
|
+
</Btn>
|
|
140
|
+
</div>
|
|
141
|
+
</Dropdown>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.sidebar-chevron {
|
|
146
|
+
margin-inline-start: auto;
|
|
147
|
+
transition: transform 0.2s ease;
|
|
148
|
+
opacity: 0.6;
|
|
149
|
+
}
|
|
150
|
+
.sidebar-chevron-open {
|
|
151
|
+
transform: rotate(180deg);
|
|
152
|
+
}
|
|
153
|
+
.sidebar-children {
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-rows: 0fr;
|
|
156
|
+
transition: grid-template-rows 0.25s ease;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
.sidebar-children-open {
|
|
160
|
+
grid-template-rows: 1fr;
|
|
161
|
+
}
|
|
162
|
+
.sidebar-children > * {
|
|
163
|
+
min-height: 0;
|
|
164
|
+
}
|
|
165
|
+
.sidebar-child {
|
|
166
|
+
font-size: 0.85rem;
|
|
167
|
+
}
|
|
168
|
+
.sidebar-flyout {
|
|
169
|
+
min-width: 180px;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 2px;
|
|
173
|
+
}
|
|
174
|
+
.sidebar-flyout-label {
|
|
175
|
+
margin: 0;
|
|
176
|
+
padding: 0.25rem 0.5rem 0.375rem;
|
|
177
|
+
font-size: 0.6875rem;
|
|
178
|
+
font-weight: 600;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.05em;
|
|
181
|
+
opacity: 0.45;
|
|
182
|
+
}
|
|
183
|
+
.sidebar-flyout-item {
|
|
184
|
+
border-radius: var(--bgl-btn-border-radius);
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
@@ -4,6 +4,7 @@ export { default as AppSidebar } from './AppSidebar.vue'
|
|
|
4
4
|
export { useAppLayout } from './appLayoutContext'
|
|
5
5
|
export type { AppLayoutContext } from './appLayoutContext'
|
|
6
6
|
export { default as BottomMenu } from './BottomMenu.vue'
|
|
7
|
+
export { default as Divider } from './Divider.vue'
|
|
7
8
|
export { default as Layout } from './Layout.vue'
|
|
8
9
|
export { default as Panel } from './Panel.vue'
|
|
9
10
|
export { default as Resizable } from './Resizable.vue'
|
package/src/composables/index.ts
CHANGED
|
@@ -36,5 +36,5 @@ export function useBglSchema<T = { [key: string]: unknown }>(
|
|
|
36
36
|
return getFallbackSchema(data, _columns.value)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export { useTheme } from './useTheme'
|
|
39
|
+
export { configureTheme, useTheme } from './useTheme'
|
|
40
40
|
export { useValidateFieldValue } from './useValidateFieldValue'
|
|
@@ -37,10 +37,18 @@ const STORAGE_KEY = 'color-mode'
|
|
|
37
37
|
const colorMode = ref<string>('system')
|
|
38
38
|
const isDark = ref(false)
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Whether dark mode is enabled for this app. Defaults to `false` (light only),
|
|
42
|
+
* so dark mode is opt-in per project. Call `configureTheme({ allowDark: true })`
|
|
43
|
+
* once at app startup to enable it. When `false`, any dark/system-dark
|
|
44
|
+
* preference falls back to light and `bgl-dark-mode` is never applied.
|
|
45
|
+
*/
|
|
46
|
+
const allowDark = ref(false)
|
|
47
|
+
|
|
40
48
|
const isBrowser = typeof window !== 'undefined'
|
|
41
49
|
|
|
42
50
|
function getSystemPrefersDark() {
|
|
43
|
-
return isBrowser && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
51
|
+
return allowDark.value && isBrowser && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
function findTheme(value: string) {
|
|
@@ -69,11 +77,15 @@ function applyTheme(themeValue: string) {
|
|
|
69
77
|
root.classList.add(themeToApply.class)
|
|
70
78
|
}
|
|
71
79
|
} else {
|
|
72
|
-
// Apply the selected theme
|
|
80
|
+
// Apply the selected theme. If dark mode is disabled for this app, force
|
|
81
|
+
// any dark selection back to light so projects can opt out entirely.
|
|
73
82
|
const theme = findTheme(themeValue)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const effectiveTheme = theme && theme.isDark && !allowDark.value
|
|
84
|
+
? findTheme('light')
|
|
85
|
+
: theme
|
|
86
|
+
if (effectiveTheme) {
|
|
87
|
+
root.classList.add(effectiveTheme.class)
|
|
88
|
+
isDark.value = effectiveTheme.isDark ?? false
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
}
|
|
@@ -89,6 +101,20 @@ function toggleTheme() {
|
|
|
89
101
|
colorMode.value = cyclableThemes[nextIndex].value
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Configure theme behavior for the current app. Call once at startup.
|
|
106
|
+
*
|
|
107
|
+
* @param options.allowDark Enable dark mode for this project (default: false).
|
|
108
|
+
* When false, dark / system-dark falls back to light.
|
|
109
|
+
*/
|
|
110
|
+
export function configureTheme(options: { allowDark?: boolean } = {}) {
|
|
111
|
+
if (options.allowDark !== undefined) {
|
|
112
|
+
allowDark.value = options.allowDark
|
|
113
|
+
}
|
|
114
|
+
// Re-apply with the new policy so a previously-saved dark choice is corrected.
|
|
115
|
+
applyTheme(colorMode.value)
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
function addTheme(theme: ThemeOption) {
|
|
93
119
|
// Check if theme with this value already exists
|
|
94
120
|
const exists = themeOptions.value.some(t => t.value === theme.value)
|
package/src/directives/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as pattern } from './pattern'
|
|
2
|
+
export { default as reveal } from './reveal'
|
|
3
|
+
export type { RevealOptions } from './reveal'
|
|
2
4
|
export { default as ripple } from './ripple'
|
|
3
5
|
export { vResize } from './vResize'
|
|
4
6
|
export type { ResizeEvent, ResizeOptions } from './vResize'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Directive, DirectiveBinding } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v-reveal — animate an element into view on first scroll-intersection.
|
|
5
|
+
*
|
|
6
|
+
* <div v-reveal>…</div> // default: fade + rise
|
|
7
|
+
* <div v-reveal="'left'">…</div> // slide from a direction
|
|
8
|
+
* <div v-reveal="{ y: 'up', delay: 120 }">…</div>
|
|
9
|
+
* <Card v-for="…" v-reveal="{ delay: i * 80 }" /> // stagger
|
|
10
|
+
*
|
|
11
|
+
* Adds `.bgl-reveal` immediately (hidden, pre-transformed) and toggles
|
|
12
|
+
* `.bgl-reveal-in` once the element enters the viewport. Honors
|
|
13
|
+
* `prefers-reduced-motion` (shows instantly, no transform). All visual tuning
|
|
14
|
+
* lives in CSS (motion.css) via data-attributes, so it's themeable and cheap.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type Dir = 'up' | 'down' | 'left' | 'right' | 'none'
|
|
18
|
+
|
|
19
|
+
interface RevealOptions {
|
|
20
|
+
/** Direction the element travels in from. Default 'up'. */
|
|
21
|
+
y?: Dir
|
|
22
|
+
/** Stagger / entrance delay in ms. */
|
|
23
|
+
delay?: number
|
|
24
|
+
/** Animate only once (default) or re-run when scrolled away and back. */
|
|
25
|
+
once?: boolean
|
|
26
|
+
/** 0–1 visibility threshold before triggering. Default 0.12. */
|
|
27
|
+
threshold?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const prefersReducedMotion = () =>
|
|
31
|
+
typeof window !== 'undefined'
|
|
32
|
+
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
33
|
+
|
|
34
|
+
function parse(value: RevealOptions | Dir | undefined): Required<RevealOptions> {
|
|
35
|
+
const base: Required<RevealOptions> = { y: 'up', delay: 0, once: true, threshold: 0.12 }
|
|
36
|
+
if (!value) { return base }
|
|
37
|
+
if (typeof value === 'string') { return { ...base, y: value } }
|
|
38
|
+
return { ...base, ...value }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const observers = new WeakMap<HTMLElement, IntersectionObserver>()
|
|
42
|
+
|
|
43
|
+
const reveal: Directive<HTMLElement, RevealOptions | Dir> = {
|
|
44
|
+
mounted(el, binding: DirectiveBinding<RevealOptions | Dir>) {
|
|
45
|
+
const opts = parse(binding.value)
|
|
46
|
+
|
|
47
|
+
// Reduced motion: reveal instantly, skip all transforms/observers.
|
|
48
|
+
if (prefersReducedMotion()) { return }
|
|
49
|
+
|
|
50
|
+
el.classList.add('bgl-reveal')
|
|
51
|
+
if (opts.y !== 'none') { el.dataset.revealDir = opts.y }
|
|
52
|
+
if (opts.delay) { el.style.setProperty('--bgl-reveal-delay', `${opts.delay}ms`) }
|
|
53
|
+
|
|
54
|
+
const observer = new IntersectionObserver((entries) => {
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.isIntersecting) {
|
|
57
|
+
el.classList.add('bgl-reveal-in')
|
|
58
|
+
if (opts.once) { observer.unobserve(el) }
|
|
59
|
+
} else if (!opts.once) {
|
|
60
|
+
el.classList.remove('bgl-reveal-in')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}, { threshold: opts.threshold, rootMargin: '0px 0px -8% 0px' })
|
|
64
|
+
|
|
65
|
+
observer.observe(el)
|
|
66
|
+
observers.set(el, observer)
|
|
67
|
+
},
|
|
68
|
+
unmounted(el) {
|
|
69
|
+
observers.get(el)?.disconnect()
|
|
70
|
+
observers.delete(el)
|
|
71
|
+
},
|
|
72
|
+
getSSRProps() {
|
|
73
|
+
return {}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default reveal
|
|
78
|
+
export type { RevealOptions }
|
package/src/plugins/bagel.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { BagelToastOptions } from './useToast'
|
|
|
5
5
|
import FloatingVue from 'floating-vue'
|
|
6
6
|
import lightboxPlugin from '../components/lightbox/index'
|
|
7
7
|
import { DialogPlugin } from '../dialog/useDialog'
|
|
8
|
-
import { ripple, pattern } from '../directives'
|
|
8
|
+
import { ripple, pattern, reveal } from '../directives'
|
|
9
9
|
import { createI18n, getI18n } from '../i18n'
|
|
10
10
|
import clickOutside from '../utils/clickOutside'
|
|
11
11
|
import { ToastPlugin } from './useToast'
|
|
@@ -23,6 +23,7 @@ export const BagelVue: Plugin<BagelOptions> = {
|
|
|
23
23
|
app.directive('click-outside', clickOutside)
|
|
24
24
|
app.directive('ripple', ripple)
|
|
25
25
|
app.directive('pattern', pattern)
|
|
26
|
+
app.directive('reveal', reveal)
|
|
26
27
|
|
|
27
28
|
// Install UI plugins
|
|
28
29
|
app.use(lightboxPlugin)
|
|
@@ -767,6 +767,17 @@
|
|
|
767
767
|
filter: saturate(200%) !important;
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
+
/* Glass — frosted translucent surface for use over photos / gradients / dark
|
|
771
|
+
* heroes (nav pills, eyebrow chips, overlays). Light-on-dark by default; flip
|
|
772
|
+
* with --bgl-glass-bg / --bgl-glass-color / --bgl-glass-border. */
|
|
773
|
+
.glass {
|
|
774
|
+
background-color: var(--bgl-glass-bg, rgba(255, 255, 255, 0.18)) !important;
|
|
775
|
+
color: var(--bgl-glass-color, #fff) !important;
|
|
776
|
+
border: 1px solid var(--bgl-glass-border, rgba(255, 255, 255, 0.28)) !important;
|
|
777
|
+
backdrop-filter: blur(8px);
|
|
778
|
+
-webkit-backdrop-filter: blur(8px);
|
|
779
|
+
}
|
|
780
|
+
|
|
770
781
|
/* Backdrop Filter */
|
|
771
782
|
.backdrop-blur-none {
|
|
772
783
|
backdrop-filter: blur(0) !important;
|
package/src/styles/bagel.css
CHANGED
package/src/styles/buttons.css
CHANGED
|
@@ -101,6 +101,80 @@
|
|
|
101
101
|
border-bottom: 1px solid currentColor !important;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.hover-opacity-1,
|
|
105
|
+
.hover-opacity-2,
|
|
106
|
+
.hover-opacity-3,
|
|
107
|
+
.hover-opacity-4,
|
|
108
|
+
.hover-opacity-5,
|
|
109
|
+
.hover-opacity-6,
|
|
110
|
+
.hover-opacity-7,
|
|
111
|
+
.hover-opacity-8,
|
|
112
|
+
.hover-opacity-9,
|
|
113
|
+
.hover-opacity-10 {
|
|
114
|
+
transition: var(--bgl-transition);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.hover-opacity-1:hover,
|
|
118
|
+
.hover-opacity-1:active,
|
|
119
|
+
.hover-opacity-1:focus {
|
|
120
|
+
opacity: 0.1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.hover-opacity-2:hover,
|
|
124
|
+
.hover-opacity-2:active,
|
|
125
|
+
.hover-opacity-2:focus {
|
|
126
|
+
opacity: 0.2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
.hover-opacity-3:hover,
|
|
131
|
+
.hover-opacity-3:active,
|
|
132
|
+
.hover-opacity-3:focus {
|
|
133
|
+
opacity: 0.3;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.hover-opacity-4:hover,
|
|
137
|
+
.hover-opacity-4:active,
|
|
138
|
+
.hover-opacity-4:focus {
|
|
139
|
+
opacity: 0.4;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.hover-opacity-5:hover,
|
|
143
|
+
.hover-opacity-5:active,
|
|
144
|
+
.hover-opacity-5:focus {
|
|
145
|
+
opacity: 0.5;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.hover-opacity-6:hover,
|
|
149
|
+
.hover-opacity-6:active,
|
|
150
|
+
.hover-opacity-6:focus {
|
|
151
|
+
opacity: 0.6;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.hover-opacity-7:hover,
|
|
155
|
+
.hover-opacity-7:active,
|
|
156
|
+
.hover-opacity-7:focus {
|
|
157
|
+
opacity: 0.7;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.hover-opacity-8:hover,
|
|
161
|
+
.hover-opacity-8:active,
|
|
162
|
+
.hover-opacity-8:focus {
|
|
163
|
+
opacity: 0.8;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.hover-opacity-9:hover,
|
|
167
|
+
.hover-opacity-9:active,
|
|
168
|
+
.hover-opacity-9:focus {
|
|
169
|
+
opacity: 0.9;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.hover-opacity-10:hover,
|
|
173
|
+
.hover-opacity-10:active,
|
|
174
|
+
.hover-opacity-10:focus {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
.border {
|
|
105
179
|
border: 1px solid var(--bgl-border-color);
|
|
106
180
|
}
|