@bagelink/vue 1.15.71 → 1.15.75

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 (38) hide show
  1. package/dist/components/AddressSearch.vue.d.ts +3 -0
  2. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  3. package/dist/components/Alert.vue.d.ts +3 -0
  4. package/dist/components/Alert.vue.d.ts.map +1 -1
  5. package/dist/components/Badge.vue.d.ts +17 -2
  6. package/dist/components/Badge.vue.d.ts.map +1 -1
  7. package/dist/components/Btn.vue.d.ts +17 -2
  8. package/dist/components/Btn.vue.d.ts.map +1 -1
  9. package/dist/components/Card.vue.d.ts.map +1 -1
  10. package/dist/components/Dropdown.vue.d.ts +2 -0
  11. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  12. package/dist/components/ListItem.vue.d.ts +3 -4
  13. package/dist/components/ListItem.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts +1 -1
  15. package/dist/components/form/inputs/SelectInput.vue.d.ts +6 -0
  16. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  17. package/dist/components/layout/TabsNav.vue.d.ts +2 -0
  18. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
  19. package/dist/composables/index.d.ts +2 -0
  20. package/dist/composables/index.d.ts.map +1 -1
  21. package/dist/composables/useGradientVariant.d.ts +37 -0
  22. package/dist/composables/useGradientVariant.d.ts.map +1 -0
  23. package/dist/index.cjs +47 -47
  24. package/dist/index.mjs +5584 -5504
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/components/Alert.vue +9 -1
  28. package/src/components/Badge.vue +37 -5
  29. package/src/components/Btn.vue +49 -6
  30. package/src/components/Card.vue +2 -30
  31. package/src/components/Dropdown.vue +3 -1
  32. package/src/components/ListItem.vue +36 -6
  33. package/src/components/layout/TabsNav.vue +15 -1
  34. package/src/composables/index.ts +2 -0
  35. package/src/composables/useGradientVariant.ts +100 -0
  36. package/src/styles/bagel.css +1 -0
  37. package/src/styles/base-colors.css +9 -0
  38. package/src/styles/color-variants.css +149 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.71",
