@bagelink/vue 1.15.73 → 1.15.78

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 (35) 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/form/inputs/CodeEditor/Index.vue.d.ts +1 -1
  13. package/dist/components/form/inputs/SelectInput.vue.d.ts +6 -0
  14. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  15. package/dist/components/layout/TabsNav.vue.d.ts +2 -0
  16. package/dist/components/layout/TabsNav.vue.d.ts.map +1 -1
  17. package/dist/composables/index.d.ts +2 -0
  18. package/dist/composables/index.d.ts.map +1 -1
  19. package/dist/composables/useGradientVariant.d.ts +37 -0
  20. package/dist/composables/useGradientVariant.d.ts.map +1 -0
  21. package/dist/index.cjs +47 -47
  22. package/dist/index.mjs +6217 -6142
  23. package/dist/style.css +1 -1
  24. package/package.json +1 -1
  25. package/src/components/Alert.vue +9 -1
  26. package/src/components/Badge.vue +37 -5
  27. package/src/components/Btn.vue +49 -6
  28. package/src/components/Card.vue +2 -30
  29. package/src/components/Dropdown.vue +3 -1
  30. package/src/components/layout/TabsNav.vue +15 -1
  31. package/src/composables/index.ts +2 -0
  32. package/src/composables/useGradientVariant.ts +100 -0
  33. package/src/styles/bagel.css +1 -0
  34. package/src/styles/base-colors.css +9 -0
  35. package/src/styles/color-variants.css +158 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.73",
