@bagelink/vue 1.15.151 → 1.15.156
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.
- package/dist/components/charts/BarChart.vue.d.ts.map +1 -1
- package/dist/components/charts/ChartTooltip.vue.d.ts +12 -2
- package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -1
- package/dist/components/charts/LineChart.vue.d.ts.map +1 -1
- package/dist/components/layout/AppNav.vue.d.ts +1 -1
- package/dist/components/layout/AppNav.vue.d.ts.map +1 -1
- package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
- package/dist/form-flow/MultiStepForm.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +5 -0
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +11 -1
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.mjs +149 -98
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/charts/BarChart.vue +1 -1
- package/src/components/charts/ChartTooltip.vue +73 -16
- package/src/components/charts/Donut.vue +1 -1
- package/src/components/charts/Funnel.vue +3 -3
- package/src/components/charts/LineChart.vue +1 -1
- package/src/form-flow/FormFlow.vue +2 -0
- package/src/form-flow/MultiStepForm.vue +1 -0
- package/src/form-flow/form-flow.ts +19 -0
- package/src/form-flow/schema-fields.ts +21 -0
package/package.json
CHANGED
|
@@ -177,7 +177,7 @@ const tooltipStyle = computed(() => {
|
|
|
177
177
|
</svg>
|
|
178
178
|
|
|
179
179
|
<ChartTooltip
|
|
180
|
-
v-if="hoverIdx != null" :style="tooltipStyle"
|
|
180
|
+
v-if="hoverIdx != null" prefer="top" :style="tooltipStyle"
|
|
181
181
|
:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
|
|
182
182
|
/>
|
|
183
183
|
</div>
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Positioned by the host via `style` (absolute left/top); the host owns
|
|
8
8
|
* mouse tracking. Non-interactive (pointer-events: none).
|
|
9
|
+
*
|
|
10
|
+
* Placement is dynamic by default: the tooltip measures itself and flips to
|
|
11
|
+
* the opposite side of its anchor whenever the preferred side would clip past
|
|
12
|
+
* the top/bottom of the viewport. Hosts only express a *preferred* side via
|
|
13
|
+
* `prefer` (or the legacy `above`/`below` flags).
|
|
9
14
|
*/
|
|
15
|
+
import { nextTick, ref, watch } from 'vue'
|
|
10
16
|
import { useI18n } from '@bagelink/vue'
|
|
11
17
|
|
|
12
18
|
export interface TipRow { name: string; color?: string; value: string }
|
|
@@ -16,41 +22,95 @@ defineOptions({ name: 'BglChartTooltip' })
|
|
|
16
22
|
|
|
17
23
|
const { $t } = useI18n()
|
|
18
24
|
|
|
19
|
-
withDefaults(defineProps<{
|
|
25
|
+
const props = withDefaults(defineProps<{
|
|
20
26
|
label?: string
|
|
21
27
|
/** Big focal number (used by single-value charts like the funnel). */
|
|
22
28
|
hero?: string
|
|
23
29
|
rows?: TipRow[]
|
|
24
30
|
total?: string | null
|
|
25
31
|
stats?: TipStat[]
|
|
26
|
-
/**
|
|
32
|
+
/** Preferred side relative to the anchor point. The tooltip still flips to
|
|
33
|
+
the opposite side automatically when the preferred side would clip.
|
|
34
|
+
- `above`/`below`: cursor-anchored charts (a small gap is kept).
|
|
35
|
+
- `top`: pinned to the top of the chart (no vertical flip). */
|
|
36
|
+
prefer?: 'above' | 'below' | 'top'
|
|
37
|
+
/** @deprecated use `prefer="above"`. Float above the anchor point. */
|
|
27
38
|
above?: boolean
|
|
39
|
+
/** @deprecated use `prefer="below"`. Float below the anchor point. */
|
|
40
|
+
below?: boolean
|
|
28
41
|
}>(), {
|
|
29
42
|
rows: () => [],
|
|
30
43
|
stats: () => [],
|
|
44
|
+
prefer: 'below',
|
|
31
45
|
})
|
|
46
|
+
|
|
47
|
+
// Resolve the caller's preferred side (new `prefer` prop wins; fall back to the
|
|
48
|
+
// legacy boolean flags for compatibility).
|
|
49
|
+
function preferredSide(): 'above' | 'below' | 'top' {
|
|
50
|
+
if (props.above) return 'above'
|
|
51
|
+
if (props.below) return 'below'
|
|
52
|
+
return props.prefer
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const el = ref<HTMLElement>()
|
|
56
|
+
// The side actually used after measuring (may differ from the preferred one).
|
|
57
|
+
const side = ref<'above' | 'below' | 'top'>(preferredSide())
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Place the tooltip by the anchor's position in the viewport:
|
|
61
|
+
* anchors in the top half get the tooltip *below* them, anchors in the
|
|
62
|
+
* bottom half get it *above*. `top` placements are pinned to the chart and
|
|
63
|
+
* never flip.
|
|
64
|
+
*/
|
|
65
|
+
function place() {
|
|
66
|
+
const node = el.value
|
|
67
|
+
const want = preferredSide()
|
|
68
|
+
if (!node || want === 'top') { side.value = want; return }
|
|
69
|
+
|
|
70
|
+
// Normalize to the `below` transform first so the measured `top` always
|
|
71
|
+
// reflects the stable anchor point (not a post-flip offset), avoiding
|
|
72
|
+
// flip-flop right around the midpoint.
|
|
73
|
+
side.value = 'below'
|
|
74
|
+
nextTick(() => {
|
|
75
|
+
const n = el.value
|
|
76
|
+
if (!n) return
|
|
77
|
+
const r = n.getBoundingClientRect()
|
|
78
|
+
side.value = r.top < window.innerHeight / 2 ? 'below' : 'above'
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Re-place whenever the anchor moves or the content changes.
|
|
83
|
+
watch(
|
|
84
|
+
() => [props.label, props.hero, props.total, props.rows, props.stats, props.prefer, props.above, props.below],
|
|
85
|
+
place,
|
|
86
|
+
{ immediate: true, deep: true },
|
|
87
|
+
)
|
|
32
88
|
</script>
|
|
33
89
|
|
|
34
90
|
<template>
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
|
|
91
|
+
<div
|
|
92
|
+
ref="el"
|
|
93
|
+
class="bgl-tip absolute top-0 pointer-events-none nowrap z-5 color-white line-height-16"
|
|
94
|
+
:class="{ 'bgl-tip--above': side === 'above', 'bgl-tip--below': side === 'below' }"
|
|
95
|
+
>
|
|
96
|
+
<p v-if="label" class="opacity-7 txt-075rem m-0 pb-025 ">{{ label }}</p>
|
|
97
|
+
<p v-if="hero" class="m-0 tabular-nums bold pb-025 txt-1-25rem">{{ hero }}</p>
|
|
38
98
|
|
|
39
|
-
<p v-for="(r, i) in rows" :key="i" class="
|
|
99
|
+
<p v-for="(r, i) in rows" :key="i" class="gap-05 flex align-items-center m-0">
|
|
40
100
|
<span v-if="r.color" class="bgl-tip__dot flex-shrink-0" :style="{ background: r.color }" />
|
|
41
101
|
<span v-if="rows.length > 1" class="bgl-tip__name">{{ r.name }}</span>
|
|
42
|
-
<span class="
|
|
102
|
+
<span class="ms-auto tabular-nums semibold">{{ r.value }}</span>
|
|
43
103
|
</p>
|
|
44
104
|
|
|
45
|
-
<p v-if="total" class="
|
|
105
|
+
<p v-if="total" class="gap-05 bgl-tip__total flex align-items-center m-0">
|
|
46
106
|
<span class="bgl-tip__name">{{ $t('charts.total') }}</span>
|
|
47
|
-
<span class="
|
|
107
|
+
<span class="ms-auto tabular-nums semibold">{{ total }}</span>
|
|
48
108
|
</p>
|
|
49
109
|
|
|
50
110
|
<div v-if="stats.length" class="bgl-tip__stats flex column">
|
|
51
|
-
<p v-for="(s, i) in stats" :key="i" class="
|
|
111
|
+
<p v-for="(s, i) in stats" :key="i" class="gap-05 flex align-items-center m-0 space-between">
|
|
52
112
|
<span class="bgl-tip__name">{{ s.label }}</span>
|
|
53
|
-
<span class="
|
|
113
|
+
<span class="ms-auto tabular-nums semibold">{{ s.value }}</span>
|
|
54
114
|
</p>
|
|
55
115
|
</div>
|
|
56
116
|
</div>
|
|
@@ -58,17 +118,14 @@ withDefaults(defineProps<{
|
|
|
58
118
|
|
|
59
119
|
<style scoped>
|
|
60
120
|
.bgl-tip {
|
|
61
|
-
|
|
121
|
+
transform: translateX(-50%); transform-origin: center;
|
|
62
122
|
background: var(--bgl-black); border-radius: 0.5rem; padding: 0.4rem 0.6rem;
|
|
63
123
|
font-size: 0.75rem; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); min-width: 7rem;
|
|
64
124
|
}
|
|
65
125
|
.bgl-tip--above { transform: translate(-50%, calc(-100% - 0.6rem)); }
|
|
66
|
-
.bgl-
|
|
67
|
-
.bgl-tip__hero { margin-bottom: 0.2rem; font-size: 1.05rem; font-weight: 700; }
|
|
68
|
-
.bgl-tip__row { gap: 0.4rem; }
|
|
126
|
+
.bgl-tip--below { transform: translate(-50%, 0.6rem); }
|
|
69
127
|
.bgl-tip__dot { width: 8px; height: 8px; border-radius: 2px; }
|
|
70
128
|
.bgl-tip__name { opacity: 0.7; }
|
|
71
|
-
.bgl-tip__val { margin-inline-start: auto; font-weight: 600; }
|
|
72
129
|
.bgl-tip__total,
|
|
73
130
|
.bgl-tip__stats {
|
|
74
131
|
margin-top: 0.28rem; padding-top: 0.28rem;
|
|
@@ -190,7 +190,7 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
|
|
|
190
190
|
<span v-if="centerLabel" class="color-gray txt12">{{ centerLabel }}</span>
|
|
191
191
|
</div>
|
|
192
192
|
<ChartTooltip
|
|
193
|
-
v-if="hoverArc" above :style="tooltipStyle"
|
|
193
|
+
v-if="hoverArc" prefer="above" :style="tooltipStyle"
|
|
194
194
|
:label="hoverArc.label" :hero="hoverValue" :stats="hoverStats"
|
|
195
195
|
/>
|
|
196
196
|
</div>
|
|
@@ -292,7 +292,7 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
|
|
|
292
292
|
|
|
293
293
|
<!-- Shared tooltip -->
|
|
294
294
|
<ChartTooltip
|
|
295
|
-
v-if="hoverStage" above :style="tooltipStyle"
|
|
295
|
+
v-if="hoverStage" prefer="above" :style="tooltipStyle"
|
|
296
296
|
:label="hoverStage.label"
|
|
297
297
|
:hero="isSegmented ? undefined : hoverStage.display"
|
|
298
298
|
:rows="isSegmented ? hoverStage.rows : []"
|
|
@@ -306,8 +306,8 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
|
|
|
306
306
|
v-for="(s, i) in segments" :key="i" class="bgl-funnel__tick p-05"
|
|
307
307
|
:style="horizontal ? { flex: '1 1 0' } : { height: `${bandHeight}px` }"
|
|
308
308
|
>
|
|
309
|
-
<span class="bgl-funnel__name ellipsis-1 txt12 light" :title="s.label">{{ s.label }}</span>
|
|
310
|
-
<span class="bgl-funnel__val txt24">{{ s.display }}</span>
|
|
309
|
+
<span class="bgl-funnel__name ellipsis-1 txt12 light opacity-6" :title="s.label">{{ s.label }}</span>
|
|
310
|
+
<span class="bgl-funnel__val txt24 bold">{{ s.display }}</span>
|
|
311
311
|
</div>
|
|
312
312
|
</div>
|
|
313
313
|
</div>
|
|
@@ -231,7 +231,7 @@ const tooltipStyle = computed(() => {
|
|
|
231
231
|
|
|
232
232
|
<!-- shared tooltip following the mouse -->
|
|
233
233
|
<ChartTooltip
|
|
234
|
-
v-if="hoverIdx != null" :style="tooltipStyle"
|
|
234
|
+
v-if="hoverIdx != null" prefer="top" :style="tooltipStyle"
|
|
235
235
|
:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
|
|
236
236
|
/>
|
|
237
237
|
</div>
|
|
@@ -121,6 +121,8 @@ watch(formData, (newVal) => {
|
|
|
121
121
|
}, { deep: true })
|
|
122
122
|
|
|
123
123
|
function shouldShowField(field: FieldBuilder): boolean {
|
|
124
|
+
// Hidden fields keep their value (submitted) but are never displayed.
|
|
125
|
+
if (field._hidden) return false
|
|
124
126
|
if (field._condition == null) return true
|
|
125
127
|
return evaluateQuery(field._condition, formData.value)
|
|
126
128
|
}
|
|
@@ -197,6 +197,7 @@ const stepErrors = ref<Record<string, string>>({})
|
|
|
197
197
|
|
|
198
198
|
// Check if a field should be validated (visible based on condition)
|
|
199
199
|
function shouldValidateField(field: FieldBuilder): boolean {
|
|
200
|
+
if (field._hidden) return false
|
|
200
201
|
if (field._condition == null) return true
|
|
201
202
|
return evaluateCondition(field._condition, formData.value)
|
|
202
203
|
}
|
|
@@ -93,6 +93,8 @@ export interface FieldBuilder<TValue = any> {
|
|
|
93
93
|
_type: string
|
|
94
94
|
_config: BaseFieldConfig & Record<string, any>
|
|
95
95
|
_condition?: string
|
|
96
|
+
/** When true the field is never rendered but its value is still submitted. */
|
|
97
|
+
_hidden?: boolean
|
|
96
98
|
_validations?: Array<(value: TValue) => true | string>
|
|
97
99
|
_onUpdate?: (value: TValue, formData: Record<string, any>) => void
|
|
98
100
|
_class?: string
|
|
@@ -101,6 +103,8 @@ export interface FieldBuilder<TValue = any> {
|
|
|
101
103
|
|
|
102
104
|
// Chainable methods
|
|
103
105
|
if: (condition: string) => FieldBuilder<TValue>
|
|
106
|
+
/** Mark the field as hidden — kept in form data / submitted but not displayed. */
|
|
107
|
+
hidden: (hidden?: boolean) => FieldBuilder<TValue>
|
|
104
108
|
validate: (fn: (value: TValue) => true | string) => FieldBuilder<TValue>
|
|
105
109
|
onUpdate: (fn: (value: TValue, formData: Record<string, any>) => void) => FieldBuilder<TValue>
|
|
106
110
|
class: (className: string) => FieldBuilder<TValue>
|
|
@@ -148,6 +152,7 @@ class Field<TValue = any> implements FieldBuilder<TValue> {
|
|
|
148
152
|
_type: string
|
|
149
153
|
_config: BaseFieldConfig & Record<string, any>
|
|
150
154
|
_condition?: string
|
|
155
|
+
_hidden?: boolean
|
|
151
156
|
_validations?: Array<(value: TValue) => true | string>
|
|
152
157
|
_onUpdate?: (value: TValue, formData: Record<string, any>) => void
|
|
153
158
|
_class?: string
|
|
@@ -164,6 +169,11 @@ class Field<TValue = any> implements FieldBuilder<TValue> {
|
|
|
164
169
|
return this
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
hidden(hidden = true): this {
|
|
173
|
+
this._hidden = hidden
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
validate(fn: (value: TValue) => true | string): this {
|
|
168
178
|
if (!this._validations) {
|
|
169
179
|
this._validations = []
|
|
@@ -574,6 +584,9 @@ class Schema<T extends Record<string, any> = Record<string, any>> implements Sch
|
|
|
574
584
|
// Conditional visibility
|
|
575
585
|
if (field._condition) xUi.condition = field._condition
|
|
576
586
|
|
|
587
|
+
// Hidden (submitted but not displayed)
|
|
588
|
+
if (field._hidden) xUi.hidden = true
|
|
589
|
+
|
|
577
590
|
// Array field options
|
|
578
591
|
if (field._type === 'array') {
|
|
579
592
|
if (config.allowAdd !== undefined) xUi.allowAdd = config.allowAdd
|
|
@@ -763,6 +776,7 @@ interface JSONSchemaProperty {
|
|
|
763
776
|
cols?: number
|
|
764
777
|
class?: string
|
|
765
778
|
condition?: string
|
|
779
|
+
hidden?: boolean
|
|
766
780
|
allowAdd?: boolean
|
|
767
781
|
allowDelete?: boolean
|
|
768
782
|
component?: string
|
|
@@ -1019,6 +1033,11 @@ function createFieldFromProperty(
|
|
|
1019
1033
|
field.if(xUi.condition)
|
|
1020
1034
|
}
|
|
1021
1035
|
|
|
1036
|
+
// Apply hidden
|
|
1037
|
+
if (xUi.hidden) {
|
|
1038
|
+
field.hidden()
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1022
1041
|
return field
|
|
1023
1042
|
}
|
|
1024
1043
|
|
|
@@ -21,6 +21,7 @@ export type FormFieldType =
|
|
|
21
21
|
| 'number' | 'date' | 'email' | 'phone' | 'url'
|
|
22
22
|
| 'select' | 'radio' | 'checkbox' | 'toggle'
|
|
23
23
|
| 'file' | 'signature' | 'range' | 'color' | 'json'
|
|
24
|
+
| 'hidden'
|
|
24
25
|
| 'heading' | 'divider'
|
|
25
26
|
|
|
26
27
|
export interface SelectOption {
|
|
@@ -55,6 +56,10 @@ export interface FormField {
|
|
|
55
56
|
/** User-authored (non-grid) classes preserved alongside layout classes. */
|
|
56
57
|
class?: string
|
|
57
58
|
hideLabel?: boolean
|
|
59
|
+
/** SCIM 2.0 filter expression controlling conditional visibility. */
|
|
60
|
+
condition?: string
|
|
61
|
+
/** When true the field is never displayed but its value is still submitted. */
|
|
62
|
+
hidden?: boolean
|
|
58
63
|
richTextVariant?: 'full' | 'basic' | 'simple'
|
|
59
64
|
markdownShowFormatting?: boolean
|
|
60
65
|
textareaRows?: number
|
|
@@ -99,6 +104,10 @@ export interface JSONSchemaProperty {
|
|
|
99
104
|
/** UI extension object — layout classes + render hints (shared with form-flow). */
|
|
100
105
|
'x-ui'?: {
|
|
101
106
|
class?: string
|
|
107
|
+
/** SCIM 2.0 filter expression controlling conditional visibility. */
|
|
108
|
+
condition?: string
|
|
109
|
+
/** Field kept in form data / submitted but never displayed. */
|
|
110
|
+
hidden?: boolean
|
|
102
111
|
[key: string]: any
|
|
103
112
|
}
|
|
104
113
|
'x-field-type'?: FormFieldType
|
|
@@ -145,6 +154,9 @@ export function defaultRowSpan(type: FormFieldType): number {
|
|
|
145
154
|
return 1
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
/** Field types that never render an input in the live form. */
|
|
158
|
+
export const NON_INPUT_TYPES: FormFieldType[] = ['heading', 'divider', 'hidden']
|
|
159
|
+
|
|
148
160
|
export function inferFieldType(prop: JSONSchemaProperty): FormFieldType {
|
|
149
161
|
if (prop.format) {
|
|
150
162
|
switch (prop.format) {
|
|
@@ -204,6 +216,8 @@ export function schemaToFields(schema: JSONSchemaObject | undefined): FormField[
|
|
|
204
216
|
mColSpan: classToMColSpan(uiClass) ?? 12,
|
|
205
217
|
class: stripGridClasses(uiClass) || undefined,
|
|
206
218
|
hideLabel: prop['x-hide-label'] ?? false,
|
|
219
|
+
condition: prop['x-ui']?.condition || undefined,
|
|
220
|
+
hidden: prop['x-ui']?.hidden || (type === 'hidden') || undefined,
|
|
207
221
|
richTextVariant: prop['x-rich-text-variant'],
|
|
208
222
|
markdownShowFormatting: prop['x-markdown-show-formatting'],
|
|
209
223
|
textareaRows: prop['x-textarea-rows'],
|
|
@@ -266,6 +280,7 @@ export function fieldsToSchema(fields: FormField[]): JSONSchemaObject {
|
|
|
266
280
|
case 'file':
|
|
267
281
|
case 'signature':
|
|
268
282
|
case 'color':
|
|
283
|
+
case 'hidden':
|
|
269
284
|
prop.type = 'string'
|
|
270
285
|
break
|
|
271
286
|
case 'number':
|
|
@@ -332,6 +347,10 @@ export function fieldsToSchema(fields: FormField[]): JSONSchemaObject {
|
|
|
332
347
|
})
|
|
333
348
|
if (layoutClass) prop['x-ui'] = { ...prop['x-ui'], class: layoutClass }
|
|
334
349
|
|
|
350
|
+
// Conditional visibility + hidden — stored in x-ui (consumed by FormFlow)
|
|
351
|
+
if (field.condition) prop['x-ui'] = { ...prop['x-ui'], condition: field.condition }
|
|
352
|
+
if (field.hidden === true) prop['x-ui'] = { ...prop['x-ui'], hidden: true }
|
|
353
|
+
|
|
335
354
|
// Boolean/enum x- extensions — only write when non-default
|
|
336
355
|
if (field.hideLabel === true) prop['x-hide-label'] = true
|
|
337
356
|
if (field.richTextVariant) prop['x-rich-text-variant'] = field.richTextVariant
|
|
@@ -412,6 +431,8 @@ export function validateFormData(fields: FormField[], data: Record<string, any>)
|
|
|
412
431
|
|
|
413
432
|
for (const field of fields) {
|
|
414
433
|
if (field.type === 'heading' || field.type === 'divider') continue
|
|
434
|
+
// Hidden fields are never shown, so they can't be user-corrected — skip.
|
|
435
|
+
if (field.type === 'hidden' || field.hidden === true) continue
|
|
415
436
|
|
|
416
437
|
const value = data[field.key]
|
|
417
438
|
|