@bagelink/vue 1.15.178 → 1.15.182

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 (36) hide show
  1. package/dist/components/Card.vue.d.ts +1 -1
  2. package/dist/components/Card.vue.d.ts.map +1 -1
  3. package/dist/components/calendar/CalendarPopover.vue.d.ts +6 -6
  4. package/dist/components/dataTable/DataTable.vue.d.ts +3 -1
  5. package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
  6. package/dist/components/form/inputs/ArrayInput.vue.d.ts.map +1 -1
  7. package/dist/form-flow/FlowDesigner.vue.d.ts +13 -0
  8. package/dist/form-flow/FlowDesigner.vue.d.ts.map +1 -0
  9. package/dist/form-flow/FlowEdge.vue.d.ts +21 -0
  10. package/dist/form-flow/FlowEdge.vue.d.ts.map +1 -0
  11. package/dist/form-flow/FlowNode.vue.d.ts +30 -0
  12. package/dist/form-flow/FlowNode.vue.d.ts.map +1 -0
  13. package/dist/form-flow/FormFlowPlayer.vue.d.ts +62 -0
  14. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -0
  15. package/dist/form-flow/structure.d.ts +66 -0
  16. package/dist/form-flow/structure.d.ts.map +1 -0
  17. package/dist/index.cjs +104 -104
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.mjs +10076 -9391
  21. package/dist/style.css +1 -1
  22. package/dist/types/TableSchema.d.ts +7 -1
  23. package/dist/types/TableSchema.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/components/Card.vue +1 -1
  26. package/src/components/dataTable/DataTable.vue +13 -0
  27. package/src/components/form/inputs/ArrayInput.vue +11 -2
  28. package/src/form-flow/FlowDesigner.vue +462 -0
  29. package/src/form-flow/FlowEdge.vue +117 -0
  30. package/src/form-flow/FlowNode.vue +188 -0
  31. package/src/form-flow/FormFlowPlayer.vue +243 -0
  32. package/src/form-flow/structure.ts +123 -0
  33. package/src/index.ts +6 -0
  34. package/src/styles/layout.css +4 -0
  35. package/src/styles/mobilLayout.css +4 -0
  36. package/src/types/TableSchema.ts +7 -1
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FlowNode — a draggable step card on the FlowDesigner canvas.
4
+ *
5
+ * Emits low-level pointer intents; all graph mutation lives in FlowDesigner:
6
+ * - `move` : node dragged to a new (x, y)
7
+ * - `select` : node clicked (opens the step's field editor)
8
+ * - `startEdge`: the out-handle was grabbed to draw a new edge
9
+ * - `endEdge` : pointer released over this node (edge target)
10
+ * - `remove` : delete this step
11
+ */
12
+
13
+ import type { FormStep } from './structure'
14
+ import { computed, ref } from 'vue'
15
+
16
+ export interface Props {
17
+ step: FormStep
18
+ selected?: boolean
19
+ isStart?: boolean
20
+ fieldCount?: number
21
+ }
22
+
23
+ const props = defineProps<Props>()
24
+
25
+ const emit = defineEmits<{
26
+ 'move': [pos: { x: number, y: number }]
27
+ 'select': []
28
+ 'startEdge': [ev: PointerEvent]
29
+ 'endEdge': []
30
+ 'remove': []
31
+ 'setStart': []
32
+ }>()
33
+
34
+ const dragging = ref(false)
35
+ let startX = 0
36
+ let startY = 0
37
+ let originX = 0
38
+ let originY = 0
39
+
40
+ function onPointerDown(ev: PointerEvent) {
41
+ // Ignore drags that begin on the out-handle or action buttons.
42
+ if ((ev.target as HTMLElement).closest('.node-handle, .node-actions')) return
43
+ dragging.value = true
44
+ startX = ev.clientX
45
+ startY = ev.clientY
46
+ originX = props.step.x ?? 0
47
+ originY = props.step.y ?? 0
48
+ ;(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId)
49
+ }
50
+
51
+ function onPointerMove(ev: PointerEvent) {
52
+ if (!dragging.value) return
53
+ emit('move', {
54
+ x: originX + (ev.clientX - startX),
55
+ y: originY + (ev.clientY - startY),
56
+ })
57
+ }
58
+
59
+ function onPointerUp(ev: PointerEvent) {
60
+ if (dragging.value) {
61
+ dragging.value = false
62
+ ;(ev.currentTarget as HTMLElement).releasePointerCapture(ev.pointerId)
63
+ }
64
+ }
65
+
66
+ const title = computed(() => props.step.title?.trim() || props.step.key)
67
+ const fieldLabel = computed(() => {
68
+ const n = props.fieldCount ?? Object.keys(props.step.schema?.properties ?? {}).length
69
+ return `${n} field${n === 1 ? '' : 's'}`
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ class="flow-node"
76
+ :class="{ selected, 'is-start': isStart }"
77
+ :style="{ left: `${step.x ?? 0}px`, top: `${step.y ?? 0}px` }"
78
+ @pointerdown="onPointerDown"
79
+ @pointermove="onPointerMove"
80
+ @pointerup="onPointerUp"
81
+ @click.stop="emit('select')"
82
+ @pointerenter="dragging || emit('endEdge')"
83
+ >
84
+ <div class="node-head">
85
+ <span v-if="isStart" class="node-start-badge" title="Start step">▶</span>
86
+ <span class="node-title" :title="title">{{ title }}</span>
87
+ <div class="node-actions">
88
+ <button
89
+ v-if="!isStart"
90
+ type="button"
91
+ class="node-btn"
92
+ title="Set as start step"
93
+ @click.stop="emit('setStart')"
94
+ >★</button>
95
+ <button
96
+ type="button"
97
+ class="node-btn node-del"
98
+ title="Delete step"
99
+ @click.stop="emit('remove')"
100
+ >×</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="node-body">
105
+ <span class="node-key">{{ step.key }}</span>
106
+ <span class="node-fields">{{ fieldLabel }}</span>
107
+ </div>
108
+
109
+ <!-- Out-handle: drag from here to another node to create an edge. -->
110
+ <div
111
+ class="node-handle"
112
+ title="Drag to connect"
113
+ @pointerdown.stop="emit('startEdge', $event)"
114
+ />
115
+ </div>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .flow-node {
120
+ position: absolute;
121
+ width: 180px;
122
+ background: white;
123
+ border: 2px solid #e2e8f0;
124
+ border-radius: 8px;
125
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
126
+ cursor: grab;
127
+ user-select: none;
128
+ transition: border-color 0.15s, box-shadow 0.15s;
129
+ }
130
+ .flow-node:active { cursor: grabbing; }
131
+ .flow-node.selected {
132
+ border-color: var(--primary-color, #007bff);
133
+ box-shadow: 0 2px 8px rgba(0, 123, 255, 0.25);
134
+ }
135
+ .flow-node.is-start { border-color: var(--success-color, #28a745); }
136
+
137
+ .node-head {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 6px;
141
+ padding: 8px 10px;
142
+ border-bottom: 1px solid #f1f5f9;
143
+ }
144
+ .node-title {
145
+ flex: 1;
146
+ font-weight: 600;
147
+ font-size: 13px;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ white-space: nowrap;
151
+ }
152
+ .node-start-badge { color: var(--success-color, #28a745); font-size: 10px; }
153
+ .node-actions { display: flex; gap: 2px; }
154
+ .node-btn {
155
+ border: none;
156
+ background: transparent;
157
+ cursor: pointer;
158
+ font-size: 13px;
159
+ line-height: 1;
160
+ color: #94a3b8;
161
+ padding: 2px 4px;
162
+ border-radius: 4px;
163
+ }
164
+ .node-btn:hover { background: #f1f5f9; color: #475569; }
165
+ .node-del:hover { color: #ef4444; }
166
+
167
+ .node-body {
168
+ display: flex;
169
+ justify-content: space-between;
170
+ padding: 8px 10px;
171
+ font-size: 11px;
172
+ color: #64748b;
173
+ }
174
+ .node-key { font-family: ui-monospace, monospace; }
175
+
176
+ .node-handle {
177
+ position: absolute;
178
+ right: -8px;
179
+ top: 50%;
180
+ transform: translateY(-50%);
181
+ width: 14px;
182
+ height: 14px;
183
+ border-radius: 50%;
184
+ background: var(--primary-color, #007bff);
185
+ border: 2px solid white;
186
+ cursor: crosshair;
187
+ }
188
+ </style>
@@ -0,0 +1,243 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * FormFlowPlayer — decision-tree multi-step form runtime.
4
+ *
5
+ * Renders a `FormStructure` (see ./structure.ts) one step at a time, reusing
6
+ * FormFlow.vue per node. The client owns navigation:
7
+ * - Answers accumulate into a single `formData` object across steps.
8
+ * - After validating the current step, the *next* step is resolved by
9
+ * first-matching SCIM edge (`resolveNext`). A `null` result means the
10
+ * current step is terminal, so the primary button reads "Submit" and
11
+ * clicking it emits `complete` with the full accumulated data.
12
+ * - A visited-step stack enables Back without re-resolving.
13
+ *
14
+ * A single-node structure behaves like a plain single-step form (the primary
15
+ * button is always "Submit").
16
+ */
17
+
18
+ import type { FormStructure } from './structure'
19
+ import type { Slots } from 'vue'
20
+ import { computed, ref, useSlots, watch } from 'vue'
21
+ import { useI18n } from '../i18n'
22
+ import { fromJSONSchema } from './form-flow'
23
+ import FormFlow from './FormFlow.vue'
24
+ import { entryStepKey, resolveNext, stepByKey } from './structure'
25
+
26
+ export interface Props {
27
+ structure: FormStructure
28
+ modelValue?: Record<string, any>
29
+ components?: Record<string, any>
30
+ /** Optional async submit handler; awaited before `complete` is emitted. */
31
+ onComplete?: (data: Record<string, any>) => Promise<void> | void
32
+ }
33
+
34
+ const props = defineProps<Props>()
35
+
36
+ const emit = defineEmits<{
37
+ 'update:modelValue': [value: Record<string, any>]
38
+ 'stepChange': [stepKey: string]
39
+ 'complete': [data: Record<string, any>]
40
+ }>()
41
+
42
+ const { $t } = useI18n()
43
+ const slots: Slots = useSlots()
44
+ const slotNames = computed((): string[] => Object.keys(slots))
45
+
46
+ // Accumulated answers across every visited step.
47
+ const formData = ref<Record<string, any>>({ ...(props.modelValue || {}) })
48
+
49
+ watch(formData, val => emit('update:modelValue', val), { deep: true })
50
+ watch(() => props.modelValue, (val) => {
51
+ if (val && val !== formData.value) formData.value = { ...val }
52
+ }, { deep: true })
53
+
54
+ // Navigation state: the current step key plus the stack of previously visited
55
+ // keys (for Back). Initialised to the structure's entry step.
56
+ const currentKey = ref<string | undefined>(entryStepKey(props.structure))
57
+ const history = ref<string[]>([])
58
+
59
+ watch(() => props.structure, () => {
60
+ currentKey.value = entryStepKey(props.structure)
61
+ history.value = []
62
+ }, { deep: true })
63
+
64
+ watch(currentKey, (key) => {
65
+ if (key) emit('stepChange', key)
66
+ }, { immediate: true })
67
+
68
+ const currentStep = computed(() => stepByKey(props.structure, currentKey.value ?? null))
69
+
70
+ // Convert the current step's stored JSON Schema into a renderable schema.
71
+ const currentSchema = computed(() => {
72
+ const step = currentStep.value
73
+ return step ? fromJSONSchema(step.schema as any) : null
74
+ })
75
+
76
+ // The *next* step given current answers (reactive: recomputes as fields change).
77
+ // `null` => the current step is terminal, so the primary action is Submit.
78
+ const nextKey = computed<string | null>(() => {
79
+ if (!currentKey.value) return null
80
+ return resolveNext(props.structure, currentKey.value, formData.value)
81
+ })
82
+
83
+ const isTerminal = computed(() => nextKey.value === null)
84
+ const canGoBack = computed(() => history.value.length > 0)
85
+
86
+ const formRef = ref<{ validate: () => boolean } | null>(null)
87
+ const submitting = ref(false)
88
+
89
+ /** Validate the visible current step via the underlying FormFlow. */
90
+ function validateStep(): boolean {
91
+ return formRef.value?.validate() ?? true
92
+ }
93
+
94
+ async function onPrimary() {
95
+ if (!validateStep()) return
96
+
97
+ if (isTerminal.value) {
98
+ submitting.value = true
99
+ try {
100
+ await props.onComplete?.({ ...formData.value })
101
+ emit('complete', { ...formData.value })
102
+ }
103
+ finally {
104
+ submitting.value = false
105
+ }
106
+ return
107
+ }
108
+
109
+ // Advance to the resolved next step.
110
+ const target = nextKey.value
111
+ if (target && currentKey.value) {
112
+ history.value.push(currentKey.value)
113
+ currentKey.value = target
114
+ }
115
+ }
116
+
117
+ function onBack() {
118
+ const prev = history.value.pop()
119
+ if (prev) currentKey.value = prev
120
+ }
121
+
122
+ // Progress: position in the visited path (best-effort; branches make total
123
+ // step count indeterminate, so we show visited-count rather than a fixed total).
124
+ const stepPosition = computed(() => history.value.length + 1)
125
+
126
+ defineExpose({ formData, currentKey, nextKey, isTerminal, validateStep })
127
+ </script>
128
+
129
+ <template>
130
+ <div class="form-flow-player">
131
+ <div v-if="currentStep" class="ffp-step">
132
+ <h2 v-if="currentStep.title" class="ffp-title">
133
+ {{ currentStep.title }}
134
+ </h2>
135
+
136
+ <slot
137
+ name="step"
138
+ :step="currentStep"
139
+ :form-data="formData"
140
+ :step-key="currentKey"
141
+ >
142
+ <FormFlow
143
+ v-if="currentSchema"
144
+ ref="formRef"
145
+ v-model="formData"
146
+ :schema="currentSchema"
147
+ :components="components"
148
+ >
149
+ <template v-for="slotName in slotNames" :key="slotName" #[slotName]="slotProps">
150
+ <slot :name="slotName" v-bind="slotProps" />
151
+ </template>
152
+ </FormFlow>
153
+ </slot>
154
+ </div>
155
+
156
+ <div v-else class="ffp-empty">
157
+ <slot name="empty">
158
+ {{ $t?.('multiStep.empty') ?? 'No steps to display.' }}
159
+ </slot>
160
+ </div>
161
+
162
+ <div class="ffp-nav">
163
+ <button
164
+ type="button"
165
+ class="btn-secondary"
166
+ :disabled="!canGoBack"
167
+ @click="onBack"
168
+ >
169
+ {{ $t?.('multiStep.previous') ?? 'Back' }}
170
+ </button>
171
+
172
+ <span class="ffp-counter">{{ stepPosition }}</span>
173
+
174
+ <button
175
+ type="button"
176
+ class="btn-primary"
177
+ :disabled="submitting"
178
+ @click="onPrimary"
179
+ >
180
+ <slot name="primary-label" :is-terminal="isTerminal">
181
+ {{ isTerminal
182
+ ? ($t?.('multiStep.complete') ?? 'Submit')
183
+ : ($t?.('multiStep.next') ?? 'Next') }}
184
+ </slot>
185
+ </button>
186
+ </div>
187
+ </div>
188
+ </template>
189
+
190
+ <style scoped>
191
+ .form-flow-player {
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 1.5rem;
195
+ }
196
+
197
+ .ffp-title {
198
+ margin: 0 0 1rem 0;
199
+ font-size: 1.5rem;
200
+ }
201
+
202
+ .ffp-nav {
203
+ display: flex;
204
+ justify-content: space-between;
205
+ align-items: center;
206
+ gap: 1rem;
207
+ }
208
+
209
+ .ffp-counter {
210
+ font-size: 0.875rem;
211
+ color: #666;
212
+ }
213
+
214
+ button {
215
+ padding: 0.75rem 1.5rem;
216
+ border: none;
217
+ border-radius: 4px;
218
+ font-weight: 600;
219
+ cursor: pointer;
220
+ transition: all 0.2s;
221
+ }
222
+
223
+ .btn-primary {
224
+ background: var(--primary-color, #007bff);
225
+ color: white;
226
+ }
227
+
228
+ .btn-primary:disabled {
229
+ opacity: 0.5;
230
+ cursor: not-allowed;
231
+ }
232
+
233
+ .btn-secondary {
234
+ background: white;
235
+ color: var(--primary-color, #007bff);
236
+ border: 1px solid var(--primary-color, #007bff);
237
+ }
238
+
239
+ .btn-secondary:disabled {
240
+ opacity: 0.5;
241
+ cursor: not-allowed;
242
+ }
243
+ </style>
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Decision-tree multi-step form structure (client mirror of the backend
3
+ * `common/form_submission/src/structure.py` model).
4
+ *
5
+ * A multi-step form is a graph of steps connected by directed edges carrying a
6
+ * SCIM 2.0 condition. From a step, edges are evaluated top-to-bottom and the
7
+ * first whose condition passes (against the accumulated form data) wins.
8
+ * `resolveNext` returning `null` means the current step is terminal (the form
9
+ * is complete) — this drives the reactive Next-vs-Submit button in the player.
10
+ *
11
+ * The client owns navigation; the server exposes the same logic via
12
+ * `POST /form/definitions/{id}/resolve-next` for non-authoritative checks.
13
+ */
14
+
15
+ import { evaluateQuery } from '../utils/queryFilter'
16
+
17
+ /** A single node in the form graph. Owns its own JSON Schema. */
18
+ export interface FormStep {
19
+ key: string
20
+ title?: string
21
+ /** JSON Schema for this step's fields (stored under `schema`). */
22
+ schema: Record<string, any>
23
+ /** Canvas position (designer-only; ignored by the server). */
24
+ x?: number
25
+ y?: number
26
+ }
27
+
28
+ /** A directed edge between two steps with an optional SCIM condition. */
29
+ export interface StepEdge {
30
+ from: string
31
+ to: string
32
+ /** SCIM 2.0 filter over accumulated form data. Empty/undefined => always taken. */
33
+ condition?: string | null
34
+ label?: string | null
35
+ }
36
+
37
+ /** The decision-tree graph for a multi-step form definition. */
38
+ export interface FormStructure {
39
+ /** Explicit start step key; falls back to the first step. */
40
+ startId?: string | null
41
+ steps: FormStep[]
42
+ transitions: StepEdge[]
43
+ }
44
+
45
+ /** An empty structure (single-step / not-yet-built). */
46
+ export function emptyStructure(): FormStructure {
47
+ return { steps: [], transitions: [] }
48
+ }
49
+
50
+ /** A structure is multi-step iff it has more than one node. */
51
+ export function isMultiStep(structure: FormStructure | null | undefined): boolean {
52
+ return !!structure && structure.steps.length > 1
53
+ }
54
+
55
+ /** Look up a step by key. */
56
+ export function stepByKey(structure: FormStructure, key: string | null | undefined): FormStep | undefined {
57
+ if (key == null) return undefined
58
+ return structure.steps.find(s => s.key === key)
59
+ }
60
+
61
+ /** Outgoing edges for a step, in declared (priority) order. */
62
+ export function edgesFrom(structure: FormStructure, key: string): StepEdge[] {
63
+ return structure.transitions.filter(e => e.from === key)
64
+ }
65
+
66
+ /** The entry step key: explicit `startId` if valid, else the first step. */
67
+ export function entryStepKey(structure: FormStructure): string | undefined {
68
+ if (structure.startId && stepByKey(structure, structure.startId)) {
69
+ return structure.startId
70
+ }
71
+ return structure.steps[0]?.key
72
+ }
73
+
74
+ /**
75
+ * Resolve the next step key from `currentKey` given the accumulated answers.
76
+ *
77
+ * First outgoing edge whose SCIM `condition` passes wins (unconditional edges
78
+ * always pass, so declare them last as fallbacks). Returns `null` when no edge
79
+ * matches — the current step is terminal and the form is complete.
80
+ *
81
+ * Mirrors `FormStructure.resolve_next` on the backend.
82
+ */
83
+ export function resolveNext(
84
+ structure: FormStructure,
85
+ currentKey: string,
86
+ formData: Record<string, any>
87
+ ): string | null {
88
+ for (const edge of edgesFrom(structure, currentKey)) {
89
+ if (!edge.condition || edge.condition.trim() === '' || evaluateQuery(edge.condition, formData)) {
90
+ return edge.to
91
+ }
92
+ }
93
+ return null
94
+ }
95
+
96
+ /** Whether a step is terminal (a leaf) given the current answers. */
97
+ export function isTerminal(
98
+ structure: FormStructure,
99
+ currentKey: string,
100
+ formData: Record<string, any>
101
+ ): boolean {
102
+ return resolveNext(structure, currentKey, formData) === null
103
+ }
104
+
105
+ /**
106
+ * Union all step schemas into one flat JSON Schema (later steps win on key
107
+ * collisions; `required` is the union). Mirrors `FormStructure.merged_schema`.
108
+ */
109
+ export function mergedSchema(structure: FormStructure): Record<string, any> {
110
+ const properties: Record<string, any> = {}
111
+ const required: string[] = []
112
+ for (const step of structure.steps) {
113
+ const props = step.schema?.properties
114
+ if (props && typeof props === 'object') Object.assign(properties, props)
115
+ const req = step.schema?.required
116
+ if (Array.isArray(req)) {
117
+ for (const k of req) if (!required.includes(k)) required.push(k)
118
+ }
119
+ }
120
+ const merged: Record<string, any> = { type: 'object', properties }
121
+ if (required.length) merged.required = required
122
+ return merged
123
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,12 @@ export * from './directives'
9
9
  export * from './form-flow/form-flow'
10
10
  export { default as FormFlow } from './form-flow/FormFlow.vue'
11
11
  export { default as MultiStepForm } from './form-flow/MultiStepForm.vue'
12
+ // Decision-tree multi-step forms
13
+ export * from './form-flow/structure'
14
+ export { default as FormFlowPlayer } from './form-flow/FormFlowPlayer.vue'
15
+ export { default as FlowDesigner } from './form-flow/FlowDesigner.vue'
16
+ export { default as FlowNode } from './form-flow/FlowNode.vue'
17
+ export { default as FlowEdge } from './form-flow/FlowEdge.vue'
12
18
  export type { FormFieldType, FormFieldValidation, FormField, JSONSchemaProperty, JSONSchemaObject } from './form-flow/schema-fields'
13
19
  export { defaultRowSpan, inferFieldType, schemaToFields, fieldsToSchema, initFormData, validateFormData, useSchemaToFields } from './form-flow/schema-fields'
14
20
  // i18n exports
@@ -4123,6 +4123,10 @@
4123
4123
  flex-direction: row-reverse;
4124
4124
  }
4125
4125
 
4126
+ .flex-1 {
4127
+ flex: 1;
4128
+ }
4129
+
4126
4130
 
4127
4131
  .column,
4128
4132
  .flex-column {
@@ -4154,6 +4154,10 @@
4154
4154
  flex-direction: row-reverse;
4155
4155
  }
4156
4156
 
4157
+ .m_flex-1 {
4158
+ flex: 1;
4159
+ }
4160
+
4157
4161
  .m_column {
4158
4162
  flex-direction: column;
4159
4163
  }
@@ -1,4 +1,4 @@
1
- import type { BglFormSchemaT } from '@bagelink/vue'
1
+ import type { BglFormSchemaT, IconType } from '@bagelink/vue'
2
2
  import type { MaybeRefOrGetter, ComputedRef } from 'vue'
3
3
 
4
4
  export type SortDirectionsT = 'ASC' | 'DESC'
@@ -12,6 +12,12 @@ export interface TableSchemaProps<T extends { [key: string]: any } = { [key: str
12
12
  selectable?: boolean
13
13
  sortable?: boolean | { handle?: string }
14
14
  onLastItemVisible?: () => void
15
+ /** Icon for the built-in empty state (default: 'inbox'). Pass 'none' to hide. */
16
+ emptyIcon?: IconType | 'none'
17
+ /** Title for the built-in empty state (default: 'No results'). */
18
+ emptyTitle?: string
19
+ /** Description shown under the empty-state title. */
20
+ emptyDescription?: string
15
21
  }
16
22
 
17
23
  export interface SortingOptions {