@bagelink/vue 1.15.207 → 1.15.211

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 (34) hide show
  1. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  2. package/dist/components/Swiper.vue.d.ts +2 -2
  3. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  4. package/dist/components/charts/MapChart.vue.d.ts +4 -0
  5. package/dist/components/charts/MapChart.vue.d.ts.map +1 -1
  6. package/dist/components/dataTable/DataTable.vue.d.ts +4 -2
  7. package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
  8. package/dist/components/dataTable/useTableVirtualization.d.ts +25 -0
  9. package/dist/components/dataTable/useTableVirtualization.d.ts.map +1 -0
  10. package/dist/components/form/inputs/ArrayInput.vue.d.ts +1 -1
  11. package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
  12. package/dist/components/form/inputs/SelectInput.vue.d.ts +1 -1
  13. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/SignaturePad.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/ToggleInput.vue.d.ts +1 -0
  16. package/dist/components/form/inputs/ToggleInput.vue.d.ts.map +1 -1
  17. package/dist/composables/useSchemaField.d.ts.map +1 -1
  18. package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
  19. package/dist/index.cjs +152 -175
  20. package/dist/index.mjs +37955 -31794
  21. package/dist/style.css +2 -1
  22. package/package.json +1 -1
  23. package/src/components/charts/MapChart.vue +191 -24
  24. package/src/components/dataTable/DataTable.vue +48 -19
  25. package/src/components/form/inputs/ArrayInput.vue +3 -3
  26. package/src/components/form/inputs/EmailInput.vue +1 -1
  27. package/src/components/form/inputs/NumberInput.vue +1 -1
  28. package/src/components/form/inputs/SelectInput.vue +3 -3
  29. package/src/components/form/inputs/SignaturePad.vue +0 -7
  30. package/src/components/form/inputs/TextInput.vue +1 -1
  31. package/src/components/form/inputs/ToggleInput.vue +8 -0
  32. package/src/composables/useSchemaField.ts +13 -0
  33. package/src/form-flow/FormFlow.vue +9 -4
  34. package/src/styles/inputs.css +18 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.207",
4
+ "version": "1.15.211",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -5,21 +5,38 @@
5
5
  *
6
6
  * <MapChart :data="[{ id: 'US', value: 4200 }, { id: 'IL', label: 'Israel', value: 900 }]" />
7
7
  * <MapChart :data="rows" color="green" :height="280" />
8
+ * <MapChart :data="rows" :zoomable="false" />
8
9
  */
9
10
  import { computed, ref } from 'vue'
10
11
  import type { ThemeType } from '../../types'
12
+ import Btn from '../Btn.vue'
11
13
  import ChartTooltip from './ChartTooltip.vue'
12
14
  import { formatValue, type ValueFormat } from './core/format'
13
15
  import { resolveColor } from './core/palette'
14
16
  import { useChartAnim } from './core/useChartAnim'
15
17
  import { tipFollowStyle, trackLocalPointer } from './core/useChartTip'
16
- import { WORLD_NAMES, WORLD_PATHS, WORLD_VIEWBOX } from './core/world-paths'
18
+ import { WORLD_NAMES, WORLD_PATHS } from './core/world-paths'
17
19
  import type { MapDatum } from './mapTypes'
18
20
 
19
21
  defineOptions({ name: 'BglMapChart' })
20
22
 
21
23
  export type { MapDatum }
22
24
 
