@bagelink/vue 1.15.94 → 1.15.96

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.94",
4
+ "version": "1.15.96",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -1,15 +1,25 @@
1
1
  <script setup lang="ts">
2
2
  defineOptions({ name: 'BglCard' })
3
3
  import type { GradientDirProp, GradientProp } from '@bagelink/vue'
4
- import { computed } from 'vue'
4
+ import { computed, useSlots } from 'vue'
5
5
  import { useGradientVariant } from '@bagelink/vue'
6
6
 
7
+ const slots = useSlots()
8
+
7
9
  const props = defineProps<{
8
10
  label?: string
9
- /** Card header title. Pairs with the `#header-action` slot (e.g. a "View all"
10
- button or a more-menu) to render a divided header row — replacing the common
11
- hand-rolled `<div class="flex space-between border-bottom">…</div>` pattern.
12
- For full control use the `#header` slot instead. */
11
+ /** Card header title. Sits in the center of the header row. Pairs with the
12
+ surrounding slots to build a 6-region card:
13
+
14
+ #top-start ──── #header / title ──── #top-end (or #header-action) ┐
15
+ │ (default slot) │
16
+ └ #bottom-start ─────── #footer ──────────────── #bottom-end ──────┘
17
+
18
+ Start/end are logical (RTL-aware). The header row renders if any of
19
+ `title` / `#header` / `#top-start` / `#top-end` / `#header-action` is
20
+ present; the footer row renders if any of `#footer` / `#bottom-start` /
21
+ `#bottom-end` is present. For full control of the header center, use the
22
+ `#header` slot instead of `title`. */
13
23
  title?: string
14
24
  thin?: boolean
15
25
  outline?: boolean
@@ -62,6 +72,15 @@ const is = computed(() => {
62
72
  if (props.to !== undefined && props.to !== '') { return 'router-link' }
63
73
  return 'div'
64
74
  })
75
+
76
+ // Header row shows if any of the header pieces are present (or a title is set).
77
+ const hasHeader = computed(() =>
78
+ !!(props.title || slots.header || slots['header-action'] || slots['top-start'] || slots['top-end']),
79
+ )
80
+ // Footer row shows if any footer piece is present.
81
+ const hasFooter = computed(() =>
82
+ !!(slots.footer || slots['bottom-start'] || slots['bottom-end']),
83
+ )
65
84
  </script>
66
85
 
67
86
  <template>
@@ -80,14 +99,47 @@ const is = computed(() => {
80
99
  <span v-if="label" class="card_label block label">
81
100
  {{ label }}
82
101
  </span>
83
- <!-- Header row: title + optional trailing action. Full override via #header. -->
84
- <div v-if="$slots.header || title || $slots['header-action']" class="card_header flex space-between align-items-center">
85
- <slot name="header">
86
- <span class="card_header_title">{{ title }}</span>
87
- <span class="card_header_action flex align-items-center gap-05"><slot name="header-action" /></span>
88
- </slot>
102
+ <!-- Header row: [top-start] [header / title] [top-end / header-action].
103
+ Logical (RTL-aware) start/end. A full `#header` override still replaces
104
+ the title slot only, so `#top-start` / `#top-end` (and the legacy
105
+ `#header-action`) keep working alongside it. -->
106
+ <div
107
+ v-if="hasHeader"
108
+ class="card_header flex space-between align-items-center w-100p"
109
+ >
110
+ <span v-if="$slots['top-start']" class="card_header_start flex align-items-center gap-05">
111
+ <slot name="top-start" />
112
+ </span>
113
+ <span class="card_header_main flex-grow w-100p">
114
+ <slot name="header">
115
+ <span class="card_header_title">{{ title }}</span>
116
+ </slot>
117
+ </span>
118
+ <span
119
+ v-if="$slots['top-end'] || $slots['header-action']"
120
+ class="card_header_action flex align-items-center gap-05"
121
+ >
122
+ <slot name="top-end"><slot name="header-action" /></slot>
123
+ </span>
89
124
  </div>
125
+
90
126
  <slot />
127
+
128
+ <!-- Footer row: [bottom-start] [footer] [bottom-end]. -->
129
+ <div
130
+ v-if="hasFooter"
131
+ class="card_footer flex space-between align-items-center"
132
+ >
133
+ <span v-if="$slots['bottom-start']" class="card_footer_start flex align-items-center gap-05">
134
+ <slot name="bottom-start" />
135
+ </span>
136
+ <span class="card_footer_main flex-grow">
137
+ <slot name="footer" />
138
+ </span>
139
+ <span v-if="$slots['bottom-end']" class="card_footer_end flex align-items-center gap-05">
140
+ <slot name="bottom-end" />
141
+ </span>
142
+ </div>
91
143
  </component>
92
144
  </template>
93
145
 
@@ -106,10 +158,23 @@ padding: 0.85rem var(--bgl-card-pad);
106
158
  border-bottom: 1px solid var(--bgl-border-color);
107
159
  min-height: 0;
108
160
  }
161
+ .card_header_main {
162
+ text-align: start;
163
+ min-width: 0;
164
+ }
109
165
  .card_header_title {
110
166
  font-weight: 600;
111
167
  }
112
168
 
169
+ /* Footer row: mirrors the header — full-bleed divider via negative margins,
170
+ border on top instead of bottom. */
171
+ .card_footer {
172
+ margin: var(--bgl-card-pad) calc(var(--bgl-card-pad) * -1) calc(var(--bgl-card-pad) * -1);
173
+ padding: 0.85rem var(--bgl-card-pad);
174
+ border-top: 1px solid var(--bgl-border-color);
175
+ min-height: 0;
176
+ }
177
+
113
178
  .bgl_card {
114
179
  --bgl-card-pad: 2rem;
115
180
  border-radius: var(--bgl-card-border-radius);
@@ -129,7 +194,6 @@ padding: var(--bgl-card-pad);
129
194
  /* When padding is stripped via the p-0 utility, the header supplies its own. */
130
195
  .bgl_card.p-0 { --bgl-card-pad: 0rem; }
131
196
  .bgl_card.p-0 .card_header { padding-inline: 1rem; padding-block: 0.75rem; margin-bottom: 0; }
132
- /* Likewise the label hangs off the card edge on a p-0 card, so give it its own. */
133
197
  .bgl_card.p-0 .card_label { padding-inline: 1rem; padding-top: 0.75rem; }
134
198
 
135
199
  .bgl_card.BagelTable {
@@ -229,40 +229,47 @@ opacity: 0.5;
229
229
  pointer-events: none;
230
230
  }
231
231
 
232
+ /* Non-clickable rows render as a plain <div> — keep the default arrow cursor
233
+ (the base .list-item rule sets `cursor: pointer` for the interactive case).
234
+ The hover tint is already scoped to .list-item-fullrow, so nothing to undo. */
235
+ .notClickable {
236
+ cursor: default;
237
+ }
238
+
232
239
  /* Accent used by the icon-chip and (for rounded/active) the tint. Defaults to
233
240
  primary; set per-item via `color` prop → --bgl-list-item-accent. */
234
241
  .list-item-row {
235
242
  --bgl-list-item-accent: var(--bgl-primary);
236
243
  }
237
244
 
238
- /* Hover/active use Bagelink's translucent gray tint, which already flips from
239
- translucent-dark (light mode) to translucent-light (.bgl-dark-mode) so it
240
- reads correctly on any background. Override per-list with --bgl-list-item-hover. */
241
- .list-item:hover,
242
- .list-item.router-link-exact-active {
245
+ /* Hover/active paint on the ROW (not the inner clickable element) so the tint
246
+ spans the whole item including the #start/#end slots that live outside the
247
+ clickable area. Bagelink's translucent gray tint already flips from
248
+ translucent-dark (light mode) to translucent-light (.bgl-dark-mode), so it
249
+ reads correctly on any background. Override per-list with --bgl-list-item-hover.
250
+ Only interactive rows (those carrying .list-item-fullrow) get the tint — a
251
+ plain non-clickable row must stay flat. */
252
+ .list-item-fullrow:hover,
253
+ .list-item-row:has(.list-item.router-link-exact-active) {
243
254
  background-color: var(--bgl-list-item-hover, var(--bgl-gray-tint));
244
255
  }
245
- .list-item.router-link-exact-active,
246
- .list-item-active .list-item {
256
+ .list-item-row:has(.list-item.router-link-exact-active),
257
+ .list-item-active {
247
258
  background-color: var(--bgl-list-item-active, color-mix(in srgb, var(--bgl-list-item-accent) 12%, transparent));
248
259
  color: var(--bgl-list-item-accent);
249
260
  }
250
261
 
251
- /* Rounded card-style rows: pill-radius, tinted accent hover, no divider. */
252
- .list-item-rounded .list-item {
262
+ /* Match the painted background to the row's corner style: flush rows are square,
263
+ while rounded/standalone rows pick up the card radius so the tint follows the
264
+ pill shape rather than bleeding past it. */
265
+ .list-item-rounded {
253
266
  border-radius: var(--bgl-card-border-radius);
254
267
  }
255
- .list-item-rounded .list-item:hover {
268
+ .list-item-rounded:hover {
256
269
  background-color: var(--bgl-list-item-hover, color-mix(in srgb, var(--bgl-list-item-accent) 8%, transparent));
257
270
  }
258
271
 
259
- .notClickable,
260
- .notClickable:hover {
261
- background-color: unset;
262
- cursor: default;
263
- }
264
-
265
- .list-item:active {
272
+ .list-item-fullrow:active {
266
273
  filter: var(--bgl-hover-filter);
267
274
  }
268
275
 
@@ -13,6 +13,11 @@ interface Props {
13
13
  /** Persist sidebar state to localStorage (desktop only). Default true. */
14
14
  persist?: boolean
15
15
  storageKey?: string
16
+ /** Width of the end-docked aside panel (e.g. an AI chat). Reserved as content
17
+ margin while `asideOpen` is true. */
18
+ asideWidth?: string
19
+ /** v-model:asideOpen — controls whether content reserves room for the aside. */
20
+ asideOpen?: boolean
16
21
  }
17
22
 
18
23
  const props = withDefaults(defineProps<Props>(), {
@@ -22,6 +27,8 @@ const props = withDefaults(defineProps<Props>(), {
22
27
  open: undefined,
23
28
  persist: true,
24
29
  storageKey: 'bgl-sidebar-open',
30
+ asideWidth: '400px',
31
+ asideOpen: false,
25
32
  })
26
33
 
27
34
  const emit = defineEmits<{ 'update:open': [value: boolean] }>()
@@ -73,11 +80,14 @@ function closeOnMobile() {
73
80
 
74
81
  // Computed styles for main content margin
75
82
  const mainContentStyles = computed(() => {
83
+ // The end-docked aside reserves room on desktop only (it overlays on mobile).
84
+ const marginInlineEnd = !isMobile.value && props.asideOpen ? props.asideWidth : '0'
76
85
  if (isMobile.value) {
77
- return { marginInlineStart: '0' }
86
+ return { marginInlineStart: '0', marginInlineEnd }
78
87
  }
79
88
  return {
80
- marginInlineStart: isOpen.value ? props.sidebarWidth : sidebarCollapsedWidth
89
+ marginInlineStart: isOpen.value ? props.sidebarWidth : sidebarCollapsedWidth,
90
+ marginInlineEnd,
81
91
  }
82
92
  })
83
93
 
@@ -113,6 +123,10 @@ onUnmounted(() => {
113
123
  <!-- Sidebar Slot -->
114
124
  <slot name="sidebar" v-bind="{ isOpen, isMobile, toggleMenu, closeOnMobile }" />
115
125
 
126
+ <!-- End-docked aside slot (e.g. AI chat). Owns its own open state +
127
+ launcher; sync it to `asideOpen` so content reserves room for it. -->
128
+ <slot name="aside" v-bind="{ isMobile }" />
129
+
116
130
  <!-- Main Content Area -->
117
131
  <main
118
132
  class="main-content flex column ease-300"
@@ -157,7 +171,9 @@ z-index: 40;
157
171
  .main-content {
158
172
  flex: 1;
159
173
  overflow: hidden;
160
- transition: margin-inline-start 400ms ease;
174
+ /* Pushes when the sidebar or end aside opens/closes. Keep in sync with the
175
+ aside panel's own slide transition (see ChatPanel) so they move together. */
176
+ transition: margin 300ms ease;
161
177
  min-height: 100vh
162
178
  }
163
179