4
+ "version": "1.15.78",
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>
@@ -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 {
@@ -0,0 +1,158 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────────
2
+ * COLOR VARIANTS — soft (+ future: frost / gradient)
3
+ *
4
+ * These are *modifiers* that compose on top of any `.pair-{color}` class, the
5
+ * same way `.bgl_flatPill` / `.bgl_pill-border` already do. Components keep
6
+ * computing `pair-${color}` and simply add `.soft` — nothing about the existing
7
+ * color system changes.
8
+ *
9
+ * SOFT = light tinted background + full-color text + full-color border.
10
+ * Equivalent to: bg-{color}-30 color-{color} border border-{color}
11
+ *
12
+ * To stay DRY we expose one variable per tone — `--bgl-pair-tone` — set once for
13
+ * every shade of a color, then `.soft` derives bg / text / border from it via
14
+ * color-mix(). One rule covers all 13 tones × all 13 shades.
15
+ * ──────────────────────────────────────────────────────────────────────────── */
16
+
17
+ /* 1) Map every pair tone (base + 10..130 + light/tint/dark + semantic) to a
18
+ * single --bgl-pair-tone custom property. */
19
+ .pair-primary, .pair-default,
20
+ .pair-primary-light, .pair-primary-tint,
21
+ .pair-primary-10, .pair-primary-20, .pair-primary-30, .pair-primary-40, .pair-primary-50,
22
+ .pair-primary-60, .pair-primary-70, .pair-primary-80, .pair-primary-90, .pair-primary-100,
23
+ .pair-primary-110, .pair-primary-120, .pair-primary-130 { --bgl-pair-tone: var(--bgl-primary); }
24
+
25
+ .pair-blue, .pair-info,
26
+ .pair-blue-light, .pair-blue-tint, .pair-blue-dark,
27
+ .pair-blue-10, .pair-blue-20, .pair-blue-30, .pair-blue-40, .pair-blue-50,
28
+ .pair-blue-60, .pair-blue-70, .pair-blue-80, .pair-blue-90, .pair-blue-100,
29
+ .pair-blue-110, .pair-blue-120, .pair-blue-130 { --bgl-pair-tone: var(--bgl-blue); }
30
+
31
+ .pair-green, .pair-success,
32
+ .pair-green-light,
33
+ .pair-green-10, .pair-green-20, .pair-green-30, .pair-green-40, .pair-green-50,
34
+ .pair-green-60, .pair-green-70, .pair-green-80, .pair-green-90, .pair-green-100,
35
+ .pair-green-110, .pair-green-120, .pair-green-130 { --bgl-pair-tone: var(--bgl-green); }
36
+
37
+ .pair-red, .pair-danger,
38
+ .pair-red-light, .pair-red-tint,
39
+ .pair-red-10, .pair-red-20, .pair-red-30, .pair-red-40, .pair-red-50,
40
+ .pair-red-60, .pair-red-70, .pair-red-80, .pair-red-90, .pair-red-100,
41
+ .pair-red-110, .pair-red-120, .pair-red-130 { --bgl-pair-tone: var(--bgl-red); }
42
+
43
+ .pair-yellow, .pair-warning,
44
+ .pair-yellow-light,
45
+ .pair-yellow-10, .pair-yellow-20, .pair-yellow-30, .pair-yellow-40, .pair-yellow-50,
46
+ .pair-yellow-60, .pair-yellow-70, .pair-yellow-80, .pair-yellow-90, .pair-yellow-100,
47
+ .pair-yellow-110, .pair-yellow-120, .pair-yellow-130 { --bgl-pair-tone: var(--bgl-yellow); }
48
+
49
+ .pair-purple,
50
+ .pair-purple-light,
51
+ .pair-purple-10, .pair-purple-20, .pair-purple-30, .pair-purple-40, .pair-purple-50,
52
+ .pair-purple-60, .pair-purple-70, .pair-purple-80, .pair-purple-90, .pair-purple-100,
53
+ .pair-purple-110, .pair-purple-120, .pair-purple-130 { --bgl-pair-tone: var(--bgl-purple); }
54
+
55
+ .pair-brown,
56
+ .pair-brown-light,
57
+ .pair-brown-10, .pair-brown-20, .pair-brown-30, .pair-brown-40, .pair-brown-50,
58
+ .pair-brown-60, .pair-brown-70, .pair-brown-80, .pair-brown-90, .pair-brown-100,
59
+ .pair-brown-110, .pair-brown-120, .pair-brown-130 { --bgl-pair-tone: var(--bgl-brown); }
60
+
61
+ .pair-orange,
62
+ .pair-orange-light,
63
+ .pair-orange-10, .pair-orange-20, .pair-orange-30, .pair-orange-40, .pair-orange-50,
64
+ .pair-orange-60, .pair-orange-70, .pair-orange-80, .pair-orange-90, .pair-orange-100,
65
+ .pair-orange-110, .pair-orange-120, .pair-orange-130 { --bgl-pair-tone: var(--bgl-orange); }
66
+
67
+ .pair-turquoise,
68
+ .pair-turquoise-light,
69
+ .pair-turquoise-10, .pair-turquoise-20, .pair-turquoise-30, .pair-turquoise-40, .pair-turquoise-50,
70
+ .pair-turquoise-60, .pair-turquoise-70, .pair-turquoise-80, .pair-turquoise-90, .pair-turquoise-100,
71
+ .pair-turquoise-110, .pair-turquoise-120, .pair-turquoise-130 { --bgl-pair-tone: var(--bgl-turquoise); }
72
+
73
+ .pair-pink,
74
+ .pair-pink-light,
75
+ .pair-pink-10, .pair-pink-20, .pair-pink-30, .pair-pink-40, .pair-pink-50,
76
+ .pair-pink-60, .pair-pink-70, .pair-pink-80, .pair-pink-90, .pair-pink-100,
77
+ .pair-pink-110, .pair-pink-120, .pair-pink-130 { --bgl-pair-tone: var(--bgl-pink); }
78
+
79
+ .pair-gray,
80
+ .pair-gray-light, .pair-gray-tint,
81
+ .pair-gray-10, .pair-gray-20, .pair-gray-30, .pair-gray-40, .pair-gray-50,
82
+ .pair-gray-60, .pair-gray-70, .pair-gray-80, .pair-gray-90, .pair-gray-100,
83
+ .pair-gray-110, .pair-gray-120, .pair-gray-130 { --bgl-pair-tone: var(--bgl-gray); }
84
+
85
+ .pair-black,
86
+ .pair-black-tint,
87
+ .pair-black-10, .pair-black-20, .pair-black-30, .pair-black-40, .pair-black-50,
88
+ .pair-black-60, .pair-black-70, .pair-black-80, .pair-black-90, .pair-black-100,
89
+ .pair-black-110, .pair-black-120, .pair-black-130 { --bgl-pair-tone: var(--bgl-black); }
90
+
91
+ .pair-white { --bgl-pair-tone: var(--bgl-white); }
92
+
93
+ /* 2) SOFT modifier — applies on any element carrying a pair-* class.
94
+ * bg ≈ 18% tint, border ≈ full tone, text = full tone. */
95
+ [class*="pair-"].soft {
96
+ background-color: color-mix(in srgb, var(--bgl-pair-tone) 18%, transparent) !important;
97
+ color: var(--bgl-pair-tone) !important;
98
+ border: 1px solid var(--bgl-pair-tone) !important;
99
+ }
100
+
101
+ /* White is special: a white tone on a light surface needs dark text/border. */
102
+ .pair-white.soft {
103
+ color: var(--bgl-black) !important;
104
+ border-color: var(--bgl-black) !important;
105
+ }
106
+
107
+ /* 3) FROST modifier — translucent + blur, tinted by the pair color.
108
+ * Like `soft` but glassier: lower-opacity fill, a blurred backdrop, and a
109
+ * faint same-tone border. Shines over photos / gradients / dark heroes.
110
+ * Text uses the full tone so it stays legible on the light translucent fill. */
111
+ [class*="pair-"].frost {
112
+ /* `--bgl-pair-tone` falls back to white so a frost with no color (or before a
113
+ tone is mapped) renders as the classic light-on-dark glass. */
114
+ background-color: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-white)) 14%, transparent) !important;
115
+ color: var(--bgl-pair-tone, var(--bgl-white)) !important;
116
+ border: 1px solid color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-white)) 35%, transparent) !important;
117
+ backdrop-filter: blur(8px);
118
+ -webkit-backdrop-filter: blur(8px);
119
+ }
120
+
121
+ .pair-white.frost {
122
+ color: var(--bgl-white) !important;
123
+ }
124
+
125
+ /* 4) GRADIENT modifier — a colored linear gradient + white text.
126
+ *
127
+ * Works two ways, both pure CSS so it applies to <div> as well as components:
128
+ *
129
+ * a) Automatic (just a pair class): sweeps from the tone to a darker shade of
130
+ * itself. <div class="pair-blue gradient">
131
+ *
132
+ * b) Explicit stops: provide --bgl-grad-from / --bgl-grad-via / --bgl-grad-to
133
+ * (and optionally --bgl-grad-angle). The Btn/Badge components inject these
134
+ * from their `gradient` / `gradient-dir` props, and the existing
135
+ * gradients.css utilities (from-*, via-*, to-*, to-tr, …) compose too.
136
+ *
137
+ * The `via` slot collapses to nothing when unset (2-stop), expands when set
138
+ * (3-stop) — exactly like gradients.css. */
139
+ [class*="pair-"].gradient {
140
+ /* Auto (single-tone) gradient: a vivid sweep from a lighter take on the tone
141
+ to a darker one, giving real depth without going muddy.
142
+
143
+ IMPORTANT: gradients.css declares --bgl-grad-from/to on :root (= transparent),
144
+ so a `var(--bgl-grad-from, fallback)` would never use the fallback. We instead
145
+ *set* --bgl-grad-from/to right here to the auto values. The Btn/Badge
146
+ components override them again via inline style for explicit 2–3 stop
147
+ gradients, and gradients.css `from-*`/`to-*` utilities still win by being
148
+ later/inline. */
149
+ --bgl-grad-from: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-primary)) 90%, #fff);
150
+ --bgl-grad-to: color-mix(in srgb, var(--bgl-pair-tone, var(--bgl-primary)) 55%, #000);
151
+ background-image: linear-gradient(
152
+ var(--bgl-grad-angle, 135deg),
153
+ var(--bgl-grad-from),
154
+ var(--bgl-grad-via, ) var(--bgl-grad-to)
155
+ ) !important;
156
+ color: var(--bgl-white) !important;
157
+ border: none !important;
158
+ }