25
+ /**
26
+ * Crop empty ocean padding from Natural Earth sphere-fit (paths leave ~16%
27
+ * empty at the bottom and thin side margins inside 960×480).
28
+ */
29
+ const MAP_VIEW = {
30
+ x: 16,
31
+ y: 0,
32
+ width: 928,
33
+ height: 412,
34
+ } as const
35
+
36
+ const MIN_ZOOM = 1
37
+ const MAX_ZOOM = 8
38
+ const ZOOM_STEP = 1.35
39
+
23
40
  const props = withDefaults(defineProps<{
24
41
  data: MapDatum[]
25
42
  /** Base tone for the value ramp (low → high intensity). */
@@ -27,6 +44,8 @@ const props = withDefaults(defineProps<{
27
44
  height?: number
28
45
  /** Show min→max legend strip under the map. */
29
46
  legend?: boolean
47
+ /** Wheel + button zoom / drag-to-pan. */
48
+ zoomable?: boolean
30
49
  currency?: string
31
50
  prefix?: string
32
51
  suffix?: string
@@ -35,10 +54,12 @@ const props = withDefaults(defineProps<{
35
54
  color: 'primary',
36
55
  height: 280,
37
56
  legend: true,
57
+ zoomable: true,
38
58
  animated: true,
39
59
  })
40
60
 
41
61
  const el = ref<HTMLElement>()
62
+ const svgEl = ref<SVGSVGElement>()
42
63
  const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
43
64
 
44
65
  const fmt = computed<ValueFormat>(() => ({
@@ -64,8 +85,8 @@ const byId = computed(() => {
64
85
  const maxValue = computed(() => Math.max(0, ...[...byId.value.values()].map(d => d.value)))
65
86
 
66
87
  const baseColor = computed(() => resolveColor(props.color))
67
- const emptyFill = 'color-mix(in srgb, var(--bgl-gray) 12%, var(--bgl-box-bg))'
68
- const strokeColor = 'color-mix(in srgb, var(--bgl-gray) 28%, var(--bgl-box-bg))'
88
+ const emptyFill = 'var(--bgl-bg)'
89
+ const strokeColor = 'var(--bgl-border-color)'
69
90
 
70
91
  /** Continuous ramp: empty → light tint → full base color by value share. */
71
92
  function fillFor(value: number | undefined): string {
@@ -103,12 +124,17 @@ const hoverId = ref<string | null>(null)
103
124
  const tipX = ref(0)
104
125
  const tipY = ref(0)
105
126
 
106
- function onEnter(id: string) { hoverId.value = id }
127
+ function onEnter(id: string) {
128
+ if (dragging.value) return
129
+ hoverId.value = id
130
+ }
107
131
  function onLeave() { hoverId.value = null }
108
- function onMove(e: MouseEvent) { trackLocalPointer(e, tipX, tipY) }
132
+ function onMove(e: MouseEvent) {
133
+ if (!dragging.value) trackLocalPointer(e, tipX, tipY)
134
+ }
109
135
 
110
136
  const hover = computed(() => {
111
- if (!hoverId.value) return null
137
+ if (!hoverId.value || dragging.value) return null
112
138
  const c = countries.value.find(x => x.id === hoverId.value)
113
139
  return c?.hasData ? c : null
114
140
  })
@@ -121,41 +147,178 @@ const tooltipStyle = tipFollowStyle(tipX, tipY)
121
147
 
122
148
  const legendMin = computed(() => formatValue(0, fmt.value))
123
149
  const legendMax = computed(() => formatValue(maxValue.value, fmt.value))
150
+
151
+ // ── Zoom / pan ──────────────────────────────────────────────────────────────
152
+ const scale = ref(1)
153
+ const tx = ref(0)
154
+ const ty = ref(0)
155
+ const dragging = ref(false)
156
+ const dragOrigin = ref<{ x: number; y: number; tx: number; ty: number } | null>(null)
157
+
158
+ const mapTransform = computed(() =>
159
+ `translate(${tx.value} ${ty.value}) scale(${scale.value})`,
160
+ )
161
+
162
+ const isZoomed = computed(() => scale.value > MIN_ZOOM + 0.001)
163
+
164
+ function clamp(n: number, min: number, max: number) {
165
+ return Math.min(max, Math.max(min, n))
166
+ }
167
+
168
+ /** Client → SVG user coords (cropped viewBox space). */
169
+ function clientToSvg(clientX: number, clientY: number) {
170
+ const svg = svgEl.value
171
+ if (!svg) return { x: 0, y: 0 }
172
+ const pt = svg.createSVGPoint()
173
+ pt.x = clientX
174
+ pt.y = clientY
175
+ const ctm = svg.getScreenCTM()?.inverse()
176
+ if (!ctm) return { x: 0, y: 0 }
177
+ const p = pt.matrixTransform(ctm)
178
+ return { x: p.x, y: p.y }
179
+ }
180
+
181
+ function zoomAt(clientX: number, clientY: number, next: number) {
182
+ const newScale = clamp(next, MIN_ZOOM, MAX_ZOOM)
183
+ if (newScale === scale.value) return
184
+ const { x, y } = clientToSvg(clientX, clientY)
185
+ const k = newScale / scale.value
186
+ tx.value = x - (x - tx.value) * k
187
+ ty.value = y - (y - ty.value) * k
188
+ scale.value = newScale
189
+ if (newScale <= MIN_ZOOM) {
190
+ tx.value = 0
191
+ ty.value = 0
192
+ scale.value = MIN_ZOOM
193
+ }
194
+ }
195
+
196
+ function onWheel(e: WheelEvent) {
197
+ if (!props.zoomable) return
198
+ e.preventDefault()
199
+ const factor = e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP
200
+ zoomAt(e.clientX, e.clientY, scale.value * factor)
201
+ }
202
+
203
+ function zoomBy(factor: number) {
204
+ const svg = svgEl.value
205
+ if (!svg) return
206
+ const r = svg.getBoundingClientRect()
207
+ zoomAt(r.left + r.width / 2, r.top + r.height / 2, scale.value * factor)
208
+ }
209
+
210
+ function resetZoom() {
211
+ scale.value = MIN_ZOOM
212
+ tx.value = 0
213
+ ty.value = 0
214
+ }
215
+
216
+ function onPointerDown(e: PointerEvent) {
217
+ if (!props.zoomable || !isZoomed.value || e.button !== 0) return
218
+ dragging.value = true
219
+ hoverId.value = null
220
+ const p = clientToSvg(e.clientX, e.clientY)
221
+ dragOrigin.value = { x: p.x, y: p.y, tx: tx.value, ty: ty.value }
222
+ ;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
223
+ }
224
+
225
+ function onPointerMove(e: PointerEvent) {
226
+ if (!dragging.value || !dragOrigin.value) return
227
+ const p = clientToSvg(e.clientX, e.clientY)
228
+ tx.value = dragOrigin.value.tx + (p.x - dragOrigin.value.x)
229
+ ty.value = dragOrigin.value.ty + (p.y - dragOrigin.value.y)
230
+ }
231
+
232
+ function onPointerUp(e: PointerEvent) {
233
+ if (!dragging.value) return
234
+ dragging.value = false
235
+ dragOrigin.value = null
236
+ ;(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
237
+ }
238
+
239
+ const croppedViewBox = `${MAP_VIEW.x} ${MAP_VIEW.y} ${MAP_VIEW.width} ${MAP_VIEW.height}`
124
240
  </script>
125
241
 
126
242
  <template>
127
243
  <div ref="el" class="bgl-map w-100p flex column gap-075">
128
244
  <div
129
- class="bgl-map__plot relative w-100p"
245
+ class="bgl-map__plot relative w-100p overflow-hidden"
246
+ :class="{
247
+ 'bgl-map__plot--zoomable': zoomable,
248
+ 'bgl-map__plot--zoomed': zoomable && isZoomed,
249
+ 'bgl-map__plot--dragging': dragging,
250
+ }"
130
251
  :style="{ height: `${height}px` }"
131
252
  @mousemove="onMove"
132
253
  @mouseleave="onLeave"
254
+ @wheel="onWheel"
255
+ @pointerdown="onPointerDown"
256
+ @pointermove="onPointerMove"
257
+ @pointerup="onPointerUp"
258
+ @pointercancel="onPointerUp"
133
259
  >
134
260
  <svg
261
+ ref="svgEl"
135
262
  class="bgl-map__svg"
136
263
  dir="ltr"
137
- :viewBox="`0 0 ${WORLD_VIEWBOX.width} ${WORLD_VIEWBOX.height}`"
264
+ :viewBox="croppedViewBox"
138
265
  preserveAspectRatio="xMidYMid meet"
139
266
  width="100%"
140
267
  height="100%"
141
268
  >
142
- <path
143
- v-for="c in countries"
144
- :key="c.id"
145
- :d="c.d"
146
- :fill="c.fill"
147
- :stroke="strokeColor"
148
- stroke-width="0.6"
149
- vector-effect="non-scaling-stroke"
150
- class="bgl-map__country"
151
- :class="{
152
- 'opacity-5': hoverId != null && hoverId !== c.id,
153
- 'bgl-map__country--hot': hoverId === c.id && c.hasData,
154
- }"
155
- @mouseenter="onEnter(c.id)"
156
- />
269
+ <g :transform="mapTransform">
270
+ <path
271
+ v-for="c in countries"
272
+ :key="c.id"
273
+ :d="c.d"
274
+ :fill="c.fill"
275
+ :stroke="strokeColor"
276
+ stroke-width="0.6"
277
+ vector-effect="non-scaling-stroke"
278
+ class="bgl-map__country"
279
+ :class="{
280
+ 'opacity-5': hoverId != null && hoverId !== c.id,
281
+ 'bgl-map__country--hot': hoverId === c.id && c.hasData,
282
+ }"
283
+ @mouseenter="onEnter(c.id)"
284
+ />
285
+ </g>
157
286
  </svg>
158
287
 
288
+ <div
289
+ v-if="zoomable"
290
+ class="bgl-map__zoom flex column gap-025 absolute bottom-05 end-025 z-1 p-025 round bg-box shadow-10 border"
291
+ @pointerdown.stop
292
+ >
293
+ <Btn
294
+ flat
295
+ thin
296
+ round
297
+ size="xs"
298
+ icon="add"
299
+ :disabled="scale >= MAX_ZOOM"
300
+ @click="zoomBy(ZOOM_STEP)"
301
+ />
302
+ <Btn
303
+ flat
304
+ thin
305
+ round
306
+ size="xs"
307
+ icon="remove"
308
+ :disabled="scale <= MIN_ZOOM"
309
+ @click="zoomBy(1 / ZOOM_STEP)"
310
+ />
311
+ <Btn
312
+ :disabled="!isZoomed"
313
+ flat
314
+ thin
315
+ round
316
+ size="xs"
317
+ icon="zoom_out_map"
318
+ @click="resetZoom"
319
+ />
320
+ </div>
321
+
159
322
  <ChartTooltip
160
323
  v-if="hover"
161
324
  prefer="above"
@@ -183,12 +346,16 @@ const legendMax = computed(() => formatValue(maxValue.value, fmt.value))
183
346
 
184
347
  <style scoped>
185
348
  .bgl-map__plot { min-height: 0; }
186
- .bgl-map__svg { display: block; overflow: visible; }
349
+ .bgl-map__plot--zoomable { touch-action: none; }
350
+ .bgl-map__plot--zoomed { cursor: grab; }
351
+ .bgl-map__plot--dragging { cursor: grabbing; }
352
+ .bgl-map__svg { display: block; overflow: hidden; }
187
353
  .bgl-map__country {
188
354
  cursor: default;
189
355
  transition: opacity 0.15s ease, fill 0.15s ease;
190
356
  outline: none;
191
357
  }
358
+ .bgl-map__plot--zoomed .bgl-map__country { cursor: inherit; }
192
359
  .bgl-map__country--hot { stroke: var(--bgl-gray); stroke-width: 1.2; }
193
360
  .bgl-map__ramp {
194
361
  height: 0.5rem;
@@ -32,6 +32,8 @@ const slots: SetupContext['slots'] = useSlots()
32
32
 
33
33
  const loading = defineModel('loading', { default: false })
34
34
  const itemHeight = defineModel('itemHeight', { default: 50 })
35
+ /** Cell font size. Number = px; string = any CSS size (e.g. '0.875rem'). */
36
+ const fontSize = defineModel<number | string | undefined>('fontSize', { default: undefined })
35
37
  const selectedItems = defineModel<string[]>('selectedItems', { default: [] as string[] })
36
38
 
37
39
  const {
@@ -126,9 +128,14 @@ const computedItemHeight = computed(() => `${itemHeight.value}px`)
126
128
 
127
129
  watch(
128
130
  () => loading.value,
129
- (newLoadingVal, oldLoadingVal) => {
131
+ async (newLoadingVal, oldLoadingVal) => {
130
132
  if (newLoadingVal === oldLoadingVal) { return }
131
- if (!newLoadingVal) { registerLastItemObserver() }
133
+ if (!newLoadingVal) {
134
+ registerLastItemObserver()
135
+ scrollTo(0)
136
+ await nextTick()
137
+ scrollTo(0)
138
+ }
132
139
  },
133
140
  { immediate: true }
134
141
  )
@@ -139,9 +146,14 @@ watch(
139
146
  )
140
147
 
141
148
  watch(
142
- () => computedData.value,
143
- (newData, oldData) => {
144
- if (newData.length === oldData.length || props.onLastItemVisible !== undefined) { return }
149
+ () => computedData.value.length,
150
+ async (newLen, oldLen) => {
151
+ // Infinite-scroll appends: keep position. First load (0 → n) still resets.
152
+ const isAppend = props.onLastItemVisible !== undefined && (oldLen ?? 0) > 0
153
+ if (newLen === oldLen || isAppend) return
154
+
155
+ scrollTo(0)
156
+ await nextTick()
145
157
  scrollTo(0)
146
158
  }
147
159
  )
@@ -150,12 +162,26 @@ const showLoading = computed(() => loading.value)
150
162
 
151
163
  // Show the empty state only once loading has settled and there are no rows.
152
164
  const isEmpty = computed(() => !showLoading.value && computedData.value.length === 0)
165
+
166
+ const resolvedFontSize = computed(() => {
167
+ const size = fontSize.value
168
+ if (size == null || size === '') return null
169
+ return typeof size === 'number' ? `${size}px` : String(size)
170
+ })
171
+
172
+ /** Applied to body cells — falls back to the historical default. */
173
+ const bodyFontSize = computed(() => resolvedFontSize.value ?? '0.88em')
174
+ /** Header is slightly smaller than body when a custom size is set. */
175
+ const headerFontSize = computed(() => {
176
+ if (!resolvedFontSize.value) return '0.8rem'
177
+ return `calc(${resolvedFontSize.value} * 0.9)`
178
+ })
153
179
  </script>
154
180
 
155
181
  <template>
156
182
  <div class="table-list-wrap h-100p" v-bind="containerProps" :class="{ overflowHiddenLoading: showLoading }">
157
183
  <Loading v-if="showLoading" class="h100p" />
158
- <div v-bind="wrapperProps" :class="{ 'pointer-events-none': showLoading }">
184
+ <div v-if="!isEmpty" v-bind="wrapperProps" :class="{ 'pointer-events-none': showLoading }">
159
185
  <table class="infinite-wrapper" :class="{ selecting: computedSelectedItems.length }">
160
186
  <thead class="row first-row">
161
187
  <th v-if="isSortable" class="col-drag" />
@@ -202,16 +228,16 @@ const isEmpty = computed(() => !showLoading.value && computedData.value.length =
202
228
  <tr v-if="props.onLastItemVisible !== undefined" ref="lastItemEl" style="height: 1px;" />
203
229
  </tbody>
204
230
  </table>
205
- <!-- Built-in empty state (overridable via the #empty slot) -->
206
- <slot v-if="isEmpty" name="empty">
207
- <EmptyState
208
- :icon="props.emptyIcon ?? 'inbox'"
209
- :title="props.emptyTitle ?? 'No results'"
210
- :description="props.emptyDescription"
211
- compact
212
- />
213
- </slot>
214
231
  </div>
232
+ <!-- Outside the virtualized wrapper so EmptyState doesn't inflate scroll height -->
233
+ <slot v-if="isEmpty" name="empty">
234
+ <EmptyState
235
+ :icon="props.emptyIcon ?? 'inbox'"
236
+ :title="props.emptyTitle ?? 'No results'"
237
+ :description="props.emptyDescription"
238
+ compact
239
+ />
240
+ </slot>
215
241
  </div>
216
242
  </template>
217
243
 
@@ -238,6 +264,9 @@ cursor: grab;
238
264
 
239
265
  .table-list-wrap {
240
266
  min-height: 150px;
267
+ /* Prevent browser scroll anchoring from pushing content to the bottom
268
+ when rows mount and the virtual list height jumps. */
269
+ overflow-anchor: none;
241
270
  }
242
271
 
243
272
  .selected {
@@ -292,7 +321,7 @@ width: 100%;
292
321
  }
293
322
 
294
323
  th {
295
- font-size: 0.8rem;
324
+ font-size: v-bind(headerFontSize);
296
325
  color: var(--bgl-black-tint);
297
326
  position: sticky;
298
327
  top: 0;
@@ -315,7 +344,7 @@ cursor: pointer;
315
344
  }
316
345
 
317
346
  .row.first-row {
318
- font-size: 0.8rem;
347
+ font-size: v-bind(headerFontSize);
319
348
  color: var(--bgl-black-tint);
320
349
  position: sticky;
321
350
  top: 0;
@@ -377,7 +406,7 @@ margin-top: -2px;
377
406
  }
378
407
 
379
408
  .rows {
380
- font-size: 0.88em;
409
+ font-size: v-bind(bodyFontSize);
381
410
  }
382
411
 
383
412
  .table-list {
@@ -476,7 +505,7 @@ line-height: 1;
476
505
  }
477
506
 
478
507
  tbody tr {
479
- font-size: 0.88em;
508
+ font-size: v-bind(bodyFontSize);
480
509
  height: v-bind(computedItemHeight);
481
510
  transition: all 200ms ease;
482
511
  }
@@ -11,7 +11,7 @@ import { useBagelInputShell } from './bagelInputShell'
11
11
 
12
12
  const props = defineProps<{
13
13
  label?: string
14
- helpText?: string
14
+ helptext?: string
15
15
  modelValue: T[]
16
16
  allowAdd?: boolean
17
17
  allowDelete?: boolean
@@ -176,8 +176,8 @@ function getItemRef(i: number) {
176
176
  <label v-if="label" class="txt12 txt-gray-90 mb-025 block">
177
177
  {{ resolveI18n(label) }}
178
178
  </label>
179
- <p v-if="helpText" class="txt12 txt-gray-50 mb-05">
180
- {{ helpText }}
179
+ <p v-if="helptext" class="helptext">
180
+ {{ helptext }}
181
181
  </p>
182
182
  <template v-if="!collapsible">
183
183
  <div v-for="(_, i) in items" :key="i" class="array-input-row grid align-items-center" :class="{ 'simpleArray': isSimple, 'pb-1': !isSimple }">
@@ -351,7 +351,7 @@ onMounted(() => {
351
351
  <label :for="id">
352
352
  <span v-if="label || (underlined && placeholder)" class="label-text">{{ resolveI18n(label) || resolveI18n(placeholder) }}<span v-if="required"> *</span></span>
353
353
  <div v-if="!underlined" class="flex">
354
- <span v-if="helptext" class="opacity-7 light">{{ helptext }}</span>
354
+ <span v-if="helptext" class="helptext">{{ helptext }}</span>
355
355
  <span
356
356
  v-if="suggestedCorrection" class="pointer nowrap inline-block ms-auto color-red txt-10px p-0"
357
357
  @click.prevent="applyCorrection"
@@ -217,7 +217,7 @@ const hasValue = computed(() => formattedValue.value.length > 0)
217
217
  :class="[{ 'bgl-big-ctrl-num-btn': layout === 'vertical' }]" tabindex="-1" @click="decrement"
218
218
  />
219
219
  </div>
220
- <p v-if="helptext" class="opacity-7 light">
220
+ <p v-if="helptext" class="helptext">
221
221
  {{ helptext }}
222
222
  </p>
223
223
  </div>
@@ -36,7 +36,7 @@ interface PropTypes extends BagelInputShellProps {
36
36
  icon?: IconType
37
37
  hideLabel?: boolean
38
38
  defaultValue?: Option
39
- helpText?: string
39
+ helptext?: string
40
40
  clearable?: boolean
41
41
  placement?: AlignedPlacement
42
42
  searchPlaceholder?: string
@@ -315,8 +315,8 @@ onMounted(() => {
315
315
  {{ error }}
316
316
  </div>
317
317
  </label>
318
- <p v-if="helpText" class="opacity-7 txt-small light">
319
- {{ helpText }}
318
+ <p v-if="helptext" class="helptext">
319
+ {{ helptext }}
320
320
  </p>
321
321
  </template>
322
322
  <Skeleton v-if="isLoading" :count="6" width="200px" height="25px" class="mx-1 my-1" />
@@ -294,13 +294,6 @@ font-size: var(--bgl-label-font-size);
294
294
  margin-bottom: 0.25rem;
295
295
  }
296
296
 
297
- .bgl_input.signature-pad > .helptext {
298
- font-size: var(--bgl-label-font-size);
299
- margin: 0 0 0.25rem 0;
300
- line-height: 1.4;
301
- opacity: 0.7;
302
- }
303
-
304
297
  .bgl_input.signature-pad .canvas[disabled] {
305
298
  background: var(--input-disabled-bg);
306
299
  border: 1px solid var(--bgl-gray);
@@ -186,10 +186,10 @@ onMounted(async () => {
186
186
  :placeholder="underlined ? ' ' : (resolveI18n(placeholder) || resolveI18n(label))" :disabled :required
187
187
  v-bind="nativeInputAttrs" :pattern="normalizedPattern" @input="updateInputVal" @focusout="onFocusout"
188
188
  />
189
- <p v-if="helptext" class="opacity-7 light">{{ helptext }}</p>
190
189
  <Icon v-if="iconStart" :class="{ ' -top-025 line-height-0 ': underlined }" class="iconStart" :icon="iconStart" />
191
190
  <Icon v-if="icon" :class="{ ' -top-025 line-height-0 ': underlined }" :icon />
192
191
  </label>
192
+ <p v-if="helptext" class="helptext">{{ helptext }}</p>
193
193
  <div v-if="error" class="error-message">
194
194
  {{ error }}
195
195
  </div>
@@ -8,6 +8,7 @@ const props = withDefaults(defineProps<{
8
8
  label?: string
9
9
  id?: string
10
10
  title?: string
11
+ helptext?: string
11
12
  small?: boolean
12
13
  required?: boolean
13
14
  defaultValue?: boolean
@@ -47,6 +48,10 @@ onMounted(() => {
47
48
  {{ resolveI18n(label) }}
48
49
  </slot>
49
50
  </label>
51
+
52
+ <p v-if="helptext" class="helptext">
53
+ {{ helptext }}
54
+ </p>
50
55
  </div>
51
56
  </template>
52
57
 
@@ -58,6 +63,9 @@ display: unset !important;
58
63
  .bagel-input.bgl-toggle *{
59
64
  cursor: pointer !important;
60
65
  }
66
+ .bagel-input.bgl-toggle .helptext {
67
+ cursor: default !important;
68
+ }
61
69
  .bagel-input.bgl-toggle input{
62
70
  padding: unset !important;
63
71
  min-width: unset !important;
@@ -26,6 +26,16 @@ const SLOT_VALUE_COMPONENTS = new Set(['div', 'span', 'p'])
26
26
 
27
27
  const SRC_VALUE_COMPONENTS = new Set(['img', 'iframe'])
28
28
 
29
+ /** Display components that take content via `value` (not `modelValue`). */
30
+ const VALUE_PROP_NAMES = new Set(['BglBadge', 'Badge', 'BglPill', 'Pill'])
31
+
32
+ function usesValueProp(Component: unknown): boolean {
33
+ if (!Component || typeof Component === 'string') return false
34
+ const name = (Component as { name?: string, __name?: string }).name
35
+ ?? (Component as { __name?: string }).__name
36
+ return !!name && VALUE_PROP_NAMES.has(name)
37
+ }
38
+
29
39
  export interface UseSchemaFieldOptions<T, SFP extends Path<T>> {
30
40
  mode?: 'form' | 'preview' | 'table'
31
41
  getFormData?: () => T
@@ -217,6 +227,9 @@ export function useSchemaField<T extends { [key: string]: any }>(optns: UseSchem
217
227
  // Slot components don't need a value prop
218
228
  } else if (isSrcValueComponent) {
219
229
  props.src = transformedValue
230
+ } else if (usesValueProp(Component)) {
231
+ // Badge etc. read `value`, not modelValue — empty outline pills otherwise
232
+ props.value = transformedValue != null ? String(transformedValue) : transformedValue
220
233
  } else {
221
234
  props.modelValue = transformedValue
222
235
  }
@@ -293,12 +293,15 @@ const mergedErrors = computed(() => {
293
293
 
294
294
  function getFieldProps(field: FieldBuilder, key: string) {
295
295
  const label = generateLabel(key, field)
296
+ const { helpText, ...configRest } = field._config
296
297
  const baseProps: any = {
297
298
  label,
298
299
  'modelValue': getFieldValue(key),
299
300
  'onUpdate:modelValue': (val: any) => { updateField(key, val, field) },
300
301
  'error': mergedErrors.value[key],
301
- ...field._config,
302
+ ...configRest,
303
+ // Inputs use `helptext`; FormFlow schema API keeps `helpText`
304
+ ...(helpText != null ? { helptext: helpText } : {}),
302
305
  name: field._config.name ?? key,
303
306
  }
304
307
 
@@ -326,14 +329,16 @@ function getFieldProps(field: FieldBuilder, key: string) {
326
329
 
327
330
  if (field._type === 'array') {
328
331
  // `default` / `schema` are form-schema metadata — never pass them through to ArrayInput
329
- // (and the earlier `...field._config` spread may have already set them).
332
+ // (and the earlier config spread may have already set them).
330
333
  delete baseProps.default
331
334
  delete baseProps.schema
332
335
  delete baseProps.itemType
333
- const configWithoutSchema = Object.fromEntries(
336
+ const { helpText: arrayHelpText, ...arrayConfigRest } = Object.fromEntries(
334
337
  Object.entries(field._config).filter(([k]) => k !== 'schema' && k !== 'itemType' && k !== 'default')
335
338
  )
336
- Object.assign(baseProps, configWithoutSchema)
339
+ Object.assign(baseProps, arrayConfigRest)
340
+ if (arrayHelpText != null) baseProps.helptext = arrayHelpText
341
+ delete baseProps.helpText
337
342
  // Re-apply binding after config spread so handlers never get overwritten.
338
343
  baseProps.modelValue = getFieldValue(key)
339
344
  baseProps['onUpdate:modelValue'] = (val: any) => { updateField(key, val, field) }
@@ -59,6 +59,24 @@ select {
59
59
  color: var(--bgl-input-label-active-color);
60
60
  }
61
61
 
62
+ .helptext {
63
+ display: block;
64
+ margin: 0.15rem 0 0.25rem;
65
+ line-height: 1.4;
66
+ font-size: var(--bgl-label-font-size);
67
+ font-weight: 300;
68
+ color: var(--bgl-label-color);
69
+ cursor: default;
70
+ user-select: text;
71
+ }
72
+
73
+ /* Keep helptext muted even when nested inside a focused/active label */
74
+ .bagel-input:focus-within .helptext,
75
+ .bagel-input.open .helptext,
76
+ .bagel-input:has(:checked) .helptext {
77
+ color: var(--bgl-label-color);
78
+ }
79
+
62
80
  .bagel-input input,
63
81
  .bagel-input select,
64
82
  .custom-select .input {