@bagelink/vue 1.14.15 → 1.15.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.
Files changed (102) hide show
  1. package/dist/components/Alert.vue.d.ts.map +1 -1
  2. package/dist/components/Badge.vue.d.ts.map +1 -1
  3. package/dist/components/Btn.vue.d.ts.map +1 -1
  4. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  5. package/dist/components/Image.vue.d.ts.map +1 -1
  6. package/dist/components/ListItem.vue.d.ts.map +1 -1
  7. package/dist/components/MapEmbed/Index.vue.d.ts.map +1 -1
  8. package/dist/components/Pagination.vue.d.ts.map +1 -1
  9. package/dist/components/Swiper.vue.d.ts.map +1 -1
  10. package/dist/components/Toast.vue.d.ts.map +1 -1
  11. package/dist/components/form/index.d.ts.map +1 -1
  12. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  13. package/dist/components/index.d.ts.map +1 -1
  14. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  15. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  16. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  17. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  18. package/dist/components/layout/Resizable.vue.d.ts.map +1 -1
  19. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
  20. package/dist/components/layout/appLayoutContext.d.ts +24 -0
  21. package/dist/components/layout/appLayoutContext.d.ts.map +1 -0
  22. package/dist/components/layout/index.d.ts.map +1 -1
  23. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  24. package/dist/composables/index.d.ts.map +1 -1
  25. package/dist/composables/useDevice.d.ts.map +1 -1
  26. package/dist/composables/useEscapeKey.d.ts +12 -0
  27. package/dist/composables/useEscapeKey.d.ts.map +1 -0
  28. package/dist/composables/useSchemaField.d.ts.map +1 -1
  29. package/dist/composables/useTheme.d.ts.map +1 -1
  30. package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
  31. package/dist/form-flow/form-flow.d.ts.map +1 -1
  32. package/dist/index.cjs +203 -207
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.mjs +25819 -28870
  35. package/dist/style.css +1 -1
  36. package/dist/types/BagelForm.d.ts.map +1 -1
  37. package/dist/types/BtnOptions.d.ts.map +1 -1
  38. package/dist/utils/constants.d.ts.map +1 -1
  39. package/dist/utils/index.d.ts.map +1 -1
  40. package/package.json +3 -6
  41. package/src/components/Alert.vue +34 -14
  42. package/src/components/Badge.vue +145 -22
  43. package/src/components/Btn.vue +43 -31
  44. package/src/components/Dropdown.vue +5 -12
  45. package/src/components/FilterQuery.vue +1 -1
  46. package/src/components/Image.vue +3 -2
  47. package/src/components/JSONSchema.vue +2 -2
  48. package/src/components/JsonBuilder.vue +1 -1
  49. package/src/components/ListItem.vue +1 -3
  50. package/src/components/MapEmbed/Index.vue +10 -9
  51. package/src/components/NavBar.vue +2 -2
  52. package/src/components/Spreadsheet/Index.vue +1 -1
  53. package/src/components/Swiper.vue +3 -1
  54. package/src/components/Toast.vue +23 -8
  55. package/src/components/calendar/Index.vue +4 -4
  56. package/src/components/calendar/views/MonthView.vue +3 -3
  57. package/src/components/form/index.ts +0 -4
  58. package/src/components/form/inputs/EmailInput.vue +1 -1
  59. package/src/components/form/inputs/NumberInput.vue +1 -1
  60. package/src/components/form/inputs/OTP.vue +2 -2
  61. package/src/components/form/inputs/SelectInput.vue +3 -3
  62. package/src/components/form/inputs/TelInput.vue +2 -2
  63. package/src/components/form/inputs/TextInput.vue +1 -1
  64. package/src/components/form/inputs/Upload/upload.css +2 -2
  65. package/src/components/index.ts +2 -6
  66. package/src/components/layout/AppContent.vue +5 -19
  67. package/src/components/layout/AppLayout.vue +47 -18
  68. package/src/components/layout/AppSidebar.vue +16 -33
  69. package/src/components/layout/Resizable.vue +5 -2
  70. package/src/components/layout/TabsNav.vue +5 -5
  71. package/src/components/layout/appLayoutContext.ts +44 -0
  72. package/src/components/layout/index.ts +2 -0
  73. package/src/components/lightbox/Lightbox.vue +3 -9
  74. package/src/composables/index.ts +1 -0
  75. package/src/composables/useDevice.ts +2 -1
  76. package/src/composables/useEscapeKey.ts +56 -0
  77. package/src/composables/useSchemaField.ts +2 -17
  78. package/src/composables/useTheme.ts +23 -19
  79. package/src/form-flow/FormFlow.vue +2 -0
  80. package/src/form-flow/form-flow.ts +7 -0
  81. package/src/index.ts +0 -2
  82. package/src/styles/inputs.css +1 -1
  83. package/src/types/BagelForm.ts +46 -151
  84. package/src/types/BtnOptions.ts +5 -3
  85. package/src/utils/constants.ts +7 -0
  86. package/src/utils/index.ts +19 -3
  87. package/src/utils/sizeParsing.ts +5 -5
  88. package/vite.config.ts +5 -1
  89. package/src/components/Carousel.vue +0 -724
  90. package/src/components/ImportData.vue +0 -1749
  91. package/src/components/Pill.vue +0 -150
  92. package/src/components/Slider.vue +0 -1446
  93. package/src/components/Title.vue +0 -23
  94. package/src/components/ToolBar.vue +0 -9
  95. package/src/components/form/BagelForm.vue +0 -219
  96. package/src/components/form/BglFieldSet.vue +0 -14
  97. package/src/components/form/BglMultiStepForm.vue +0 -469
  98. package/src/components/form/FieldArray.vue +0 -422
  99. package/src/components/form/useBagelFormState.ts +0 -76
  100. package/src/composables/useFormField.ts +0 -38
  101. package/src/dialog/DialogOLD.vue +0 -358
  102. package/src/utils/BagelFormUtils.ts +0 -684
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { LatLngExpression, Map, Marker } from 'leaflet'
3
- import { appendScript, sleep } from '@bagelink/vue'
4
- import { onMounted, ref, watch } from 'vue'
3
+ import { appendScript, awaitGlobal } from '@bagelink/vue'
4
+ import { onMounted, onUnmounted, ref, watch } from 'vue'
5
5
  import './leaflet.css'
