@bagelink/vue 1.15.59 → 1.15.63
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 +20 -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/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 +8104 -7796
- 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 +12 -15
- package/src/components/layout/Divider.vue +57 -0
- package/src/components/layout/Layout.vue +17 -4
- package/src/components/layout/SidebarNavItem.vue +227 -0
- package/src/components/layout/index.ts +1 -0
- 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/dark.css +107 -2
- package/src/styles/layout.css +65 -13
- package/src/styles/motion.css +91 -0
- package/src/styles/text.css +38 -6
- package/src/utils/index.ts +1 -1
|
@@ -5,10 +5,12 @@ import { computed, ref, watch } from 'vue'
|
|
|
5
5
|
import { useRoute } from 'vue-router'
|
|
6
6
|
import { resolveI18n } from '../../i18n'
|
|
7
7
|
import { useAppLayout, SIDEBAR_COLLAPSED_WIDTH, SIDEBAR_COLLAPSED_WIDTH_CARD } from './appLayoutContext'
|
|
8
|
+
import SidebarNavItem from './SidebarNavItem.vue'
|
|
8
9
|
|
|
9
10
|
// Extended interface for links with active route tracking
|
|
10
11
|
interface LinkWithAction extends NavLink {
|
|
11
12
|
activeRoutes?: string[] // Routes that should be marked as active for this link
|
|
13
|
+
children?: LinkWithAction[] // Nested sub-links (expandable group / collapsed flyout)
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
interface Props {
|
|
@@ -167,21 +169,16 @@ const sidebarStyles = computed(() => {
|
|
|
167
169
|
|
|
168
170
|
<!-- Navigation Links -->
|
|
169
171
|
<nav class="sidebar-nav flex column flex-stretch gap-025 align-items-start" :class="{ 'justify-content-center': props.centerlinks }">
|
|
170
|
-
<
|
|
171
|
-
v-for="link in props.navLinks" :key="link.to"
|
|
172
|
-
:
|
|
173
|
-
|
|
174
|
-
:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
<Icon :name="link.icon" size="1.2" />
|
|
181
|
-
<span class="nav-text">
|
|
182
|
-
{{ resolveI18n(link.label) }}
|
|
183
|
-
</span>
|
|
184
|
-
</Btn>
|
|
172
|
+
<SidebarNavItem
|
|
173
|
+
v-for="link in props.navLinks" :key="link.to || link.label"
|
|
174
|
+
:link="link"
|
|
175
|
+
:open="isVisuallyOpen"
|
|
176
|
+
:real-open="isOpen"
|
|
177
|
+
:is-mobile="isMobile"
|
|
178
|
+
:bg-color="props.bgColor"
|
|
179
|
+
:text-color="props.textColor"
|
|
180
|
+
:active-color="props.activeColor"
|
|
181
|
+
/>
|
|
185
182
|
</nav>
|
|
186
183
|
<!-- Footer -->
|
|
187
184
|
<div class="sidebar-footer flex column flex-stretch gap-025 align-items-start px-025 mt-auto">
|
|
@@ -0,0 +1,57 @@
|
|
|
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 flex gap-075" :class="className" role="separator">
|
|
40
|
+
<span class="divider bgl-divider-line" />
|
|
41
|
+
<span class="bgl-divider-label flex-shrink-0 white-space-nowrap"><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-line {
|
|
51
|
+
flex: 1;
|
|
52
|
+
}
|
|
53
|
+
.bgl-divider-label {
|
|
54
|
+
font-size: 0.8125rem;
|
|
55
|
+
color: var(--bgl-text-soft, var(--bgl-gray));
|
|
56
|
+
}
|
|
57
|
+
</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,227 @@
|
|
|
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 visually expanded (stays true during the collapse transition) */
|
|
17
|
+
open: boolean
|
|
18
|
+
/** real open state — flips immediately when the user toggles the sidebar */
|
|
19
|
+
realOpen?: boolean
|
|
20
|
+
isMobile: boolean
|
|
21
|
+
bgColor: string
|
|
22
|
+
textColor: string
|
|
23
|
+
activeColor: string
|
|
24
|
+
}>(), {})
|
|
25
|
+
|
|
26
|
+
const route = useRoute()
|
|
27
|
+
|
|
28
|
+
const hasChildren = computed(() => !!props.link.children?.length)
|
|
29
|
+
|
|
30
|
+
// active detection (mirrors AppSidebar logic, recursive for parents)
|
|
31
|
+
function linkActive(link: LinkWithAction): boolean {
|
|
32
|
+
const linkPath = link.to
|
|
33
|
+
if (link.activeRoutes?.length) {
|
|
34
|
+
return link.activeRoutes.some(p =>
|
|
35
|
+
p === '/' ? route.path === p : route.path === p || route.path.startsWith(`${p}/`))
|
|
36
|
+
}
|
|
37
|
+
if (!linkPath) return false
|
|
38
|
+
if (linkPath === '/') return route.path === linkPath
|
|
39
|
+
return route.path === linkPath || route.path.startsWith(`${linkPath}/`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const childActive = computed(() => props.link.children?.some(linkActive) ?? false)
|
|
43
|
+
const selfActive = computed(() => linkActive(props.link))
|
|
44
|
+
const isActive = computed(() => selfActive.value || (hasChildren.value && childActive.value))
|
|
45
|
+
|
|
46
|
+
// expanded/collapsed group state — auto-open when a child is active
|
|
47
|
+
const expanded = ref(childActive.value)
|
|
48
|
+
watch(childActive, v => { if (v) expanded.value = true })
|
|
49
|
+
|
|
50
|
+
// Collapse the open child group smoothly *before* the layout switches to the
|
|
51
|
+
// icon-only flyout, so parents don't jump up when the sidebar narrows. We watch
|
|
52
|
+
// `realOpen` (which flips immediately on toggle) rather than `open` (which stays
|
|
53
|
+
// true throughout the width transition), so the grid-rows collapse animation runs
|
|
54
|
+
// in sync with the sidebar narrowing instead of snapping at the end.
|
|
55
|
+
watch(() => props.realOpen, (isOpenNow) => {
|
|
56
|
+
if (isOpenNow) expanded.value = childActive.value
|
|
57
|
+
else expanded.value = false
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function onParentClick() {
|
|
61
|
+
if (hasChildren.value) expanded.value = !expanded.value
|
|
62
|
+
else props.link.action?.()
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<!-- LEAF link -->
|
|
68
|
+
<Btn
|
|
69
|
+
v-if="!hasChildren"
|
|
70
|
+
:title="!open && !isMobile ? resolveI18n(link.label) : ''"
|
|
71
|
+
fullWidth alignTxt="start" class="flex-shrink-0 px-075"
|
|
72
|
+
:class="{ 'nav-btn-active': selfActive }"
|
|
73
|
+
:style="{
|
|
74
|
+
backgroundColor: selfActive ? activeColor : bgColor,
|
|
75
|
+
color: selfActive ? 'white' : textColor,
|
|
76
|
+
}"
|
|
77
|
+
:to="link.to || '/'" @click="link.action"
|
|
78
|
+
>
|
|
79
|
+
<Icon :name="link.icon" size="1.2" />
|
|
80
|
+
<span class="nav-text">{{ resolveI18n(link.label) }}</span>
|
|
81
|
+
</Btn>
|
|
82
|
+
|
|
83
|
+
<!-- PARENT with children, EXPANDED sidebar -->
|
|
84
|
+
<div v-else-if="open || isMobile" class="sidebar-group w100p">
|
|
85
|
+
<Btn
|
|
86
|
+
fullWidth alignTxt="start" flat class="flex-shrink-0 px-075 sidebar-group-toggle txt-start sidebar-parent"
|
|
87
|
+
:class="{ 'sidebar-parent-active bold': isActive }"
|
|
88
|
+
:style="{
|
|
89
|
+
color: isActive ? activeColor : textColor,
|
|
90
|
+
}"
|
|
91
|
+
@click="onParentClick"
|
|
92
|
+
>
|
|
93
|
+
<Icon :name="link.icon" size="1.2" />
|
|
94
|
+
<span class="nav-text flex-grow">{{ resolveI18n(link.label) }}</span>
|
|
95
|
+
<Icon name="expand_more" size="1.1" class="nav-text sidebar-chevron" :class="{ 'sidebar-chevron-open': expanded }" />
|
|
96
|
+
</Btn>
|
|
97
|
+
<div class="sidebar-children" :class="{ 'sidebar-children-open': expanded }">
|
|
98
|
+
<div class="sidebar-children-inner ps-1">
|
|
99
|
+
<Btn
|
|
100
|
+
v-for="child in link.children" :key="child.to || child.label"
|
|
101
|
+
fullWidth alignTxt="start" class="flex-shrink-0 ps-075 pe-075 sidebar-child"
|
|
102
|
+
:class="{ 'bold sidebar-child-active': linkActive(child) }"
|
|
103
|
+
:style="{
|
|
104
|
+
color: linkActive(child) ? activeColor : textColor,
|
|
105
|
+
'--sidebar-active-color': activeColor,
|
|
106
|
+
}"
|
|
107
|
+
flat
|
|
108
|
+
:to="child.to || '/'" @click="child.action"
|
|
109
|
+
>
|
|
110
|
+
<Icon v-if="child.icon" :name="child.icon" size="1.05" class="opacity-7" />
|
|
111
|
+
<span class="nav-text">{{ resolveI18n(child.label) }}</span>
|
|
112
|
+
</Btn>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- PARENT with children, COLLAPSED sidebar → hover popover flyout -->
|
|
118
|
+
<Dropdown
|
|
119
|
+
v-else
|
|
120
|
+
placement="right-start"
|
|
121
|
+
:triggers="['hover']"
|
|
122
|
+
:delay="{ show: 0, hide: 200 }"
|
|
123
|
+
card
|
|
124
|
+
class="w100p sidebar-flyout-trigger"
|
|
125
|
+
>
|
|
126
|
+
<template #trigger>
|
|
127
|
+
<Btn
|
|
128
|
+
fullWidth alignTxt="start" class="flex-shrink-0 px-075"
|
|
129
|
+
:class="{ 'nav-btn-active': isActive }"
|
|
130
|
+
:style="{
|
|
131
|
+
backgroundColor: isActive ? activeColor : bgColor,
|
|
132
|
+
color: isActive ? 'white' : textColor,
|
|
133
|
+
}"
|
|
134
|
+
>
|
|
135
|
+
<Icon :name="link.icon" size="1.2" />
|
|
136
|
+
<span class="nav-text">{{ resolveI18n(link.label) }}</span>
|
|
137
|
+
</Btn>
|
|
138
|
+
</template>
|
|
139
|
+
<!-- flyout content -->
|
|
140
|
+
<div class="sidebar-flyout p-05 min-w-180px display-flex gap-025 column">
|
|
141
|
+
<p class="sidebar-flyout-label m-0 semi opacity-5 uppercase">{{ resolveI18n(link.label) }}</p>
|
|
142
|
+
<Btn
|
|
143
|
+
v-for="child in link.children" :key="child.to || child.label"
|
|
144
|
+
fullWidth alignTxt="start" thin flat class="flex-shrink-0 px-075 radius-1 sidebar-child"
|
|
145
|
+
:class="{ 'bold sidebar-child-active': linkActive(child) }"
|
|
146
|
+
:style="{
|
|
147
|
+
color: linkActive(child) ? activeColor : 'var(--bgl-text-color)',
|
|
148
|
+
'--sidebar-active-color': activeColor,
|
|
149
|
+
}"
|
|
150
|
+
:to="child.to || '/'" @click="child.action"
|
|
151
|
+
>
|
|
152
|
+
<Icon v-if="child.icon" :name="child.icon" size="1.05" class="opacity-7" />
|
|
153
|
+
<span>{{ resolveI18n(child.label) }}</span>
|
|
154
|
+
</Btn>
|
|
155
|
+
</div>
|
|
156
|
+
</Dropdown>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.sidebar-chevron {
|
|
161
|
+
margin-inline-start: auto;
|
|
162
|
+
transition: transform 0.2s ease;
|
|
163
|
+
opacity: 0.6;
|
|
164
|
+
}
|
|
165
|
+
.sidebar-chevron-open {
|
|
166
|
+
transform: rotate(180deg)!important;
|
|
167
|
+
}
|
|
168
|
+
.sidebar-children {
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-rows: 0fr;
|
|
171
|
+
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
}
|
|
174
|
+
.sidebar-children-open {
|
|
175
|
+
grid-template-rows: 1fr;
|
|
176
|
+
}
|
|
177
|
+
.sidebar-children-inner {
|
|
178
|
+
min-height: 0;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 0.125rem;
|
|
182
|
+
}
|
|
183
|
+
.sidebar-children-open .sidebar-children-inner {
|
|
184
|
+
padding-block: 0.25rem;
|
|
185
|
+
}
|
|
186
|
+
/* Parent toggle — flat <Btn> with no color prop picks up a default `pair-black`
|
|
187
|
+
background; neutralize it so the active parent shows only colored text/icon. */
|
|
188
|
+
.sidebar-parent.bgl_btn {
|
|
189
|
+
background-color: transparent !important;
|
|
190
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
191
|
+
}
|
|
192
|
+
.sidebar-parent.bgl_btn:hover {
|
|
193
|
+
background-color: color-mix(in srgb, currentColor 8%, transparent) !important;
|
|
194
|
+
filter: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.sidebar-flyout-label {
|
|
198
|
+
padding: 0.25rem 0.5rem 0.375rem;
|
|
199
|
+
font-size: 0.6875rem;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
202
|
+
|
|
203
|
+
<!-- Global (un-scoped) so the same child styling also applies inside the flyout,
|
|
204
|
+
which is teleported to <body> and therefore outside this component's scope. -->
|
|
205
|
+
<style>
|
|
206
|
+
.sidebar-child {
|
|
207
|
+
font-size: 0.85rem;
|
|
208
|
+
position: relative;
|
|
209
|
+
transition: background-color 0.15s ease, color 0.15s ease, font-weight 0.15s ease;
|
|
210
|
+
}
|
|
211
|
+
/* Neutralize the default `pair-black` background a flat <Btn> picks up when no
|
|
212
|
+
color prop is set, so we can control the child background ourselves. */
|
|
213
|
+
.sidebar-child.bgl_btn {
|
|
214
|
+
background-color: transparent !important;
|
|
215
|
+
}
|
|
216
|
+
/* Active child — filled with a subtle tint of the active color. */
|
|
217
|
+
.sidebar-child.bgl_btn.sidebar-child-active,
|
|
218
|
+
.sidebar-child.bgl_btn.sidebar-child-active:hover {
|
|
219
|
+
background-color: color-mix(in srgb, var(--sidebar-active-color) 14%, transparent) !important;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Hover indication for children (mirrors the parent feel) */
|
|
223
|
+
.sidebar-child.bgl_btn:hover {
|
|
224
|
+
background-color: color-mix(in srgb, currentColor 12%, transparent) !important;
|
|
225
|
+
filter: none;
|
|
226
|
+
}
|
|
227
|
+
</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/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/dark.css
CHANGED
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
filter: invert(1);
|
|
27
27
|
} */
|
|
28
28
|
|
|
29
|
+
/* `:root.bgl-dark-mode` (the class lives on <html>) raises specificity to
|
|
30
|
+
(0,2,0) so these tokens win over any project that redefines colors in its
|
|
31
|
+
own `:root {}` (e.g. udi sets --bgl-blue: #7ad3f7). The bare .bgl-dark-mode
|
|
32
|
+
is kept as a fallback in case the class is applied to a non-root element. */
|
|
33
|
+
:root.bgl-dark-mode,
|
|
29
34
|
.bgl-dark-mode {
|
|
30
35
|
/* ---- Dark palette (raw values) ---------------------------------------- */
|
|
31
36
|
--bgl-dm-bg: #121317;
|
|
@@ -41,14 +46,31 @@
|
|
|
41
46
|
/* primary text */
|
|
42
47
|
--bgl-dm-text-muted: #9a9b9d;
|
|
43
48
|
/* secondary text */
|
|
44
|
-
--bgl-dm-border:
|
|
45
|
-
/* borders / dividers
|
|
49
|
+
--bgl-dm-border: rgba(255, 255, 255, 0.12);
|
|
50
|
+
/* borders / dividers — translucent light, mirroring the light-mode
|
|
51
|
+
#00000020 (translucent dark). Blends over any dark surface (bg, card,
|
|
52
|
+
input) instead of being a fixed opaque gray. */
|
|
46
53
|
|
|
47
54
|
/* ---- Brand (slightly adjusted for dark backgrounds) ------------------- */
|
|
48
55
|
--bgl-primary: #4f7cff !important;
|
|
49
56
|
--bgl-primary-tint: #4f7cff33;
|
|
50
57
|
--bgl-primary-light: #1a2236;
|
|
51
58
|
|
|
59
|
+
/* ---- Status / accent colors (tuned for dark backgrounds) ------------- *
|
|
60
|
+
* The light-mode bases (e.g. blue #2e5bff, green #75c98f, red #ed6c6f,
|
|
61
|
+
* yellow #ffbb00) are either too dark/saturated or too pastel to read well
|
|
62
|
+
* on a dark surface. Shift them lighter and slightly desaturated so they
|
|
63
|
+
* pop against the dark UI without glowing. Tints follow the same hue at low
|
|
64
|
+
* opacity for soft fills/badges. */
|
|
65
|
+
--bgl-blue: #5b8cff;
|
|
66
|
+
--bgl-blue-tint: rgba(91, 140, 255, 0.18);
|
|
67
|
+
--bgl-green: #5dd49b;
|
|
68
|
+
--bgl-green-tint: rgba(93, 212, 155, 0.18);
|
|
69
|
+
--bgl-red: #f47174;
|
|
70
|
+
--bgl-red-tint: rgba(244, 113, 116, 0.18);
|
|
71
|
+
--bgl-yellow: #ffc94d;
|
|
72
|
+
--bgl-yellow-tint: rgba(255, 201, 77, 0.18);
|
|
73
|
+
|
|
52
74
|
/* ---- Surfaces / backgrounds ------------------------------------------ */
|
|
53
75
|
--bgl-bg: var(--bgl-dm-bg);
|
|
54
76
|
--bgl-box-bg: var(--bgl-dm-surface);
|
|
@@ -58,6 +80,10 @@
|
|
|
58
80
|
--bgl-skeleton-bg: var(--bgl-dm-surface-2);
|
|
59
81
|
--bgl-skeleton-pulse: var(--bgl-dm-surface);
|
|
60
82
|
--bgl-gray-light: var(--bgl-dm-surface-2);
|
|
83
|
+
/* --bgl-gray (#b7b7b7 in light) is used directly by color-gray / bg-gray and
|
|
84
|
+
as a neutral mid tone. On dark, keep it a readable mid-gray so color-gray
|
|
85
|
+
text stays legible; bg-gray fills get a dedicated darker treatment below. */
|
|
86
|
+
--bgl-gray: #8b8f96;
|
|
61
87
|
|
|
62
88
|
/* ---- Text ------------------------------------------------------------- */
|
|
63
89
|
--bgl-text-color: var(--bgl-dm-text);
|
|
@@ -121,6 +147,57 @@
|
|
|
121
147
|
--bgl-gray-10: color-mix(in oklab, white 4%, var(--bgl-dm-surface));
|
|
122
148
|
--bgl-gray-20: color-mix(in oklab, white 8%, var(--bgl-dm-surface));
|
|
123
149
|
--bgl-gray-30: color-mix(in oklab, white 13%, var(--bgl-dm-surface));
|
|
150
|
+
/* Mid/high end of the scale: in light mode these go DARKER as the number
|
|
151
|
+
rises (gray-80 ≈ a strong gray used for lines/dividers). On dark they must
|
|
152
|
+
instead get LIGHTER so "higher number = more contrast vs the surface"
|
|
153
|
+
still holds — otherwise gray-80 lines vanish on the dark background.
|
|
154
|
+
Fixes calendar hour-lines and any other gray-40..90 separators at once. */
|
|
155
|
+
--bgl-gray-40: color-mix(in oklab, white 18%, var(--bgl-dm-surface));
|
|
156
|
+
--bgl-gray-50: color-mix(in oklab, white 24%, var(--bgl-dm-surface));
|
|
157
|
+
--bgl-gray-60: color-mix(in oklab, white 32%, var(--bgl-dm-surface));
|
|
158
|
+
--bgl-gray-70: color-mix(in oklab, white 42%, var(--bgl-dm-surface));
|
|
159
|
+
--bgl-gray-80: color-mix(in oklab, white 54%, var(--bgl-dm-surface));
|
|
160
|
+
--bgl-gray-90: color-mix(in oklab, white 70%, var(--bgl-dm-surface));
|
|
161
|
+
|
|
162
|
+
/* ---- Colored tint scale (10/20/30) ----------------------------------- *
|
|
163
|
+
* In light mode --bgl-{color}-10/20/30 are near-white pastel washes (e.g.
|
|
164
|
+
* green-30 = 70% white + green) used as soft fills — chat bubbles, badges,
|
|
165
|
+
* highlighted rows. On a dark UI those pale pastels glow and force dark text
|
|
166
|
+
* to be unreadable. Remap each to a low-opacity wash of its OWN base color
|
|
167
|
+
* OVER the dark surface, so the fill stays subtly colored but dark, and the
|
|
168
|
+
* light text token reads on top. Higher number = a touch more color.
|
|
169
|
+
* Because these sit in the high-specificity :root.bgl-dark-mode block, they
|
|
170
|
+
* also override any project that hard-sets e.g. --bgl-blue-30 in :root. */
|
|
171
|
+
--bgl-blue-10: color-mix(in oklab, var(--bgl-blue) 10%, var(--bgl-dm-surface));
|
|
172
|
+
--bgl-blue-20: color-mix(in oklab, var(--bgl-blue) 16%, var(--bgl-dm-surface));
|
|
173
|
+
--bgl-blue-30: color-mix(in oklab, var(--bgl-blue) 24%, var(--bgl-dm-surface));
|
|
174
|
+
--bgl-green-10: color-mix(in oklab, var(--bgl-green) 10%, var(--bgl-dm-surface));
|
|
175
|
+
--bgl-green-20: color-mix(in oklab, var(--bgl-green) 16%, var(--bgl-dm-surface));
|
|
176
|
+
--bgl-green-30: color-mix(in oklab, var(--bgl-green) 24%, var(--bgl-dm-surface));
|
|
177
|
+
--bgl-red-10: color-mix(in oklab, var(--bgl-red) 10%, var(--bgl-dm-surface));
|
|
178
|
+
--bgl-red-20: color-mix(in oklab, var(--bgl-red) 16%, var(--bgl-dm-surface));
|
|
179
|
+
--bgl-red-30: color-mix(in oklab, var(--bgl-red) 24%, var(--bgl-dm-surface));
|
|
180
|
+
--bgl-yellow-10: color-mix(in oklab, var(--bgl-yellow) 10%, var(--bgl-dm-surface));
|
|
181
|
+
--bgl-yellow-20: color-mix(in oklab, var(--bgl-yellow) 16%, var(--bgl-dm-surface));
|
|
182
|
+
--bgl-yellow-30: color-mix(in oklab, var(--bgl-yellow) 24%, var(--bgl-dm-surface));
|
|
183
|
+
--bgl-purple-10: color-mix(in oklab, var(--bgl-purple) 10%, var(--bgl-dm-surface));
|
|
184
|
+
--bgl-purple-20: color-mix(in oklab, var(--bgl-purple) 16%, var(--bgl-dm-surface));
|
|
185
|
+
--bgl-purple-30: color-mix(in oklab, var(--bgl-purple) 24%, var(--bgl-dm-surface));
|
|
186
|
+
--bgl-brown-10: color-mix(in oklab, var(--bgl-brown) 10%, var(--bgl-dm-surface));
|
|
187
|
+
--bgl-brown-20: color-mix(in oklab, var(--bgl-brown) 16%, var(--bgl-dm-surface));
|
|
188
|
+
--bgl-brown-30: color-mix(in oklab, var(--bgl-brown) 24%, var(--bgl-dm-surface));
|
|
189
|
+
--bgl-orange-10: color-mix(in oklab, var(--bgl-orange) 10%, var(--bgl-dm-surface));
|
|
190
|
+
--bgl-orange-20: color-mix(in oklab, var(--bgl-orange) 16%, var(--bgl-dm-surface));
|
|
191
|
+
--bgl-orange-30: color-mix(in oklab, var(--bgl-orange) 24%, var(--bgl-dm-surface));
|
|
192
|
+
--bgl-turquoise-10: color-mix(in oklab, var(--bgl-turquoise) 10%, var(--bgl-dm-surface));
|
|
193
|
+
--bgl-turquoise-20: color-mix(in oklab, var(--bgl-turquoise) 16%, var(--bgl-dm-surface));
|
|
194
|
+
--bgl-turquoise-30: color-mix(in oklab, var(--bgl-turquoise) 24%, var(--bgl-dm-surface));
|
|
195
|
+
--bgl-pink-10: color-mix(in oklab, var(--bgl-pink) 10%, var(--bgl-dm-surface));
|
|
196
|
+
--bgl-pink-20: color-mix(in oklab, var(--bgl-pink) 16%, var(--bgl-dm-surface));
|
|
197
|
+
--bgl-pink-30: color-mix(in oklab, var(--bgl-pink) 24%, var(--bgl-dm-surface));
|
|
198
|
+
--bgl-primary-10: color-mix(in oklab, var(--bgl-primary) 10%, var(--bgl-dm-surface));
|
|
199
|
+
--bgl-primary-20: color-mix(in oklab, var(--bgl-primary) 16%, var(--bgl-dm-surface));
|
|
200
|
+
--bgl-primary-30: color-mix(in oklab, var(--bgl-primary) 24%, var(--bgl-dm-surface));
|
|
124
201
|
}
|
|
125
202
|
|
|
126
203
|
/* Native form controls (date/time pickers) render dark glyphs that are
|
|
@@ -156,6 +233,25 @@
|
|
|
156
233
|
border-color: var(--bgl-dm-border);
|
|
157
234
|
}
|
|
158
235
|
|
|
236
|
+
/* .pair-white (e.g. <Btn color="white">) and .pair-gray are neutral/secondary
|
|
237
|
+
fills on light UIs (literal white / a light #b7b7b7 gray). On dark they stay
|
|
238
|
+
glaring light blocks, so for the FILLED variant turn them into a raised dark
|
|
239
|
+
surface with light text and a hairline border (a proper secondary button).
|
|
240
|
+
Flat pills and border pills keep their own treatment (they only tint text /
|
|
241
|
+
outline, not the fill), so we exclude them. */
|
|
242
|
+
.bgl-dark-mode .pair-white:not(.bgl_flatPill):not(.bgl_pill-border),
|
|
243
|
+
.bgl-dark-mode .pair-gray:not(.bgl_flatPill):not(.bgl_pill-border) {
|
|
244
|
+
background-color: var(--bgl-dm-surface-2);
|
|
245
|
+
color: var(--bgl-dm-text);
|
|
246
|
+
border-color: var(--bgl-dm-border);
|
|
247
|
+
}
|
|
248
|
+
/* Hover/active on the dark secondary button: lift one step instead of the
|
|
249
|
+
default lighten-filter which would wash the light original out. */
|
|
250
|
+
.bgl-dark-mode .pair-white:not(.bgl_flatPill):not(.bgl_pill-border):hover,
|
|
251
|
+
.bgl-dark-mode .pair-gray:not(.bgl_flatPill):not(.bgl_pill-border):hover {
|
|
252
|
+
background-color: color-mix(in oklab, white 8%, var(--bgl-dm-surface-2));
|
|
253
|
+
}
|
|
254
|
+
|
|
159
255
|
/* ----------------------------------------------------------------------------
|
|
160
256
|
* Absolute color utilities (bg-white / bg-black …)
|
|
161
257
|
* ----------------------------------------------------------------------------
|
|
@@ -177,6 +273,15 @@
|
|
|
177
273
|
color: var(--bgl-dm-text);
|
|
178
274
|
}
|
|
179
275
|
|
|
276
|
+
/* bg-gray is a solid light-gray (#b7b7b7) fill in light mode — used as a
|
|
277
|
+
neutral surface. On dark it becomes a bright patch, so remap it to a raised
|
|
278
|
+
dark surface (one step up from the card) like the other neutral fills. */
|
|
279
|
+
.bgl-dark-mode .bg-gray:not(.keep-white) {
|
|
280
|
+
background: var(--bgl-dm-surface-2) !important;
|
|
281
|
+
--alpha-color: var(--bgl-dm-surface-2);
|
|
282
|
+
color: var(--bgl-dm-text);
|
|
283
|
+
}
|
|
284
|
+
|
|
180
285
|
.bgl-dark-mode .bg-input-white:not(.keep-white) input,
|
|
181
286
|
.bgl-dark-mode .bg-input-white:not(.keep-white) textarea,
|
|
182
287
|
.bgl-dark-mode .bg-input-white:not(.keep-white) .selectinput-btn,
|