4
+ "version": "1.15.75",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -10,7 +10,10 @@ type AlertType = 'info' | 'success' | 'warning' | 'error'
10
10
  interface Props {
11
11
  message?: string
12
12
  thin?: boolean
13
+ /** Border + transparent background (drops the tinted fill). */
13
14
  outline?: boolean
15
+ /** Adds a border while keeping the tinted background. */
16
+ frame?: boolean
14
17
  dismissable?: boolean
15
18
  type?: AlertType
16
19
  /** Boolean shorthands: <Alert error>...</Alert> */
@@ -42,7 +45,7 @@ const typeIcon: Record<AlertType, IconType> = {
42
45
  </script>
43
46
 
44
47
  <template>
45
- <div v-if="!isDismissed" class="alert" :class="[computedType, { thin, outline }]" :dismissable="dismissable">
48
+ <div v-if="!isDismissed" class="alert" :class="[computedType, { thin, outline, frame }]" :dismissable="dismissable">
46
49
  <Icon v-if="icon !== 'none'" class="alert_icon" :icon="icon || typeIcon[computedType]" :size="1.7" />
47
50
  <slot>
48
51
  <p class="m-0">
@@ -100,6 +103,11 @@ const typeIcon: Record<AlertType, IconType> = {
100
103
  background: unset;
101
104
  }
102
105
 
106
+ /* frame: border in the alert's accent color, keeps the tinted background. */
107
+ .alert.frame {
108
+ border: 1px solid var(--alert-outline);
109
+ }
110
+
103
111
  .alert_icon {
104
112
  line-height: 1;
105
113
  }
@@ -1,8 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  defineOptions({ name: 'BglBadge' })
3
- import type { IconType, ThemeType } from '@bagelink/vue'
3
+ import type { IconType, ThemeType, GradientProp, GradientDirProp } from '@bagelink/vue'
4
4
  import type { SetupContext } from 'vue'
5
- import { Btn, Icon } from '@bagelink/vue'
5
+ import { Btn, Icon, useGradientVariant } from '@bagelink/vue'
6
6
  import { computed, useSlots } from 'vue'
7
7
  import '../styles/base-colors.css'
8
8
 
@@ -17,10 +17,25 @@ const props = defineProps<{
17
17
  icon?: IconType
18
18
  iconEnd?: IconType
19
19
  color?: ThemeType
20
- variant?: 'solid' | 'flat' | 'outline' | 'glass'
20
+ variant?: 'solid' | 'flat' | 'outline' | 'glass' | 'soft' | 'frost'
21
21
  /** Boolean variant shorthands: <Badge flat /> — same as variant="flat" */
22
22
  flat?: boolean
23
23
  outline?: boolean
24
+ /** Soft variant: light tinted bg + full-color text & border, driven by `color`.
25
+ Shorthand for variant="soft". e.g. <Badge color="blue" soft /> */
26
+ soft?: boolean
27
+ /** Frost variant: translucent + blur, tinted by `color`. Reads beautifully
28
+ over photos / gradients / dark heroes. Shorthand for variant="frost". */
29
+ frost?: boolean
30
+ /** Gradient fill (white text). Boolean = auto (a darker shade of `color`);
31
+ or a space-separated tone list — "purple" pairs with `color`
32
+ (blue→purple), "blue purple pink" defines all stops. */
33
+ gradient?: GradientProp
34
+ /** Gradient direction: a named direction ("to-br") or an angle in degrees
35
+ (45 or "45"). Defaults to 135deg. */
36
+ gradientDir?: GradientDirProp
37
+ /** Solid fill + a hairline border (keeps the background, unlike `outline`). */
38
+ frame?: boolean
24
39
  /** Translucent frosted badge — readable on photos / gradients / dark heroes.
25
40
  Shorthand for variant="glass". */
26
41
  glass?: boolean
@@ -49,9 +64,19 @@ const computedTheme = computed(
49
64
  },
50
65
  )
51
66
 
67
+ const { isGradient, gradientStyle } = useGradientVariant({
68
+ gradient: () => props.gradient,
69
+ gradientDir: () => props.gradientDir,
70
+ color: () => props.color,
71
+ })
72
+
52
73
  const computedPairClass = computed(() => {
53
74
  const theme = computedTheme.value
54
- if (!theme) return 'pair-primary'
75
+ if (!theme) {
76
+ // Frost defaults to a white (light-on-dark) glass when no color is given.
77
+ if (computedVariant.value === 'frost') { return 'pair-white' }
78
+ return 'pair-primary'
79
+ }
55
80
  return `pair-${theme}`
56
81
  })
57
82
 
@@ -64,7 +89,10 @@ const computedSize = computed(() => {
64
89
 
65
90
  const computedVariant = computed(() => {
66
91
  if (props.variant) { return props.variant }
92
+ if (isGradient.value) { return 'gradient' }
93
+ if (props.frost) { return 'frost' }
67
94
  if (props.glass) { return 'glass' }
95
+ if (props.soft) { return 'soft' }
68
96
  if (props.flat) { return 'flat' }
69
97
  if (props.outline || props.border) { return 'outline' }
70
98
  return 'solid'
@@ -76,7 +104,11 @@ const computedClasses = computed(() => {
76
104
  'round': props.round,
77
105
  'bgl_flatPill': computedVariant.value === 'flat',
78
106
  'bgl_pill-border': computedVariant.value === 'outline',
107
+ 'bgl_pill-frame': props.frame,
79
108
  'bgl_glassPill': computedVariant.value === 'glass',
109
+ 'soft': computedVariant.value === 'soft',
110
+ 'frost': computedVariant.value === 'frost',
111
+ 'gradient': computedVariant.value === 'gradient',
80
112
  'pillLarge': computedSize.value === 'lg',
81
113
  'pillSmall': computedSize.value === 'sm',
82
114
  }
@@ -91,7 +123,7 @@ const computedClasses = computed(() => {
91
123
  <template>
92
124
  <div
93
125
  class="bgl_pill"
94
- style="height: var(--bgl-pill-height);"
126
+ :style="[{ height: 'var(--bgl-pill-height)' }, gradientStyle]"
95
127
  :disabled="disabled"
96
128
  :class="computedClasses"
97
129
  >
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
- import type { IconType, ExtendedThemeType, TranslatableString } from '@bagelink/vue'
2
+ import type { IconType, ExtendedThemeType, TranslatableString, GradientProp, GradientDirProp } from '@bagelink/vue'
3
3
  import type { SetupContext } from 'vue'
4
- import { Icon, Loading, useDialog, useI18n, resolveI18n, MOBILE_BREAKPOINT } from '@bagelink/vue'
4
+ import { Icon, Loading, useDialog, useI18n, resolveI18n, useGradientVariant, MOBILE_BREAKPOINT } from '@bagelink/vue'
5
5
  import { useSlots, ref, onMounted, onUnmounted, computed } from 'vue'
6
6
  import { RouterLink } from 'vue-router'
7
7
  defineOptions({ name: 'BglBtn' })
@@ -13,10 +13,25 @@ const props = withDefaults(
13
13
  iconSize?: number | string
14
14
  iconMobileSize?: number | string
15
15
  color?: ExtendedThemeType
16
- variant?: 'solid' | 'flat' | 'outline'
16
+ variant?: 'solid' | 'flat' | 'outline' | 'soft' | 'frost'
17
17
  /** Boolean variant shorthands: <Btn flat /> — same as variant="flat" */
18
18
  flat?: boolean
19
19
  outline?: boolean
20
+ /** Soft variant: light tinted bg + full-color text & border, driven by `color`.
21
+ Shorthand for variant="soft". e.g. <Btn color="blue" soft /> */
22
+ soft?: boolean
23
+ /** Frost variant: translucent + blur, tinted by `color`. Reads beautifully
24
+ over photos / gradients / dark heroes. Shorthand for variant="frost". */
25
+ frost?: boolean
26
+ /** Gradient fill (white text). Boolean = auto (a darker shade of `color`);
27
+ or a space-separated tone list — "purple" pairs with `color`
28
+ (blue→purple), "blue purple pink" defines all stops. */
29
+ gradient?: GradientProp
30
+ /** Gradient direction: a named direction ("to-br") or an angle in degrees
31
+ (45 or "45"). Defaults to 135deg. */
32
+ gradientDir?: GradientDirProp
33
+ /** Solid fill + a hairline border (keeps the background, unlike `outline`). */
34
+ frame?: boolean
20
35
  /** @deprecated Use `outline` */
21
36
  border?: boolean
22
37
  thin?: boolean
@@ -67,17 +82,41 @@ const emit = defineEmits<{
67
82
 
68
83
  const { $t } = useI18n()
69
84
 
85
+ const { isGradient, gradientStyle } = useGradientVariant({
86
+ gradient: () => props.gradient,
87
+ gradientDir: () => props.gradientDir,
88
+ color: () => props.color,
89
+ })
90
+
70
91
  const computedVariant = computed(() => {
71
92
  if (props.variant) { return props.variant }
72
- if (props.flat) { return 'flat' }
93
+ if (isGradient.value) { return 'gradient' }
94
+ if (props.frost) { return 'frost' }
95
+ if (props.soft) { return 'soft' }
96
+ // An explicit `outline`/`border` wins over `flat` — components like Dropdown
97
+ // default the trigger to `flat`, so `<Dropdown outline>` must still resolve to
98
+ // the outline variant rather than being swallowed by the inherited flat.
73
99
  if (props.outline || props.border) { return 'outline' }
100
+ // `frame` is "solid fill + a border", so it also overrides an inherited
101
+ // `flat` (e.g. Dropdown's flat trigger) to restore the solid background.
102
+ if (props.frame) { return 'solid' }
103
+ if (props.flat) { return 'flat' }
74
104
  return 'solid'
75
105
  })
76
106
 
77
107
  const computedPairClass = computed(() => {
78
108
  const theme = props.color
79
109
  if (!theme) {
80
- // Only flat/outline buttons get a default for visibility
110
+ // Frost defaults to a white (light-on-dark) glass when no color is given.
111
+ if (computedVariant.value === 'frost') {
112
+ return 'pair-white'
113
+ }
114
+ // Gradient with explicit stops (e.g. gradient="blue purple") needs a pair
115
+ // class only to activate `.gradient`; default to primary for the auto case.
116
+ if (computedVariant.value === 'gradient') {
117
+ return 'pair-primary'
118
+ }
119
+ // Only flat/outline/soft buttons get a default for visibility
81
120
  if (computedVariant.value !== 'solid') {
82
121
  return 'pair-black'
83
122
  }
@@ -181,7 +220,11 @@ const slots: SetupContext['slots'] = useSlots()
181
220
  round,
182
221
  'bgl_flatPill': computedVariant === 'flat',
183
222
  'bgl_pill-border': computedVariant === 'outline',
184
- }, computedPairClass]" :tabindex="disabled ? -1 : 0" @click.stop="handleClick" @keydown.enter="handleClick" @keydown.space="handleClick"
223
+ 'soft': computedVariant === 'soft',
224
+ 'frost': computedVariant === 'frost',
225
+ 'gradient': computedVariant === 'gradient',
226
+ 'bgl_pill-frame': frame,
227
+ }, computedPairClass]" :style="gradientStyle" :tabindex="disabled ? -1 : 0" @click.stop="handleClick" @keydown.enter="handleClick" @keydown.space="handleClick"
185
228
  >
186
229
  <Loading v-if="loading" class="h-100p" size="15" color="currentColor" />
187
230
  <div v-else class="bgl_btn-flex">
@@ -49,7 +49,7 @@ const is = computed(() => {
49
49
  <component
50
50
  :is="is" v-ripple="!!to" v-bind="bind" class="bgl_card" :class="{
51
51
  thin,
52
- 'border': outline,
52
+ 'border bg-transparent': outline,
53
53
  'h-100': h100,
54
54
  [bg || '']: bg,
55
55
  'overflow-x': overflowX,
@@ -57,7 +57,7 @@ const is = computed(() => {
57
57
  'card_frame': frame,
58
58
  }"
59
59
  >
60
- <span v-if="label" class="card_label">
60
+ <span v-if="label" class="card_label block label">
61
61
  {{ label }}
62
62
  </span>
63
63
  <!-- Header row: title + optional trailing action. Full override via #header. -->
@@ -76,15 +76,6 @@ const is = computed(() => {
76
76
  .card_frame {
77
77
  border: 1px solid var(--bgl-border-color);
78
78
  }
79
- .card_label {
80
- font-size: 1rem;
81
- position: relative;
82
- top: -0.5rem;
83
- padding: 0.75rem 0;
84
- display: block;
85
- border-bottom: 1px solid var(--bgl-border-color);
86
- margin-bottom: 1rem;
87
- }
88
79
 
89
80
  /* Header row (title + action). Pulls out to the card edges via negative margins
90
81
  so its divider spans full-width regardless of the card's own padding, then
@@ -99,20 +90,6 @@ min-height: 0;
99
90
  font-weight: 600;
100
91
  }
101
92
 
102
- .border .card_label {
103
- font-size: 0.7rem;
104
- font-weight: 300;
105
- background: var(--bgl-box-bg);
106
- padding: 0 0.75rem;
107
- position: absolute;
108
- top: -0.5rem;
109
- inset-inline-start: 1rem;
110
- border-left: 1px solid var(--bgl-border-color);
111
- border-right: 1px solid var(--bgl-border-color);
112
- border-bottom: unset;
113
-
114
- }
115
-
116
93
  .bgl_card {
117
94
  --bgl-card-pad: 2rem;
118
95
  border-radius: var(--bgl-card-border-radius);
@@ -125,11 +102,6 @@ position: relative;
125
102
  background: var(--bgl-gray-tint);
126
103
  }
127
104
 
128
- .bgl_card.border {
129
- border: 1px solid var(--bgl-border-color);
130
- background-color: transparent;
131
- }
132
-
133
105
  .bgl_card.thin {
134
106
  --bgl-card-pad: 1rem;
135
107
  padding: var(--bgl-card-pad);
@@ -27,6 +27,8 @@ const props = withDefaults(defineProps<{
27
27
  iconEnd?: IconType
28
28
  border?: boolean
29
29
  outline?: boolean
30
+ /** Solid fill + a hairline border on the trigger (keeps the background, unlike `outline`). */
31
+ frame?: boolean
30
32
  round?: boolean
31
33
  placement?: AlignedPlacement
32
34
  disablePlacement?: boolean
@@ -386,7 +388,7 @@ defineExpose({ show, hide, shown })
386
388
  >
387
389
  <slot name="trigger" :show :hide :shown>
388
390
  <Btn
389
- :class="triggerClass" :iconEnd :icon="iconSet" :value :thin :flat :outline :round :color
391
+ :class="triggerClass" :iconEnd :icon="iconSet" :value :thin :flat :outline :frame :round :color
390
392
  :disabled @click="onTriggerClick"
391
393
  />
392
394
  </slot>
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
  import type { IconType, ThemeType } from '@bagelink/vue'
3
3
  import { Avatar, Icon } from '@bagelink/vue'
4
- import { computed } from 'vue'
4
+ import { computed, useAttrs } from 'vue'
5
+
6
+ defineOptions({ inheritAttrs: false })
5
7
 
6
8
  const props = withDefaults(
7
9
  defineProps<{
@@ -31,9 +33,8 @@ const props = withDefaults(
31
33
  /** Visually mark as selected (for non-router selection lists). */
32
34
  active?: boolean
33
35
  /** Render as an interactive row (cursor + hover) without needing a handler.
34
- Implied when `to`, `href`, or `onClick` is set. */
36
+ Implied when `to`, `href`, or a `@click` listener is set. */
35
37
  clickable?: boolean
36
- onClick?: () => void
37
38
  }>(),
38
39
  {
39
40
  ellipsis: true,
@@ -41,9 +42,34 @@ const props = withDefaults(
41
42
  }
42
43
  )
43
44
 
45
+ const attrs = useAttrs()
46
+
47
+ // Split inherited attrs: event listeners (onClick, onClickStop, …) belong on the
48
+ // clickable element so handlers + modifiers like `.stop` fire on the actual
49
+ // button/link row; everything else (class, style, data-*, …) stays on the root
50
+ // wrapper to preserve existing behavior.
51
+ const listenerAttrs = computed(() => {
52
+ const out: Record<string, any> = {}
53
+ for (const k of Object.keys(attrs)) {
54
+ if (/^on[A-Z]/.test(k)) { out[k] = (attrs as any)[k] }
55
+ }
56
+ return out
57
+ })
58
+ const rootAttrs = computed(() => {
59
+ const out: Record<string, any> = {}
60
+ for (const k of Object.keys(attrs)) {
61
+ if (!/^on[A-Z]/.test(k)) { out[k] = (attrs as any)[k] }
62
+ }
63
+ return out
64
+ })
65
+
44
66
  const hasTo = computed(() => props.to !== undefined && props.to !== '')
45
67
  const hasHref = computed(() => props.href !== undefined && props.href !== '')
46
- const isClickable = computed(() => hasTo.value || hasHref.value || props.onClick !== undefined || props.clickable === true)
68
+ // A `@click` listener (with or without modifiers like `.stop`) shows up in
69
+ // $attrs as `onClick` / `onClickStop` / etc — detect any of them so a clickable
70
+ // row is recognized even when the handler uses modifiers.
71
+ const hasClickListener = computed(() => Object.keys(attrs).some(k => /^onClick/i.test(k)))
72
+ const isClickable = computed(() => hasTo.value || hasHref.value || hasClickListener.value || props.clickable === true)
47
73
 
48
74
  const isComponent = computed(() => {
49
75
  if (hasTo.value) { return 'router-link' }
@@ -53,7 +79,10 @@ const isComponent = computed(() => {
53
79
  })
54
80
 
55
81
  const bind = computed(() => {
56
- const obj: { [key: string]: any } = {}
82
+ // Spread the inherited event listeners (including `@click`/`@click.stop`) onto
83
+ // the clickable element itself, so the handler fires on the actual button/link
84
+ // row — not the outer wrapper — and modifiers like `.stop` keep working.
85
+ const obj: { [key: string]: any } = { ...listenerAttrs.value }
57
86
  if (props.to !== undefined && props.to !== '') { obj.to = props.to }
58
87
  else if (props.href !== undefined && props.href !== '') { obj.href = props.href }
59
88
  if (props.target !== undefined && props.target !== undefined && ((props.to !== undefined && props.to !== '') || (props.href !== undefined && props.href !== ''))) { obj.target = props.target }
@@ -69,6 +98,7 @@ const bind = computed(() => {
69
98
 
70
99
  <template>
71
100
  <div
101
+ v-bind="rootAttrs"
72
102
  class="flex space-between list-item-row"
73
103
  :class="{ 'no-border-list': props.flat || rounded, 'list-item-flush': props.fullWidth, 'list-item-fullrow': isClickable, 'list-item-rounded': rounded, 'list-item-active': active }"
74
104
  :style="color ? { '--bgl-list-item-accent': `var(--bgl-${color})` } : undefined"
@@ -86,7 +116,7 @@ const bind = computed(() => {
86
116
  'py-05': props.thin,
87
117
  'px-1': !props.fullWidth,
88
118
  'px-0': props.fullWidth,
89
- }" @click="typeof onClick === 'function' && onClick()"
119
+ }"
90
120
  >
91
121
  <!-- Leading visual INSIDE the clickable area. Use #media for custom art
92
122
  (covers, thumbnails); `src`/`showAvatar` and `icon` are shortcuts. -->
@@ -23,6 +23,8 @@ const props = defineProps<{
23
23
  alignTxt?: 'center' | 'start' | 'end'
24
24
  alignTxtMobile?: 'center' | 'start' | 'end'
25
25
  outline?: boolean
26
+ /** Adds a border around the tabs wrapper (keeps the background). */
27
+ frame?: boolean
26
28
  }>()
27
29
 
28
30
  const emit = defineEmits(['update:modelValue'])
@@ -140,7 +142,7 @@ onBeforeUnmount(() => {
140
142
  </script>
141
143
 
142
144
  <template>
143
- <div ref="tabsWrap" class="grid auto-flow-columns relative fit-content bgl_tabs_wrap overflow-hidden" :class="{ 'bgl_flat-tabs': flat, 'bgl_vertical-tabs': vertical, 'outline': outline }">
145
+ <div ref="tabsWrap" class="grid auto-flow-columns relative fit-content bgl_tabs_wrap overflow-hidden" :class="{ 'bgl_flat-tabs': flat, 'bgl_vertical-tabs': vertical, 'bgl_tabs-outline': outline, 'bgl_tabs-frame': frame }">
144
146
  <slot name="tabs" v-bind="{ selectTab, isActive, tabLabel, tabs: tabEls }">
145
147
  <button v-for="(tab, i) in props.tabs" :key="i" type="button" :class="[
146
148
  { active: isActive(tab) },
@@ -200,6 +202,18 @@ border-radius: calc(var(--bgl_tabs-border-radius) * 1.4);
200
202
  gap: var(--bgl_tabs-gap);
201
203
  }
202
204
 
205
+ /* outline: border + drop the filled background & shadow (transparent shell). */
206
+ .bgl_tabs-outline.bgl_tabs_wrap {
207
+ border: 1px solid var(--bgl-border-color);
208
+ background: transparent;
209
+ box-shadow: none;
210
+ }
211
+
212
+ /* frame: border on top of the normal filled background (keeps bg & shadow). */
213
+ .bgl_tabs-frame.bgl_tabs_wrap {
214
+ border: 1px solid var(--bgl-border-color);
215
+ }
216
+
203
217
  .bgl_tab {
204
218
  border: none;
205
219
  background: transparent;
@@ -8,6 +8,8 @@ export { useAddToCalendar } from './useAddToCalendar'
8
8
  export { useDevice } from './useDevice'
9
9
  export { useEscapeKey } from './useEscapeKey'
10
10
  export { useExcel } from './useExcel'
11
+ export type { GradientDir, GradientDirProp, GradientProp } from './useGradientVariant'
12
+ export { useGradientVariant } from './useGradientVariant'
11
13
  export { useLocalStore } from './useLocalStore'
12
14
  export { usePolling } from './usePolling'
13
15
  export { useQuery } from './useQuery'
@@ -0,0 +1,100 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import { computed, toValue } from 'vue'
3
+
4
+ /** The 8 named directions, mirroring the `.to-*` utilities in gradients.css. */
5
+ export type GradientDir
6
+ = 'to-t' | 'to-b' | 'to-l' | 'to-r' | 'to-tl' | 'to-tr' | 'to-bl' | 'to-br'
7
+
8
+ /** `gradient` prop: boolean (auto from `color`) or a space-separated tone list,
9
+ * e.g. "purple" (2-stop with `color`) or "blue purple pink" (full control). */
10
+ export type GradientProp = boolean | string
11
+
12
+ /** `gradient-dir` prop: a named direction, or an angle in degrees (number or
13
+ * numeric string). Undefined → CSS default (135deg). */
14
+ export type GradientDirProp = GradientDir | number | string
15
+
16
+ const DIR_MAP: Record<GradientDir, string> = {
17
+ 'to-t': 'to top',
18
+ 'to-b': 'to bottom',
19
+ 'to-l': 'to left',
20
+ 'to-r': 'to right',
21
+ 'to-tl': 'to top left',
22
+ 'to-tr': 'to top right',
23
+ 'to-bl': 'to bottom left',
24
+ 'to-br': 'to bottom right',
25
+ }
26
+
27
+ /** Resolve a gradient-dir value to a CSS angle/keyword, or undefined for default. */
28
+ function resolveAngle(dir: GradientDirProp | undefined): string | undefined {
29
+ if (dir == null || dir === '') { return undefined }
30
+ if (typeof dir === 'string' && dir in DIR_MAP) {
31
+ return DIR_MAP[dir as GradientDir]
32
+ }
33
+ const n = Number(dir)
34
+ return Number.isFinite(n) ? `${n}deg` : undefined
35
+ }
36
+
37
+ /** Split a tone string ("blue purple pink") into individual tone tokens. */
38
+ function parseTones(value: GradientProp | undefined): string[] {
39
+ if (typeof value !== 'string') { return [] }
40
+ return value.trim().split(/\s+/).filter(Boolean)
41
+ }
42
+
43
+ interface UseGradientVariantArgs {
44
+ /** The `gradient` prop value. */
45
+ gradient: MaybeRefOrGetter<GradientProp | undefined>
46
+ /** The `gradient-dir` prop value. */
47
+ gradientDir?: MaybeRefOrGetter<GradientDirProp | undefined>
48
+ /** The component's `color` prop — used as the first stop when present. */
49
+ color?: MaybeRefOrGetter<string | undefined>
50
+ }
51
+
52
+ /**
53
+ * Shared logic for the `gradient` variant across Btn / Badge (and anything else).
54
+ *
55
+ * Stop resolution:
56
+ * - `color` (if set) is the first stop, followed by tones from `gradient`.
57
+ * - With no `color`, all stops come from `gradient`.
58
+ * - A single resolved stop (boolean `gradient`, or one tone) lets the CSS
59
+ * `.gradient` modifier auto-derive a darker second stop — so we inject
60
+ * nothing and rely on its `--bgl-grad-default-*` fallback.
61
+ *
62
+ * Returns the inline CSS custom properties to bind via `:style`. Named direction
63
+ * / via / extra stops authored as gradients.css classes still compose on top.
64
+ */
65
+ export function useGradientVariant({ gradient, gradientDir, color }: UseGradientVariantArgs) {
66
+ /** Whether the gradient variant is active at all. */
67
+ const isGradient = computed(() => {
68
+ const g = toValue(gradient)
69
+ return g === true || (typeof g === 'string' && g.trim() !== '')
70
+ })
71
+
72
+ const stops = computed(() => {
73
+ const c = toValue(color)
74
+ const tones = parseTones(toValue(gradient))
75
+ return [c, ...tones].filter(Boolean) as string[]
76
+ })
77
+
78
+ const gradientStyle = computed<Record<string, string>>(() => {
79
+ if (!isGradient.value) { return {} }
80
+
81
+ const style: Record<string, string> = {}
82
+ const angle = resolveAngle(toValue(gradientDir))
83
+ if (angle) { style['--bgl-grad-angle'] = angle }
84
+
85
+ const s = stops.value
86
+ // 0–1 stops → let the CSS auto-derive a darker second stop. Inject nothing.
87
+ if (s.length < 2) { return style }
88
+
89
+ style['--bgl-grad-from'] = `var(--bgl-${s[0]})`
90
+ style['--bgl-grad-to'] = `var(--bgl-${s[s.length - 1]})`
91
+ // Middle stops become the `via` slot (comma-terminated, like gradients.css).
92
+ const middle = s.slice(1, -1)
93
+ if (middle.length) {
94
+ style['--bgl-grad-via'] = `${middle.map(t => `var(--bgl-${t})`).join(', ')}, `
95
+ }
96
+ return style
97
+ })
98
+
99
+ return { isGradient, gradientStyle, stops }
100
+ }
@@ -34,6 +34,7 @@
34
34
  @import "mobileColors.css";
35
35
  @import "appearance.css";
36
36
  @import "gradients.css";
37
+ @import "color-variants.css";
37
38
 
38
39
  /* Icon font-family bindings — mirrored from Icon.vue so icons work even when
39
40
  that component's injected <style> is not yet present (e.g. Vite dev mode). */
@@ -1514,6 +1514,15 @@
1514
1514
  outline: 1px solid;
1515
1515
  }
1516
1516
 
1517
+ /* frame: keeps the solid fill but adds a hairline border on top.
1518
+ Unlike .bgl_pill-border (outline variant) it does NOT clear the background.
1519
+ The border tracks the pill's own tone (falling back to the neutral border
1520
+ color) so a filled primary button gets a primary-toned edge, not a stray
1521
+ grey ring that reads like an outline. */
1522
+ .bgl_pill-frame {
1523
+ border: 1px solid color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-border-color)) 55%, transparent);
1524
+ }
1525
+
1517
1526
  /* Base colors flat/border */
1518
1527
  .pair-blue.bgl_flatPill,
1519
1528
  .pair-blue.bgl_pill-border {