6
6
 
7
7
  type MapMarker = {
@@ -40,14 +40,9 @@ const id = ref(Math.random().toString(36).slice(2, 10))
40
40
  const defaultMarkerSVG = '<svg width="28" height="38" viewBox="0 0 28 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.2263 37.7955C17.0897 37.7955 19.4109 37.0138 19.4109 36.0496C19.4109 35.0854 17.0897 34.3037 14.2263 34.3037C11.363 34.3037 9.04175 35.0854 9.04175 36.0496C9.04175 37.0138 11.363 37.7955 14.2263 37.7955Z" fill="black" fill-opacity="0.1"/><path d="M14.2265 0.549591C21.2842 0.549591 27.0131 6.23786 27.0787 13.28V13.4024C27.0787 19.3328 24.4759 24.4306 21.5627 28.2764C18.6511 32.12 15.4577 34.6754 14.3457 35.5097C14.2748 35.5629 14.1778 35.5629 14.1068 35.5097C12.9947 34.675 9.80135 32.1197 6.88984 28.2762C3.97665 24.4304 1.37378 19.3328 1.37378 13.4024C1.37378 6.30387 7.12806 0.549591 14.2265 0.549591Z" fill="#ED1b3E" stroke="#ED6C6F"/><path d="M14.2263 21.6185C18.7639 21.6185 22.4424 17.94 22.4424 13.4024C22.4424 8.86477 18.7639 5.18631 14.2263 5.18631C9.68872 5.18631 6.01025 8.86477 6.01025 13.4024C6.01025 17.94 9.68872 21.6185 14.2263 21.6185Z" fill="white"/></svg>'
41
41
  const leafletScriptUrl = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'
42
42
 
43
- async function loadGlobalL() {
44
- while (!window.L) { await sleep(100) }
45
- return window.L
46
- }
47
-
48
43
  async function initializeMap() {
49
44
  await appendScript(leafletScriptUrl)
50
- L.value = await loadGlobalL()
45
+ L.value = await awaitGlobal('L')
51
46
  if (!map.value) {
52
47
  map.value = L.value.map(id.value, {
53
48
  center: props.center,
@@ -96,7 +91,7 @@ function latLangFromMarker(marker: MapMarker): LatLngExpression {
96
91
  }
97
92
 
98
93
  async function watchMarkers(markers?: MapMarker[]) {
99
- if (!L.value) { L.value = await loadGlobalL() }
94
+ if (!L.value) { L.value = await awaitGlobal('L') }
100
95
  _markers.value.forEach(marker => marker.remove())
101
96
  if (!markers) { return }
102
97
 
@@ -111,6 +106,12 @@ watch(() => props.markers, watchMarkers, { immediate: true })
111
106
  watch(() => props.center, center => map.value?.setView(center, props.zoom), { immediate: true })
112
107
 
113
108
  onMounted(initializeMap)
109
+
110
+ onUnmounted(() => {
111
+ _markers.value.forEach(marker => marker.remove())
112
+ map.value?.remove()
113
+ map.value = undefined
114
+ })
114
115
  </script>
115
116
 
116
117
  <template>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import type { IconType, NavLink } from '@bagelink/vue'
3
- import { Icon } from '@bagelink/vue'
3
+ import { Icon, MOBILE_BREAKPOINT } from '@bagelink/vue'
4
4
  import { onMounted, ref } from 'vue'
5
5
  import { resolveI18n } from '../i18n'
6
6
 
@@ -24,7 +24,7 @@ withDefaults(
24
24
  const isOpen = ref(true)
25
25
 
26
26
  function calcIsOpen() {
27
- isOpen.value = window.innerWidth < 1100
27
+ isOpen.value = window.innerWidth <= MOBILE_BREAKPOINT
28
28
 
29
29
  const storedNavOpenVal = localStorage.getItem('navOpen')
30
30
  if (storedNavOpenVal === 'true' || storedNavOpenVal === null) { isOpen.value = true }
@@ -217,7 +217,7 @@ function handleSpreadsheetKeyDown(event: KeyboardEvent) {
217
217
  flat thin small value="$t:spreadsheet.selectAll"
218
218
  @click="visibleColumns = columnOptions.map(col => col.key)"
219
219
  />
220
- <Btn flat thin small value="$t:spreadsheet.clearAll" @click="visibleColumns = []" />
220
+ <Btn flat thin sm value="$t:spreadsheet.clearAll" @click="visibleColumns = []" />
221
221
  </div>
222
222
  <CheckInput
223
223
  v-for="col in columnOptions" :key="col.key" v-model="visibleColumns"
@@ -14,6 +14,8 @@ import 'swiper/css/effect-cube'
14
14
  import 'swiper/css/effect-flip'
15
15
  import 'swiper/css/effect-cards'
16
16
 
17
+ defineOptions({ name: 'BglSwiper' })
18
+
17
19
  type SwiperEffect = 'slide' | 'fade' | 'cube' | 'coverflow' | 'flip' | 'cards'
18
20
  type SwiperDirection = 'horizontal' | 'vertical'
19
21
  type SwiperVariant = 'default' | 'testimonial' | 'gallery' | 'cards' | 'coverflow' | 'hero'
@@ -505,7 +507,7 @@ defineExpose({
505
507
  opacity: 0.8;
506
508
  }
507
509
 
508
- @media screen and (max-width: 900px) {
510
+ @media screen and (max-width: 910px) {
509
511
  .swi-ctrl {
510
512
  padding: 0rem 0.5rem;
511
513
  }
@@ -1,30 +1,45 @@
1
1
  <script setup lang="ts">
2
2
  import { Icon } from '@bagelink/vue'
3
+ import { computed } from 'vue'
3
4
 
4
5
  defineOptions({ name: 'BglToast' })
5
6
 
7
+ type ToastType = 'success' | 'error' | 'warning' | 'info'
8
+
6
9
  interface Props {
7
10
  message: string
8
- type?: 'success' | 'error' | 'warning' | 'info'
11
+ type?: ToastType
12
+ /** Boolean shorthands: <Toast error message="..." /> */
13
+ info?: boolean
14
+ success?: boolean
15
+ warning?: boolean
16
+ error?: boolean
9
17
  showIcon?: boolean
10
18
  showCloseButton?: boolean
11
19
  closeToast?: () => void
12
20
  }
13
21
 
14
- withDefaults(defineProps<Props>(), {
15
- type: 'info',
22
+ const props = withDefaults(defineProps<Props>(), {
16
23
  showIcon: true,
17
24
  showCloseButton: true,
18
25
  })
26
+
27
+ const computedType = computed<ToastType>(() => {
28
+ if (props.type) { return props.type }
29
+ if (props.error) { return 'error' }
30
+ if (props.warning) { return 'warning' }
31
+ if (props.success) { return 'success' }
32
+ return 'info'
33
+ })
19
34
  </script>
20
35
 
21
36
  <template>
22
- <div class="custom-toast" :class="`custom-toast--${type}`">
37
+ <div class="custom-toast" :class="`custom-toast--${computedType}`">
23
38
  <div v-if="showIcon" class="custom-toast__icon">
24
- <Icon v-if="type === 'success'" icon="check_circle" />
25
- <Icon v-if="type === 'error'" icon="dangerous" />
26
- <Icon v-if="type === 'warning'" icon="report" />
27
- <Icon v-if="type === 'info'" icon="info" />
39
+ <Icon v-if="computedType === 'success'" icon="check_circle" />
40
+ <Icon v-if="computedType === 'error'" icon="dangerous" />
41
+ <Icon v-if="computedType === 'warning'" icon="report" />
42
+ <Icon v-if="computedType === 'info'" icon="info" />
28
43
  </div>
29
44
  <div class="custom-toast__content">
30
45
  {{ message }}
@@ -163,20 +163,20 @@ onMounted(() => {
163
163
  {{ formatDate(currentDate, 'YYYY') }}
164
164
  </h3>
165
165
  <div class="ms-auto flex gap-025 m_flex-wrap">
166
- <TabsNav size="s" v-if="viewSwitcher === 'tabs'" :model-value="currentView" :tabs="viewTabs" group="calendar-view" @update:model-value="handleViewChange($event as CalendarView)"
166
+ <TabsNav size="sm" v-if="viewSwitcher === 'tabs'" :model-value="currentView" :tabs="viewTabs" group="calendar-view" @update:model-value="handleViewChange($event as CalendarView)"
167
167
  style="--bgl_tabs-border-radius: calc(var(--bgl-btn-border-radius) / 2)" class="txt12 m_mb-05 m_w-100p " align-txt="center" />
168
168
  <Dropdown v-else thin :value="currentView" iconEnd="keyboard_arrow_down" color="gray">
169
169
  <ListItem v-for="(_, key) in views" :key="key" thin :title="t(`calendar.views.${key.toLowerCase()}`)" @click="handleViewChange(key)" />
170
170
  </Dropdown>
171
171
  <div class="flex gap-025">
172
- <Btn icon="calendar" size="s" class="txt12" color="gray" :value="t('calendar.today')" @click="handleDateChange(new Date())" />
172
+ <Btn icon="calendar" size="sm" class="txt12" color="gray" :value="t('calendar.today')" @click="handleDateChange(new Date())" />
173
173
  <Btn
174
- icon="chevron_left" color="gray" size="s" class=""
174
+ icon="chevron_left" color="gray" size="sm" class=""
175
175
  :disabled="currentView === 'Agenda'"
176
176
  @click="handleDateChange(timeDelta(currentDate, { [viewToDeltaKey[currentView]]: -1 }))"
177
177
  />
178
178
  <Btn
179
- icon="chevron_right" color="gray" size="s" class=""
179
+ icon="chevron_right" color="gray" size="sm" class=""
180
180
  :disabled="currentView === 'Agenda'"
181
181
  @click="handleDateChange(timeDelta(currentDate, { [viewToDeltaKey[currentView]]: 1 }))"
182
182
  />
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { SetupContext } from 'vue'
3
3
  import type { CalendarEvent } from '../CalendarTypes'
4
- import { formatDate, getI18n } from '@bagelink/vue'
4
+ import { formatDate, getI18n, useDevice } from '@bagelink/vue'
5
5
  import { computed, useSlots } from 'vue'
6
6
 
7
7
  interface MonthViewEvent {
@@ -23,8 +23,8 @@ const emit = defineEmits<{
23
23
 
24
24
  const slots: SetupContext['slots'] = useSlots()
25
25
 
26
- // Responsive state
27
- const isMobile = computed(() => window.innerWidth < 768)
26
+ // Responsive state (reactive — tracks resize via useDevice)
27
+ const { isMobile } = useDevice()
28
28
 
29
29
  // Calendar data
30
30
  const weekDays = computed(() => {
@@ -1,5 +1 @@
1
- export { default as BglForm } from './BagelForm.vue'
2
- export { default as BagelForm } from './BagelForm.vue'
3
- export { default as BagelMultiStepForm } from './BglMultiStepForm.vue'
4
- export { default as FieldArray } from './FieldArray.vue'
5
1
  export * from './inputs'
@@ -507,6 +507,6 @@ onMounted(() => {
507
507
  }
508
508
 
509
509
  .bagel-input.has-error input {
510
- border-color: var(--bgl-red, #dc3545) !important;
510
+ border-color: var(--bgl-red) !important;
511
511
  }
512
512
  </style>
@@ -320,6 +320,6 @@ const hasValue = computed(() => formattedValue.value.length > 0)
320
320
  }
321
321
 
322
322
  .bagel-input.has-error input {
323
- border-color: var(--bgl-red, #dc3545) !important;
323
+ border-color: var(--bgl-red) !important;
324
324
  }
325
325
  </style>
@@ -117,11 +117,11 @@ function isDigitsFull() {
117
117
 
118
118
  <style scoped>
119
119
  .otp-error-message {
120
- color: var(--bgl-red, #dc3545);
120
+ color: var(--bgl-red);
121
121
  }
122
122
  .otp_wrap.has-error input,
123
123
  .otp_wrap.has-error textarea {
124
- outline: 1px solid var(--bgl-red, #dc3545) !important;
124
+ outline: 1px solid var(--bgl-red) !important;
125
125
  }
126
126
 
127
127
  .otp_wrap {
@@ -457,12 +457,12 @@ onMounted(() => {
457
457
  }
458
458
 
459
459
  .selectinput.has-error .selectinput-btn {
460
- border-color: var(--bgl-red, #dc3545) !important;
461
- outline: 1px solid var(--bgl-red, #dc3545) !important;
460
+ border-color: var(--bgl-red) !important;
461
+ outline: 1px solid var(--bgl-red) !important;
462
462
  }
463
463
 
464
464
  .selectinput.underlined.has-error .selectinput-btn {
465
- border-color: var(--bgl-red, #dc3545) !important;
465
+ border-color: var(--bgl-red) !important;
466
466
  }
467
467
 
468
468
  /* Underlined mode styling */
@@ -399,10 +399,10 @@ onMounted(initializeCountry)
399
399
  }
400
400
 
401
401
  .bagel-input.has-error .tel-input {
402
- border: 1px solid var(--bgl-red, #dc3545) !important;
402
+ border: 1px solid var(--bgl-red) !important;
403
403
  }
404
404
  .bagel-input.has-error .tel-input {
405
- --bgl-border-color: var(--bgl-red, #dc3545) !important;
405
+ --bgl-border-color: var(--bgl-red) !important;
406
406
  }
407
407
 
408
408
  @keyframes highlight-country {
@@ -272,6 +272,6 @@ onMounted(async () => {
272
272
 
273
273
  .bagel-input.has-error input,
274
274
  .bagel-input.has-error textarea {
275
- border-color: var(--bgl-red, #dc3545) !important;
275
+ border-color: var(--bgl-red) !important;
276
276
  }
277
277
  </style>
@@ -266,7 +266,7 @@
266
266
  }
267
267
 
268
268
  .bagel-input.has-error .fileUploadWrap {
269
- outline-color: var(--bgl-red, #dc3545);
269
+ outline-color: var(--bgl-red);
270
270
  }
271
271
 
272
272
  /* ─── Variant: frame ─────────────────────────────────────────────────────── */
@@ -285,5 +285,5 @@
285
285
  }
286
286
 
287
287
  .bagel-input.has-error :deep(.bgl-card) {
288
- border-color: var(--bgl-red, #dc3545);
288
+ border-color: var(--bgl-red);
289
289
  }
@@ -6,11 +6,12 @@ export { default as Alert } from './Alert.vue'
6
6
  export * from './analytics'
7
7
  export { default as Avatar } from './Avatar.vue'
8
8
  export { default as Badge } from './Badge.vue'
9
+ /** @deprecated Renamed to Badge. Pill is an alias that will be removed in a future version. */
10
+ export { default as Pill } from './Badge.vue'
9
11
  export { default as BglVideo } from './BglVideo.vue'
10
12
  export { default as Btn } from './Btn.vue'
11
13
  export { default as Calendar } from './calendar/Index.vue'
12
14
  export { default as Card } from './Card.vue'
13
- export { default as Carousel } from './Carousel.vue'
14
15
  export { default as DataPreview } from './DataPreview.vue'
15
16
  export { default as DataTable } from './dataTable/DataTable.vue'
16
17
  /** @deprecated Use DataTable instead. TableSchema is an alias that will be removed in a future version. */
@@ -27,7 +28,6 @@ export { default as Icon } from './Icon/Icon.vue'
27
28
  export { FONT_AWESOME_ICONS, FONT_AWESOME_BRANDS_ICONS, MATERIAL_ICONS } from './Icon/constants'
28
29
  export { default as IframeVue } from './IframeVue.vue'
29
30
  export { default as Image } from './Image.vue'
30
- export { default as ImportData } from './ImportData.vue'
31
31
  export * from './layout'
32
32
  export { default as ListItem } from './ListItem.vue'
33
33
  export { default as ListView } from './ListView.vue'
@@ -37,13 +37,9 @@ export { default as Menu } from './Menu.vue'
37
37
  export { default as NavBar } from './NavBar.vue'
38
38
  export { default as PageTitle } from './PageTitle.vue'
39
39
  export { default as Pagination } from './Pagination.vue'
40
- export { default as Pill } from './Pill.vue'
41
40
  export { default as Rating } from './Rating.vue'
42
41
  export { default as RouterWrapper } from './RouterWrapper.vue'
43
- export { default as Slider } from './Slider.vue'
44
42
  export { default as Spreadsheet } from './Spreadsheet/Index.vue'
45
43
  export { default as Swiper } from './Swiper.vue'
46
- export { default as Title } from './Title.vue'
47
- export { default as ToolBar } from './ToolBar.vue'
48
44
  export { default as TopBar } from './TopBar.vue'
49
45
  export { default as Zoomer } from './Zoomer.vue'
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" setup>
2
2
  import { Btn, PageTitle } from '@bagelink/vue'
3
- import { inject, computed } from 'vue'
3
+ import { useAppLayout } from './appLayoutContext'
4
4
 
5
5
  interface Props {
6
6
  title?: string
@@ -14,28 +14,13 @@ withDefaults(defineProps<Props>(), {
14
14
  border: true,
15
15
  })
16
16
 
17
- // Inject menu state from parent
18
- const menuState = inject('menuState', {
19
- isOpen: { value: true },
20
- isMobile: { value: false },
21
- toggleMenu: () => { },
22
- }) as {
23
- isOpen: { value: boolean }
24
- isMobile: { value: boolean }
25
- toggleMenu: () => void
26
- }
27
-
28
- // Inject sidebar card style state
29
- const sidebarCardStyle = inject('sidebarCardStyle', { value: false })
30
-
31
- // Computed property to check if sidebar has card style
32
- const hasSidebarCard = computed(() => sidebarCardStyle?.value ?? false)
17
+ const { isOpen, toggleMenu, sidebarCardStyle } = useAppLayout()
33
18
  </script>
34
19
 
35
20
  <template>
36
21
  <div
37
22
  class="app-content h-100p flex column" :class="{
38
- paddingAppContent: hasSidebarCard,
23
+ paddingAppContent: sidebarCardStyle,
39
24
  }"
40
25
  >
41
26
  <!-- Header -->
@@ -48,7 +33,8 @@ const hasSidebarCard = computed(() => sidebarCardStyle?.value ?? false)
48
33
  <!-- Menu Toggle Button -->
49
34
  <Btn
50
35
  v-if="showMenuButton" flat icon="dock_to_right" class="menuToggleButton"
51
- @click="menuState.toggleMenu"
36
+ :aria-expanded="isOpen" aria-controls="bgl-app-sidebar" aria-label="Toggle sidebar"
37
+ @click="toggleMenu"
52
38
  />
53
39
 
54
40
  <!-- Back Button -->
@@ -1,35 +1,69 @@
1
1
  <script lang="ts" setup>
2
- import { ref, provide, onMounted, onUnmounted, computed } from 'vue'
2
+ import { ref, provide, onMounted, onUnmounted, computed, watch } from 'vue'
3
+ import { useEscapeKey } from '../../composables/useEscapeKey'
4
+ import { MOBILE_BREAKPOINT } from '../../utils/constants'
5
+ import { AppLayoutKey, SIDEBAR_COLLAPSED_WIDTH, SIDEBAR_COLLAPSED_WIDTH_CARD } from './appLayoutContext'
3
6
 
4
7
  interface Props {
5
8
  sidebarWidth?: string
6
9
  sidebarCardStyle?: boolean
7
10
  defaultOpen?: boolean
11
+ /** v-model:open — controlled sidebar state */
12
+ open?: boolean
13
+ /** Persist sidebar state to localStorage (desktop only). Default true. */
14
+ persist?: boolean
15
+ storageKey?: string
8
16
  }
9
17
 
10
18
  const props = withDefaults(defineProps<Props>(), {
11
19
  sidebarWidth: '260px',
12
20
  sidebarCardStyle: false,
13
21
  defaultOpen: true,
22
+ open: undefined,
23
+ persist: true,
24
+ storageKey: 'bgl-sidebar-open',
14
25
  })
15
26
 
16
- // Menu state
17
- const isOpen = ref(props.defaultOpen)
27
+ const emit = defineEmits<{ 'update:open': [value: boolean] }>()
28
+
29
+ function readStored(): boolean | undefined {
30
+ if (!props.persist || typeof localStorage === 'undefined') { return undefined }
31
+ const raw = localStorage.getItem(props.storageKey)
32
+ return raw === null ? undefined : raw === 'true'
33
+ }
34
+
35
+ const isOpen = ref(props.open ?? readStored() ?? props.defaultOpen)
18
36
  const isMobile = ref(false)
19
37
 
20
- // Check if mobile
38
+ const sidebarCollapsedWidth = props.sidebarCardStyle ? SIDEBAR_COLLAPSED_WIDTH_CARD : SIDEBAR_COLLAPSED_WIDTH
39
+
40
+ // Sync controlled prop → internal state
41
+ watch(() => props.open, (v) => {
42
+ if (v !== undefined) { isOpen.value = v }
43
+ })
44
+
45
+ watch(isOpen, (v) => {
46
+ emit('update:open', v)
47
+ // Persist only explicit desktop state — never the forced mobile collapse
48
+ if (props.persist && !isMobile.value && typeof localStorage !== 'undefined') {
49
+ localStorage.setItem(props.storageKey, String(v))
50
+ }
51
+ })
52
+
21
53
  function checkMobile() {
22
- isMobile.value = window.innerWidth < 910
54
+ isMobile.value = window.innerWidth <= MOBILE_BREAKPOINT
23
55
  if (isMobile.value) {
24
56
  isOpen.value = false
25
57
  }
26
58
  }
27
59
 
28
- // Toggle menu
29
60
  function toggleMenu() {
30
61
  isOpen.value = !isOpen.value
31
62
  }
32
63
 
64
+ // Esc closes the sidebar overlay on mobile
65
+ useEscapeKey(() => { isOpen.value = false }, () => isMobile.value && isOpen.value)
66
+
33
67
  // Close menu on mobile when clicking outside
34
68
  function closeOnMobile() {
35
69
  if (isMobile.value) {
@@ -42,26 +76,21 @@ const mainContentStyles = computed(() => {
42
76
  if (isMobile.value) {
43
77
  return { marginInlineStart: '0' }
44
78
  }
45
- const collapsedWidth = props.sidebarCardStyle ? '82px' : '66px'
46
79
  return {
47
- marginInlineStart: isOpen.value ? props.sidebarWidth : collapsedWidth
80
+ marginInlineStart: isOpen.value ? props.sidebarWidth : sidebarCollapsedWidth
48
81
  }
49
82
  })
50
83
 
51
- // Provide state to child components
52
- provide('menuState', {
84
+ provide(AppLayoutKey, {
53
85
  isOpen,
54
86
  isMobile,
55
87
  toggleMenu,
56
88
  closeOnMobile,
57
89
  sidebarWidth: props.sidebarWidth,
58
- sidebarCollapsedWidth: props.sidebarCardStyle ? '82px' : '66px'
90
+ sidebarCollapsedWidth,
91
+ sidebarCardStyle: props.sidebarCardStyle,
59
92
  })
60
93
 
61
- // Provide sidebar card style based on prop
62
- provide('sidebarCardStyle', { value: props.sidebarCardStyle })
63
-
64
- // Initialize
65
94
  onMounted(() => {
66
95
  checkMobile()
67
96
  window.addEventListener('resize', checkMobile)
@@ -82,7 +111,7 @@ onUnmounted(() => {
82
111
  />
83
112
 
84
113
  <!-- Sidebar Slot -->
85
- <slot name="sidebar" />
114
+ <slot name="sidebar" v-bind="{ isOpen, isMobile, toggleMenu, closeOnMobile }" />
86
115
 
87
116
  <!-- Main Content Area -->
88
117
  <main
@@ -90,11 +119,11 @@ onUnmounted(() => {
90
119
  :style="mainContentStyles"
91
120
  >
92
121
  <!-- Header Slot -->
93
- <slot name="header" />
122
+ <slot name="header" v-bind="{ isOpen, isMobile, toggleMenu, closeOnMobile }" />
94
123
 
95
124
  <!-- Page Content -->
96
125
  <div class="page-content overflow w-100p h-100p">
97
- <slot />
126
+ <slot v-bind="{ isOpen, isMobile, toggleMenu, closeOnMobile }" />
98
127
  </div>
99
128
  </main>
100
129
  </div>
@@ -1,9 +1,10 @@
1
1
  <script lang="ts" setup>
2
2
  import type { NavLink } from '@bagelink/vue'
3
3
  import { Btn, Icon } from '@bagelink/vue'
4
- import { inject, computed, ref, watch } from 'vue'
4
+ import { computed, ref, watch } from 'vue'
5
5
  import { useRoute } from 'vue-router'
6
6
  import { resolveI18n } from '../../i18n'
7
+ import { useAppLayout, SIDEBAR_COLLAPSED_WIDTH, SIDEBAR_COLLAPSED_WIDTH_CARD } from './appLayoutContext'
7
8
 
8
9
  // Extended interface for links with active route tracking
9
10
  interface LinkWithAction extends NavLink {
@@ -24,13 +25,11 @@ interface Props {
24
25
  frame?: boolean
25
26
  activeRoutes?: string[]
26
27
  centerlinks?: boolean
27
- defaultOpen?: boolean
28
28
  }
29
29
 
30
30
  const props = withDefaults(defineProps<Props>(), {
31
31
  card: true,
32
32
  centerlinks: false,
33
- defaultOpen: true,
34
33
  bgColor: 'var(--bgl-white)',
35
34
  textColor: 'var(--bgl-black)',
36
35
  activeColor: 'var(--bgl-black)',
@@ -43,32 +42,16 @@ const props = withDefaults(defineProps<Props>(), {
43
42
  const route = useRoute()
44
43
  const isTransitioning = ref(false)
45
44
 
46
- // Inject menu state from parent
47
- const _fallbackIsOpen = ref(props.defaultOpen)
48
- const _fallbackIsMobile = ref(false)
49
-
50
- const menuState = inject('menuState', {
51
- isOpen: _fallbackIsOpen,
52
- isMobile: _fallbackIsMobile,
53
- closeOnMobile: () => void 0,
54
- sidebarWidth: '260px',
55
- sidebarCollapsedWidth: '66px',
56
- }) as {
57
- isOpen: { value: boolean }
58
- isMobile: { value: boolean }
59
- closeOnMobile: () => void
60
- sidebarWidth: string
61
- sidebarCollapsedWidth: string
62
- }
45
+ const { isOpen, isMobile, sidebarWidth, toggleMenu, closeOnMobile } = useAppLayout()
63
46
 
64
47
  // visually "open" during transition — prevents logo/padding from jumping immediately
65
- const isVisuallyOpen = computed(() => menuState.isOpen.value || isTransitioning.value)
48
+ const isVisuallyOpen = computed(() => isOpen.value || isTransitioning.value)
66
49
 
67
50
  // Watch for changes in menu state to handle transitioning
68
51
  watch(
69
- () => menuState.isOpen.value,
52
+ () => isOpen.value,
70
53
  () => {
71
- if (!menuState.isMobile.value) {
54
+ if (!isMobile.value) {
72
55
  isTransitioning.value = true
73
56
  // Reset after transition completes
74
57
  setTimeout(() => {
@@ -134,9 +117,9 @@ function isActiveRoute(link: LinkWithAction): boolean {
134
117
  const sidebarStyles = computed(() => {
135
118
  let width = '280px'
136
119
 
137
- if (!menuState.isMobile.value) {
138
- const collapsedWidth = props.card ? '82px' : '68px'
139
- width = menuState.isOpen.value ? menuState.sidebarWidth : collapsedWidth
120
+ if (!isMobile.value) {
121
+ const collapsedWidth = props.card ? SIDEBAR_COLLAPSED_WIDTH_CARD : SIDEBAR_COLLAPSED_WIDTH
122
+ width = isOpen.value ? sidebarWidth : collapsedWidth
140
123
  }
141
124
 
142
125
  return {
@@ -147,13 +130,13 @@ const sidebarStyles = computed(() => {
147
130
 
148
131
  <template>
149
132
  <aside
133
+ id="bgl-app-sidebar"
150
134
  class="app-sidebar transition-400 fixed start top bottom h-100vh z-99" :class="{
151
- 'sidebar-mobile-open': menuState.isMobile.value && menuState.isOpen.value,
152
- 'sidebar-mobile-closed':
153
- menuState.isMobile.value && !menuState.isOpen.value,
135
+ 'sidebar-mobile-open': isMobile && isOpen,
136
+ 'sidebar-mobile-closed': isMobile && !isOpen,
154
137
  'transitioning': isTransitioning,
155
138
  'p-05': props.card,
156
- 'sidebar-collapsed': !menuState.isMobile.value && !menuState.isOpen.value,
139
+ 'sidebar-collapsed': !isMobile && !isOpen,
157
140
  }" :style="sidebarStyles"
158
141
  >
159
142
  <div
@@ -186,7 +169,7 @@ const sidebarStyles = computed(() => {
186
169
  <nav class="sidebar-nav flex column flex-stretch gap-025 align-items-start" :class="{ 'justify-content-center': props.centerlinks }">
187
170
  <Btn
188
171
  v-for="link in props.navLinks" :key="link.to"
189
- :title="!menuState.isOpen.value && !menuState.isMobile.value ? resolveI18n(link.label) : ''" fullWidth
172
+ :title="!isOpen && !isMobile ? resolveI18n(link.label) : ''" fullWidth
190
173
  alignTxt="start" class="flex-shrink-0 px-075" :class="{ 'nav-btn-active': isActiveRoute(link) }"
191
174
  :style="{
192
175
  backgroundColor: isActiveRoute(link) ? props.activeColor : props.bgColor,
@@ -205,7 +188,7 @@ const sidebarStyles = computed(() => {
205
188
  <!-- Footer Links -->
206
189
  <Btn
207
190
  v-for="link in props.footerLinks" :key="link.to || link.label"
208
- :title="!menuState.isOpen.value && !menuState.isMobile.value ? resolveI18n(link.label) : ''"
191
+ :title="!isOpen && !isMobile ? resolveI18n(link.label) : ''"
209
192
  :style="{
210
193
  backgroundColor: isActiveRoute(link) ? props.activeColor : props.bgColor,
211
194
  color: isActiveRoute(link) ? 'white' : props.textColor,
@@ -219,7 +202,7 @@ const sidebarStyles = computed(() => {
219
202
  </span>
220
203
  </Btn>
221
204
  <!-- Custom Footer Content Slot -->
222
- <slot name="footer" />
205
+ <slot name="footer" v-bind="{ isOpen, isMobile, toggleMenu, closeOnMobile }" />
223
206
  </div>
224
207
  </div>
225
208
  </aside>
@@ -1,6 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
  import { computed, onMounted, onUnmounted, provide, ref } from 'vue'
3
3
  import { RESIZABLE_KEY, useResizableLayoutProvider } from '../../composables/useResizableLayout'
4
+ import { MOBILE_BREAKPOINT } from '../../utils/constants'
5
+
6
+ defineOptions({ name: 'BglResizable' })
4
7
 
5
8
  interface Props {
6
9
  /** Lay panels side-by-side. Default: vertical (stacked). */
@@ -11,14 +14,14 @@ interface Props {
11
14
  * Set `:breakpoint="0"` to disable responsive switching entirely.
12
15
  */
13
16
  mobileVertical?: boolean
14
- /** Width (px) below which the mobile layout activates. @default 768 */
17
+ /** Width (px) below which the mobile layout activates. @default MOBILE_BREAKPOINT (910) */
15
18
  breakpoint?: number
16
19
  }
17
20
 
18
21
  const props = withDefaults(defineProps<Props>(), {
19
22
  horizontal: false,
20
23
  mobileVertical: true,
21
- breakpoint: 768,
24
+ breakpoint: MOBILE_BREAKPOINT,
22
25
  })
23
26
 
24
27
  const containerEl = ref<HTMLElement>()