@bagelink/vue 1.15.61 → 1.15.65

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 (145) hide show
  1. package/dist/components/AccordionItem.vue.d.ts.map +1 -1
  2. package/dist/components/Avatar.vue.d.ts +6 -1
  3. package/dist/components/Avatar.vue.d.ts.map +1 -1
  4. package/dist/components/Badge.vue.d.ts.map +1 -1
  5. package/dist/components/Card.vue.d.ts +7 -0
  6. package/dist/components/Card.vue.d.ts.map +1 -1
  7. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  8. package/dist/components/EmptyState.vue.d.ts +43 -0
  9. package/dist/components/EmptyState.vue.d.ts.map +1 -0
  10. package/dist/components/Icon/Icon.vue.d.ts +13 -0
  11. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  12. package/dist/components/Image.vue.d.ts +26 -1
  13. package/dist/components/Image.vue.d.ts.map +1 -1
  14. package/dist/components/ListItem.vue.d.ts +9 -9
  15. package/dist/components/ListItem.vue.d.ts.map +1 -1
  16. package/dist/components/Menu.vue.d.ts.map +1 -1
  17. package/dist/components/Swiper.vue.d.ts +3 -3
  18. package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
  19. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  20. package/dist/components/charts/BarChart.vue.d.ts +34 -0
  21. package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
  22. package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
  23. package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
  24. package/dist/components/charts/Donut.vue.d.ts +53 -0
  25. package/dist/components/charts/Donut.vue.d.ts.map +1 -0
  26. package/dist/components/charts/Funnel.vue.d.ts +53 -0
  27. package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
  28. package/dist/components/charts/Gauge.vue.d.ts +28 -0
  29. package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
  30. package/dist/components/charts/LineChart.vue.d.ts +37 -0
  31. package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
  32. package/dist/components/charts/RadialBars.vue.d.ts +34 -0
  33. package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
  34. package/dist/components/charts/RankBars.vue.d.ts +27 -0
  35. package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
  36. package/dist/components/charts/Sparkline.vue.d.ts +25 -0
  37. package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
  38. package/dist/components/charts/StatCard.vue.d.ts +28 -0
  39. package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
  40. package/dist/components/charts/core/data.d.ts +46 -0
  41. package/dist/components/charts/core/data.d.ts.map +1 -0
  42. package/dist/components/charts/core/format.d.ts +13 -0
  43. package/dist/components/charts/core/format.d.ts.map +1 -0
  44. package/dist/components/charts/core/palette.d.ts +19 -0
  45. package/dist/components/charts/core/palette.d.ts.map +1 -0
  46. package/dist/components/charts/core/uid.d.ts +2 -0
  47. package/dist/components/charts/core/uid.d.ts.map +1 -0
  48. package/dist/components/charts/core/useChartAnim.d.ts +11 -0
  49. package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
  50. package/dist/components/charts/core/useChartFrame.d.ts +21 -0
  51. package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
  52. package/dist/components/charts/core/useScale.d.ts +16 -0
  53. package/dist/components/charts/core/useScale.d.ts.map +1 -0
  54. package/dist/components/charts/index.d.ts +12 -0
  55. package/dist/components/charts/index.d.ts.map +1 -0
  56. package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
  57. package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
  58. package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
  59. package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
  60. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  61. package/dist/components/index.d.ts +3 -1
  62. package/dist/components/index.d.ts.map +1 -1
  63. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  64. package/dist/components/layout/Divider.vue.d.ts.map +1 -1
  65. package/dist/components/layout/Layout.vue.d.ts +1 -1
  66. package/dist/components/layout/Layout.vue.d.ts.map +1 -1
  67. package/dist/components/layout/Panel.vue.d.ts +1 -1
  68. package/dist/components/layout/Panel.vue.d.ts.map +1 -1
  69. package/dist/components/layout/SidebarNavItem.vue.d.ts +3 -1
  70. package/dist/components/layout/SidebarNavItem.vue.d.ts.map +1 -1
  71. package/dist/components/layout/Timeline.types.d.ts +9 -0
  72. package/dist/components/layout/Timeline.types.d.ts.map +1 -0
  73. package/dist/components/layout/Timeline.vue.d.ts +42 -0
  74. package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
  75. package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
  76. package/dist/components/layout/TimelineItem.vue.d.ts.map +1 -0
  77. package/dist/components/layout/index.d.ts +3 -0
  78. package/dist/components/layout/index.d.ts.map +1 -1
  79. package/dist/dialog/Dialog.vue.d.ts +4 -0
  80. package/dist/dialog/Dialog.vue.d.ts.map +1 -1
  81. package/dist/index.cjs +110 -116
  82. package/dist/index.mjs +38104 -37045
  83. package/dist/style.css +1 -1
  84. package/package.json +2 -1
  85. package/src/components/AccordionItem.vue +24 -22
  86. package/src/components/Avatar.vue +49 -11
  87. package/src/components/Badge.vue +4 -7
  88. package/src/components/Card.vue +32 -2
  89. package/src/components/Dropdown.vue +14 -3
  90. package/src/components/EmptyState.vue +91 -0
  91. package/src/components/Icon/Icon.vue +118 -25
  92. package/src/components/Image.vue +70 -3
  93. package/src/components/ListItem.vue +43 -22
  94. package/src/components/Menu.vue +10 -2
  95. package/src/components/charts/BarChart.vue +197 -0
  96. package/src/components/charts/ChartTooltip.vue +74 -0
  97. package/src/components/charts/Donut.vue +219 -0
  98. package/src/components/charts/Funnel.vue +377 -0
  99. package/src/components/charts/Gauge.vue +90 -0
  100. package/src/components/charts/LineChart.vue +255 -0
  101. package/src/components/charts/RadialBars.vue +99 -0
  102. package/src/components/charts/RankBars.vue +72 -0
  103. package/src/components/charts/Sparkline.vue +90 -0
  104. package/src/components/charts/StatCard.vue +84 -0
  105. package/src/components/charts/core/data.ts +95 -0
  106. package/src/components/charts/core/format.ts +64 -0
  107. package/src/components/charts/core/palette.ts +52 -0
  108. package/src/components/charts/core/uid.ts +6 -0
  109. package/src/components/charts/core/useChartAnim.ts +60 -0
  110. package/src/components/charts/core/useChartFrame.ts +49 -0
  111. package/src/components/charts/core/useScale.ts +39 -0
  112. package/src/components/charts/index.ts +12 -0
  113. package/src/components/form/inputs/RadioGroup.vue +2 -1
  114. package/src/components/form/inputs/RangeInput.vue +43 -15
  115. package/src/components/form/inputs/SelectInput.vue +1 -19
  116. package/src/components/index.ts +3 -1
  117. package/src/components/layout/AppSidebar.vue +1 -0
  118. package/src/components/layout/Divider.vue +2 -9
  119. package/src/components/layout/SidebarNavItem.vue +82 -41
  120. package/src/components/layout/Timeline.types.ts +9 -0
  121. package/src/components/layout/Timeline.vue +54 -0
  122. package/src/components/layout/TimelineItem.vue +93 -0
  123. package/src/components/layout/index.ts +3 -0
  124. package/src/dialog/Dialog.vue +29 -1
  125. package/src/styles/bagel.css +1 -0
  126. package/src/styles/dark.css +13 -0
  127. package/src/styles/gradients.css +181 -0
  128. package/src/styles/layout.css +9 -0
  129. package/src/styles/text.css +38 -6
  130. package/src/styles/theme.css +1 -1
  131. package/dist/components/analytics/BarChart.vue.d.ts +0 -47
  132. package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
  133. package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
  134. package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
  135. package/dist/components/analytics/LineChart.vue.d.ts +0 -35
  136. package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
  137. package/dist/components/analytics/PieChart.vue.d.ts +0 -53
  138. package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
  139. package/dist/components/analytics/index.d.ts +0 -5
  140. package/dist/components/analytics/index.d.ts.map +0 -1
  141. package/src/components/analytics/BarChart.vue +0 -262
  142. package/src/components/analytics/KpiCard.vue +0 -84
  143. package/src/components/analytics/LineChart.vue +0 -357
  144. package/src/components/analytics/PieChart.vue +0 -544
  145. package/src/components/analytics/index.ts +0 -4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.61",
4
+ "version": "1.15.65",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -90,6 +90,7 @@
90
90
  "scripts": {
91
91
  "dev": "tsx watch src/index.ts",
92
92
  "build": "vite build",
93
+ "gen:cheatsheet": "tsx scripts/gen-cheatsheet.ts",
93
94
  "start": "tsx src/index.ts"
94
95
  }
95
96
  }
@@ -72,7 +72,7 @@ function toggle() {
72
72
  v-if="iconPosition === 'start'" class="accordion-icon"
73
73
  :class="[iconClass, { open: isOpen && iconType === 'expand_more' }]"
74
74
  >
75
- <Icon :icon="computedIcon" />
75
+ <Icon :icon="computedIcon" transition="scale" />
76
76
  </span>
77
77
 
78
78
  <slot name="head">
@@ -85,17 +85,17 @@ function toggle() {
85
85
  v-if="iconPosition === 'end'" class="accordion-icon"
86
86
  :class="[iconClass, { open: isOpen && iconType === 'expand_more' }]"
87
87
  >
88
- <Icon :icon="computedIcon" />
88
+ <Icon :icon="computedIcon" transition="scale" />
89
89
  </span>
90
90
  </button>
91
- <Transition name="expand">
92
- <div
93
- v-if="isOpen" :id="`accordion-body-${id}`" class="accordion-body"
94
- :aria-hidden="isOpen ? 'false' : 'true'"
95
- >
91
+ <div
92
+ :id="`accordion-body-${id}`" class="accordion-body"
93
+ :class="{ open: isOpen }" :aria-hidden="isOpen ? 'false' : 'true'"
94
+ >
95
+ <div class="accordion-body-inner">
96
96
  <slot />
97
97
  </div>
98
- </Transition>
98
+ </div>
99
99
  </div>
100
100
  </template>
101
101
 
@@ -149,24 +149,26 @@ border-bottom: none
149
149
  text-decoration: underline;
150
150
  }
151
151
 
152
- </style>
153
-
154
- <style>
155
-
156
- .expand-enter-active,
157
- .expand-leave-active {
158
- transition: all 0.2s;
159
- transition-delay: 0ms;
152
+ /* Height animation via grid-template-rows 0fr -> 1fr. Unlike a fixed max-height,
153
+ this animates to the content's natural height with no cap (no clipping on long
154
+ bodies) and no JS measurement. The inner wrapper needs min-height:0 + overflow
155
+ hidden so it can collapse fully. */
156
+ .accordion-body {
157
+ display: grid;
158
+ grid-template-rows: 0fr;
159
+ transition: grid-template-rows 0.25s ease;
160
160
  }
161
161
 
162
- .expand-enter-from,
163
- .expand-leave-to {
164
- max-height: 0;
162
+ .accordion-body.open {
163
+ grid-template-rows: 1fr;
165
164
  }
166
165
 
167
- .expand-enter-to,
168
- .expand-leave-from {
169
- max-height: 300px;
166
+ .accordion-body-inner {
167
+ min-height: 0;
168
+ overflow: hidden;
170
169
  }
171
170
 
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .accordion-body { transition: none; }
173
+ }
172
174
  </style>
@@ -1,5 +1,8 @@
1
1
  <script setup lang="ts">
2
- defineOptions({ name: 'BglAvatar' })
2
+ // Forward consumer class/style/attrs to the inner (rounded, clipped) avatar
3
+ // element rather than the positioning wrapper — otherwise a custom background
4
+ // would render as a square behind the circle.
5
+ defineOptions({ name: 'BglAvatar', inheritAttrs: false })
3
6
  import type { IconType, ThemeType } from '@bagelink/vue'
4
7
  import { initials, Icon } from '@bagelink/vue'
5
8
  import { computed } from 'vue'
@@ -16,8 +19,17 @@ const props = withDefaults(defineProps<{
16
19
  square?: boolean | number
17
20
  /** Theme color → tinted background + matching icon/text color (e.g. an icon chip). */
18
21
  color?: ThemeType
22
+ /** Presence/status dot in the corner. `true` = green "online", or a ThemeType
23
+ (e.g. 'gray' offline, 'orange' away, 'red' busy). */
24
+ status?: boolean | ThemeType
25
+ /** Accessible label / tooltip for the status dot. */
26
+ statusTitle?: string
19
27
  }>(), { size: 50 })
20
28
 
29
+ const statusColor = computed(() => (props.status === true ? 'green' : props.status || null))
30
+ // Dot scales with the avatar but stays within sane bounds.
31
+ const dotSize = computed(() => Math.max(7, Math.min(14, Math.round(props.size * 0.28))))
32
+
21
33
  const radius = computed(() => {
22
34
  if (!props.square) { return '1000px' } // circle (default)
23
35
  if (props.square === true) { return 'var(--bgl-btn-border-radius)' }
@@ -25,32 +37,58 @@ const radius = computed(() => {
25
37
  })
26
38
 
27
39
  // When a theme color is given, tint the background and color the icon/initials.
40
+ // Mix straight from the live --bgl-<color> so it always matches the foreground —
41
+ // notably for `primary`, whose pre-baked --bgl-primary-tint token can resolve to a
42
+ // different (default) hue when an app overrides --bgl-primary in a nested scope.
28
43
  const themed = computed(() => props.color
29
- ? { background: `var(--bgl-${props.color}-tint, color-mix(in srgb, var(--bgl-${props.color}) 14%, transparent))`, color: `var(--bgl-${props.color})`, border: 'none' }
44
+ ? { background: `color-mix(in srgb, var(--bgl-${props.color}) 14%, transparent)`, color: `var(--bgl-${props.color})`, border: 'none' }
30
45
  : {})
31
46
  </script>
32
47
 
33
48
  <template>
34
- <div
35
- class="overflow-hidden txt-center p-0 avatar flex justify-content-center align-items-center"
36
- :style="{ width: `${size}px`, height: `${size}px`, borderRadius: radius, ...themed }"
37
- >
38
- <Image v-if="src" :src="src" :alt="alt || name" />
39
- <Icon v-else-if="icon" :icon="icon" :size="size / 40" />
40
- <p v-else :style="{ 'line-height': `${size}px`, 'font-size': `calc(1.5rem * ${size} / 50)` }">
41
- {{ (fallback || initials(name || '')).toUpperCase() }}
42
- </p>
49
+ <div class="avatar-wrap" :style="{ width: `${size}px`, height: `${size}px` }">
50
+ <div
51
+ v-bind="$attrs"
52
+ class="overflow-hidden txt-center p-0 avatar flex justify-content-center align-items-center"
53
+ :style="{ width: `${size}px`, height: `${size}px`, borderRadius: radius, ...themed }"
54
+ >
55
+ <Image v-if="src" :src="src" :alt="alt || name" />
56
+ <Icon v-else-if="icon" :icon="icon" :size="size / 40" />
57
+ <p v-else :style="{ 'line-height': `${size}px`, 'font-size': `calc(1.5rem * ${size} / 50)` }">
58
+ {{ (fallback || initials(name || '')).toUpperCase() }}
59
+ </p>
60
+ </div>
61
+ <span
62
+ v-if="statusColor" class="avatar-status" :title="statusTitle"
63
+ :style="{ width: `${dotSize}px`, height: `${dotSize}px`, background: `var(--bgl-${statusColor})` }"
64
+ />
43
65
  </div>
44
66
  </template>
45
67
 
46
68
  <style scoped>
47
69
 
70
+ .avatar-wrap {
71
+ position: relative;
72
+ flex-shrink: 0;
73
+ display: inline-flex;
74
+ }
75
+
48
76
  .avatar {
49
77
  background-color: var(--bgl-gray-tint);
50
78
  border: 0.5px solid var(--bgl-border-color);
51
79
  flex-shrink: 0;
52
80
  }
53
81
 
82
+ /* Presence dot — sits over the bottom-trailing corner, ringed by the surface
83
+ color so it reads cleanly on any background. */
84
+ .avatar-status {
85
+ position: absolute;
86
+ inset-block-end: 0;
87
+ inset-inline-end: 0;
88
+ border-radius: 50%;
89
+ box-shadow: 0 0 0 2px var(--bgl-box-bg, #fff);
90
+ }
91
+
54
92
  .avatar p {
55
93
  font-size: 1.5rem;
56
94
  line-height: 50px;
@@ -55,12 +55,6 @@ const computedPairClass = computed(() => {
55
55
  return `pair-${theme}`
56
56
  })
57
57
 
58
- // Dot uses the badge's theme color (strip any -light/-tint suffix for a solid dot).
59
- const dotColor = computed(() => {
60
- const base = (computedTheme.value || 'primary').replace(/-(light|tint|10|20|30|40|50|60|70)$/, '')
61
- return `var(--bgl-${base})`
62
- })
63
-
64
58
  const computedSize = computed(() => {
65
59
  if (props.size) { return props.size }
66
60
  if (props.sm) { return 'sm' }
@@ -105,7 +99,7 @@ const computedClasses = computed(() => {
105
99
  <div v-if="loading" class="loading absolute inset-0 mx-auto" />
106
100
  <div class="px-025 flex gap-025 justify-content-center align-items-center" :class="{ 'opacity-0': loading }">
107
101
  <Btn v-if="btn" class="bgl_pill-btn -ms-025" icon-size="0.8" round v-bind="btn" />
108
- <span v-if="dot" class="bgl_pill-dot" :style="{ background: dotColor }" />
102
+ <span v-if="dot" class="bgl_pill-dot" />
109
103
  <Icon v-if="icon" class="line-height-0" :icon="icon" style="font-size: var(--bgl-pill-font-size)" />
110
104
  <slot :class="{ uppercase }" />
111
105
  <template v-if="!slots.default">
@@ -138,6 +132,9 @@ width: 6px;
138
132
  height: 6px;
139
133
  border-radius: 50%;
140
134
  flex-shrink: 0;
135
+ /* Use the badge's text color so the dot is always legible on both solid
136
+ (white text) and -light/-tint (tone text) variants. */
137
+ background: currentColor;
141
138
  }
142
139
  .bgl_pill-btn{
143
140
  color: var(--bgl-pill-btn-color);
@@ -4,6 +4,11 @@ import { computed } from 'vue'
4
4
 
5
5
  const props = defineProps<{
6
6
  label?: string
7
+ /** Card header title. Pairs with the `#header-action` slot (e.g. a "View all"
8
+ button or a more-menu) to render a divided header row — replacing the common
9
+ hand-rolled `<div class="flex space-between border-bottom">…</div>` pattern.
10
+ For full control use the `#header` slot instead. */
11
+ title?: string
7
12
  thin?: boolean
8
13
  outline?: boolean
9
14
  h100?: boolean
@@ -55,6 +60,13 @@ const is = computed(() => {
55
60
  <span v-if="label" class="card_label">
56
61
  {{ label }}
57
62
  </span>
63
+ <!-- Header row: title + optional trailing action. Full override via #header. -->
64
+ <div v-if="$slots.header || title || $slots['header-action']" class="card_header flex space-between align-items-center">
65
+ <slot name="header">
66
+ <span class="card_header_title">{{ title }}</span>
67
+ <span class="card_header_action flex align-items-center gap-05"><slot name="header-action" /></span>
68
+ </slot>
69
+ </div>
58
70
  <slot />
59
71
  </component>
60
72
  </template>
@@ -74,6 +86,19 @@ border-bottom: 1px solid var(--bgl-border-color);
74
86
  margin-bottom: 1rem;
75
87
  }
76
88
 
89
+ /* Header row (title + action). Pulls out to the card edges via negative margins
90
+ so its divider spans full-width regardless of the card's own padding, then
91
+ restores breathing room below. On a p-0 card the negative margins resolve to 0. */
92
+ .card_header {
93
+ margin: calc(var(--bgl-card-pad) * -1) calc(var(--bgl-card-pad) * -1) var(--bgl-card-pad);
94
+ padding: 0.85rem var(--bgl-card-pad);
95
+ border-bottom: 1px solid var(--bgl-border-color);
96
+ min-height: 0;
97
+ }
98
+ .card_header_title {
99
+ font-weight: 600;
100
+ }
101
+
77
102
  .border .card_label {
78
103
  font-size: 0.7rem;
79
104
  font-weight: 300;
@@ -89,9 +114,10 @@ border-bottom: unset;
89
114
  }
90
115
 
91
116
  .bgl_card {
117
+ --bgl-card-pad: 2rem;
92
118
  border-radius: var(--bgl-card-border-radius);
93
119
  background: var(--bgl-box-bg);
94
- padding: 2rem 2rem;
120
+ padding: var(--bgl-card-pad);
95
121
  position: relative;
96
122
  }
97
123
 
@@ -105,8 +131,12 @@ background-color: transparent;
105
131
  }
106
132
 
107
133
  .bgl_card.thin {
108
- padding: 1rem 1rem;
134
+ --bgl-card-pad: 1rem;
135
+ padding: var(--bgl-card-pad);
109
136
  }
137
+ /* When padding is stripped via the p-0 utility, the header supplies its own. */
138
+ .bgl_card.p-0 { --bgl-card-pad: 0rem; }
139
+ .bgl_card.p-0 .card_header { padding-inline: 1rem; padding-block: 0.75rem; margin-bottom: 0; }
110
140
 
111
141
  .bgl_card.BagelTable {
112
142
  height: 100%;
@@ -482,10 +482,21 @@ animation: bgl-dropdown-enter 0.18s ease 0.04s both;
482
482
  transform-origin: top left;
483
483
  }
484
484
 
485
- /* Default menu breathing room so consumers don't wrap content in py-* just to
486
- stop items touching the rounded edges. */
485
+ /* Default menu breathing room so items don't touch the rounded top/bottom edges.
486
+ Applies to the card content itself so plain menu content (e.g. a list of Btns)
487
+ gets the same padding as content wrapped in a .bgl_card — no consumer py-* needed. */
487
488
  .bgl-dropdown__content--card {
488
- padding-block: 0.375rem;
489
+ padding-block: 0.5rem;
490
+ padding-inline: 0.375rem;
491
+ }
492
+ /* When the slot already provides a .bgl_card, that card owns the padding — drop
493
+ the container's own padding so the spacing isn't applied twice. */
494
+ .bgl-dropdown__content--card:has(> .bgl_card) {
495
+ padding: 0;
496
+ }
497
+ .bgl-dropdown__content--card .bgl_card{
498
+ padding-block: 0.5rem;
499
+ padding-inline: 0.375rem;
489
500
  }
490
501
 
491
502
  </style>
@@ -0,0 +1,91 @@
1
+ <script lang="ts" setup>
2
+ defineOptions({ name: 'BglEmptyState' })
3
+ import type { IconType } from '@bagelink/vue'
4
+ import { Icon } from '@bagelink/vue'
5
+ import { useSlots } from 'vue'
6
+ import Btn from './Btn.vue'
7
+
8
+ /**
9
+ * Placeholder for empty lists / no-results / zero states.
10
+ * - `icon` + `title` + `description` for the common case.
11
+ * - `actionLabel` (+ `to`/`href` or @action) renders a primary button.
12
+ * - Slots: `#icon`, default (body text), `#action` for full control.
13
+ * - `compact` for tight inline spots (table empties, small cards).
14
+ */
15
+ const {
16
+ icon = 'inbox',
17
+ title = '',
18
+ description = '',
19
+ actionLabel = '',
20
+ actionIcon,
21
+ to,
22
+ href,
23
+ compact = false,
24
+ class: className = '',
25
+ } = defineProps<{
26
+ icon?: IconType | 'none'
27
+ title?: string
28
+ description?: string
29
+ actionLabel?: string
30
+ actionIcon?: IconType
31
+ to?: string | Record<string, any>
32
+ href?: string
33
+ compact?: boolean
34
+ class?: string
35
+ }>()
36
+
37
+ const emit = defineEmits<{ action: [] }>()
38
+ const slots = useSlots()
39
+ </script>
40
+
41
+ <template>
42
+ <div class="bgl-empty flex-column align-items-center justify-content-center txt-center" :class="[{ 'bgl-empty--compact': compact }, className]">
43
+ <div v-if="slots.icon || icon !== 'none'" class="bgl-empty-icon flex-center">
44
+ <slot name="icon"><Icon :icon="icon" :size="compact ? 1.6 : 2.2" /></slot>
45
+ </div>
46
+ <p v-if="title" class="bgl-empty-title m-0">{{ title }}</p>
47
+ <p v-if="description || slots.default" class="bgl-empty-desc m-0">
48
+ <slot>{{ description }}</slot>
49
+ </p>
50
+ <div v-if="slots.action || actionLabel" class="bgl-empty-action">
51
+ <slot name="action">
52
+ <Btn :color="'primary'" :thin="compact" :icon="actionIcon" :value="actionLabel" :to="to" :href="href" @click="emit('action')" />
53
+ </slot>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <style scoped>
59
+ .bgl-empty {
60
+ gap: 0.75rem;
61
+ padding: 2.5rem 1.5rem;
62
+ color: var(--bgl-text-soft, var(--bgl-gray));
63
+ }
64
+ .bgl-empty--compact {
65
+ gap: 0.5rem;
66
+ padding: 1.25rem 1rem;
67
+ }
68
+ .bgl-empty-icon {
69
+ width: 3.5rem;
70
+ height: 3.5rem;
71
+ border-radius: 50%;
72
+ background: var(--bgl-gray-tint, color-mix(in srgb, var(--bgl-gray) 14%, transparent));
73
+ color: var(--bgl-gray);
74
+ }
75
+ .bgl-empty--compact .bgl-empty-icon {
76
+ width: 2.5rem;
77
+ height: 2.5rem;
78
+ }
79
+ .bgl-empty-title {
80
+ font-weight: 600;
81
+ font-size: 1rem;
82
+ color: var(--bgl-text-color, inherit);
83
+ }
84
+ .bgl-empty-desc {
85
+ font-size: 0.875rem;
86
+ max-width: 32ch;
87
+ }
88
+ .bgl-empty-action {
89
+ margin-top: 0.25rem;
90
+ }
91
+ </style>
@@ -4,6 +4,9 @@ import { useDevice } from '@bagelink/vue'
4
4
  import { computed, ref, onMounted, watchEffect } from 'vue'
5
5
  import { FONT_AWESOME_ICONS, MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
6
6
 
7
+ type AnimateType = 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'slide-up' | 'rotate-in'
8
+ type TransitionType = 'slide-up' | 'rotate' | 'scale' | 'fade'
9
+
7
10
  const props = withDefaults(defineProps<{
8
11
  icon?: IconType
9
12
  name?: IconType
@@ -14,8 +17,19 @@ const props = withDefaults(defineProps<{
14
17
  weight?: number | string
15
18
  fontAwesome?: boolean
16
19
  fill?: boolean
20
+ /** Entrance / looping effect. Pure CSS, opt-in. e.g. `animate="spin"`. */
21
+ animate?: AnimateType
22
+ /** Seconds for one cycle of `animate` (spin/pulse/etc.). */
23
+ animateSpeed?: number
24
+ /** Play the `animate` effect once instead of looping. */
25
+ animateOnce?: boolean
26
+ /** Animate the glyph whenever the icon changes (e.g. a play/pause toggle).
27
+ Defaults ON: pass `transition="rotate"` to pick a style, or
28
+ `:transition="false"` to disable. */
29
+ transition?: boolean | TransitionType
17
30
  }>(), {
18
- size: 1
31
+ size: 1,
32
+ transition: true,
19
33
  })
20
34
 
21
35
  const iconRender = computed(() => (props.icon ?? props.name) as IconType)
@@ -114,33 +128,56 @@ const isCurrentFontReady = computed(() => {
114
128
  if (iconRenderType.value === 'material') { return isMaterialReady.value }
115
129
  return isFaBrand.value ? isFABrandsReady.value : isFAFreeReady.value
116
130
  })
131
+
132
+ // Entrance / loop animation class + timing.
133
+ const animateClass = computed(() => (props.animate ? `bgl_icon--${props.animate}` : null))
134
+ const animateStyle = computed(() => {
135
+ if (!props.animate) { return undefined }
136
+ const style: Record<string, string> = {}
137
+ if (props.animateSpeed !== undefined) { style.animationDuration = `${props.animateSpeed}s` }
138
+ if (props.animateOnce) { style.animationIterationCount = '1' }
139
+ return style
140
+ })
141
+
142
+ // Swap transition: animate whenever the glyph changes. `transition` true uses the
143
+ // default style; a string picks one. The keyed inner span drives Vue's <Transition>.
144
+ const transitionName = computed(() => {
145
+ // `bgl-icon-x-none` has no CSS defined, so the swap is instant when disabled.
146
+ if (props.transition === false) { return 'bgl-icon-x-none' }
147
+ const style = props.transition === true ? 'slide-up' : props.transition
148
+ return `bgl-icon-x-${style}`
149
+ })
117
150
  </script>
118
151
 
119
152
  <template>
120
- <span
121
- v-if="iconRenderType === 'material'" class="bgl_icon-font notranslate"
122
- :class="{ 'round flex aspect-ratio-1 justify-content-center': round }" :style="{
123
- 'fontSize': `${computedSize}rem`,
124
- color,
125
- 'font-variation-settings': `'FILL' ${fill ? 1 : 0}, 'wght' ${weight || 400}`,
126
- 'width': round ? `calc(${computedSize}rem * 2)` : 'auto',
127
- 'height': round ? `calc(${computedSize}rem * 2)` : 'auto',
128
- 'visibility': isCurrentFontReady ? 'visible' : 'hidden',
129
- }" translate="no"
130
- >
131
- {{ iconRender }}
132
- </span>
133
- <span
134
- v-else-if="iconRenderType === 'font-awesome'" class="fa bgl_icon-font notranslate" :class="[
135
- `fa-${iconRender}`,
136
- {
137
- 'fa-brands': isFaBrand,
138
- 'fa-solid': fill,
139
- 'far': !fill && !isFaBrand,
140
- },
141
- ]" :style="{ 'fontSize': `${computedSize}rem`, color, 'font-variation-settings': `'wght' ${weight || 400}`, 'visibility': isCurrentFontReady ? 'visible' : 'hidden' }"
142
- translate="no"
143
- />
153
+ <Transition :name="transitionName" mode="out-in">
154
+ <span
155
+ v-if="iconRenderType === 'material'" :key="iconRender" class="bgl_icon-font notranslate"
156
+ :class="[animateClass, { 'round flex aspect-ratio-1 justify-content-center': round }]" :style="{
157
+ 'fontSize': `${computedSize}rem`,
158
+ color,
159
+ 'font-variation-settings': `'FILL' ${fill ? 1 : 0}, 'wght' ${weight || 400}`,
160
+ 'width': round ? `calc(${computedSize}rem * 2)` : 'auto',
161
+ 'height': round ? `calc(${computedSize}rem * 2)` : 'auto',
162
+ 'visibility': isCurrentFontReady ? 'visible' : 'hidden',
163
+ ...animateStyle,
164
+ }" translate="no"
165
+ >
166
+ {{ iconRender }}
167
+ </span>
168
+ <span
169
+ v-else-if="iconRenderType === 'font-awesome'" :key="iconRender" class="fa bgl_icon-font notranslate" :class="[
170
+ `fa-${iconRender}`,
171
+ animateClass,
172
+ {
173
+ 'fa-brands': isFaBrand,
174
+ 'fa-solid': fill,
175
+ 'far': !fill && !isFaBrand,
176
+ },
177
+ ]" :style="{ 'fontSize': `${computedSize}rem`, color, 'font-variation-settings': `'wght' ${weight || 400}`, 'visibility': isCurrentFontReady ? 'visible' : 'hidden', ...animateStyle }"
178
+ translate="no"
179
+ />
180
+ </Transition>
144
181
  </template>
145
182
 
146
183
  <style>
@@ -166,6 +203,62 @@ const isCurrentFontReady = computed(() => {
166
203
  .bgl_icon-font.fa-brands {
167
204
  font-family: 'Font Awesome 6 Brands', serif !important;
168
205
  }
206
+
207
+ /* ── Entrance / loop animations (opt-in via `animate`) ─────────────────────── */
208
+ .bgl_icon--spin,
209
+ .bgl_icon--pulse,
210
+ .bgl_icon--bounce { display: inline-block; animation-duration: 1s; animation-iteration-count: infinite; animation-timing-function: linear; }
211
+ .bgl_icon--fade-in,
212
+ .bgl_icon--slide-up,
213
+ .bgl_icon--rotate-in { display: inline-block; animation-duration: 0.3s; animation-iteration-count: 1; animation-timing-function: ease; }
214
+
215
+ .bgl_icon--spin { animation-name: bgl-icon-spin; }
216
+ .bgl_icon--pulse { animation-name: bgl-icon-pulse; animation-timing-function: ease-in-out; }
217
+ .bgl_icon--bounce { animation-name: bgl-icon-bounce; animation-timing-function: ease; }
218
+ .bgl_icon--fade-in { animation-name: bgl-icon-fade-in; }
219
+ .bgl_icon--slide-up { animation-name: bgl-icon-slide-up; }
220
+ .bgl_icon--rotate-in { animation-name: bgl-icon-rotate-in; }
221
+
222
+ @keyframes bgl-icon-spin { to { transform: rotate(360deg); } }
223
+ @keyframes bgl-icon-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
224
+ @keyframes bgl-icon-bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-20%); } }
225
+ @keyframes bgl-icon-fade-in { from { opacity: 0; } to { opacity: 1; } }
226
+ @keyframes bgl-icon-slide-up { from { opacity: 0; transform: translateY(0.35em); } to { opacity: 1; transform: translateY(0); } }
227
+ @keyframes bgl-icon-rotate-in { from { opacity: 0; transform: rotate(-90deg) scale(0.6); } to { opacity: 1; transform: rotate(0) scale(1); } }
228
+
229
+ @media (prefers-reduced-motion: reduce) {
230
+ .bgl_icon--spin, .bgl_icon--pulse, .bgl_icon--bounce,
231
+ .bgl_icon--fade-in, .bgl_icon--slide-up, .bgl_icon--rotate-in { animation: none; }
232
+ }
233
+
234
+ /* ── Swap transitions (on `name`/`icon` change; `transition` defaults on) ──── */
235
+ .bgl-icon-x-slide-up-enter-active,
236
+ .bgl-icon-x-slide-up-leave-active,
237
+ .bgl-icon-x-rotate-enter-active,
238
+ .bgl-icon-x-rotate-leave-active,
239
+ .bgl-icon-x-scale-enter-active,
240
+ .bgl-icon-x-scale-leave-active,
241
+ .bgl-icon-x-fade-enter-active,
242
+ .bgl-icon-x-fade-leave-active { transition: opacity 0.16s ease, transform 0.16s ease; }
243
+
244
+ .bgl-icon-x-slide-up-enter-from { opacity: 0; transform: translateY(0.3em); }
245
+ .bgl-icon-x-slide-up-leave-to { opacity: 0; transform: translateY(-0.3em); }
246
+
247
+ .bgl-icon-x-rotate-enter-from { opacity: 0; transform: rotate(-90deg); }
248
+ .bgl-icon-x-rotate-leave-to { opacity: 0; transform: rotate(90deg); }
249
+
250
+ .bgl-icon-x-scale-enter-from { opacity: 0; transform: scale(0.5); }
251
+ .bgl-icon-x-scale-leave-to { opacity: 0; transform: scale(0.5); }
252
+
253
+ .bgl-icon-x-fade-enter-from,
254
+ .bgl-icon-x-fade-leave-to { opacity: 0; }
255
+
256
+ @media (prefers-reduced-motion: reduce) {
257
+ .bgl-icon-x-slide-up-enter-active, .bgl-icon-x-slide-up-leave-active,
258
+ .bgl-icon-x-rotate-enter-active, .bgl-icon-x-rotate-leave-active,
259
+ .bgl-icon-x-scale-enter-active, .bgl-icon-x-scale-leave-active,
260
+ .bgl-icon-x-fade-enter-active, .bgl-icon-x-fade-leave-active { transition: none; }
261
+ }
169
262
  </style>
170
263
 
171
264
  <!-- <style src="./font-awesome.css" /> -->