@statezero/core 0.2.34 → 0.2.37

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.
@@ -0,0 +1,361 @@
1
+ <script setup>
2
+ import { computed, provide, inject, onMounted, unref } from 'vue'
3
+
4
+ /**
5
+ * LayoutRenderer - Renders a statezero layout tree
6
+ *
7
+ * Expects a components registry with implementations for each element type.
8
+ * Components must follow contracts (validated at mount time).
9
+ */
10
+
11
+ const props = defineProps({
12
+ // The layout element to render (root or nested)
13
+ layout: { type: Object, required: true },
14
+
15
+ // Schema with field definitions (input_properties/properties)
16
+ schema: { type: Object, default: () => ({}) },
17
+
18
+ // Component registry: { Control, Display, Alert, Label, Divider, Group, Tabs, ErrorBlock }
19
+ components: { type: Object, required: true },
20
+
21
+ // Form data (v-model for Controls)
22
+ formData: { type: Object, default: () => ({}) },
23
+
24
+ // Workflow context (for Display elements)
25
+ context: { type: Object, default: () => ({}) },
26
+
27
+ // Errors object - can be raw DRF response: { fieldName: ['error'], non_field_errors: ['global error'] }
28
+ // The renderer automatically extracts non_field_errors/__all__ for the ErrorBlock
29
+ errors: { type: Object, default: () => ({}) },
30
+
31
+ // Explicit non-field errors (optional override - if not provided, extracted from errors.non_field_errors)
32
+ nonFieldErrors: { type: Array, default: null },
33
+
34
+ // Whether this is the root renderer (internal)
35
+ isRoot: { type: Boolean, default: true }
36
+ })
37
+
38
+ const emit = defineEmits(['update:formData'])
39
+
40
+ // Extract non-field errors from the errors object (DRF format)
41
+ // Supports both "non_field_errors" and "__all__" keys
42
+ const extractedNonFieldErrors = computed(() => {
43
+ // If explicit nonFieldErrors prop provided, use it
44
+ if (props.nonFieldErrors !== null) {
45
+ return props.nonFieldErrors
46
+ }
47
+ // Otherwise extract from errors object
48
+ const nfe = props.errors?.non_field_errors || props.errors?.__all__
49
+ if (!nfe) return []
50
+ if (Array.isArray(nfe)) {
51
+ return nfe.map(err => {
52
+ if (typeof err === 'string') return err
53
+ if (err?.message) return err.message
54
+ return String(err)
55
+ })
56
+ }
57
+ return [String(nfe)]
58
+ })
59
+
60
+ const rootComponents = computed(() => props.components)
61
+ const rootSchema = computed(() => props.schema)
62
+ const rootFormData = computed(() => props.formData)
63
+ const rootContext = computed(() => props.context)
64
+ const rootErrors = computed(() => props.errors)
65
+
66
+ // Provide context to nested renderers
67
+ if (props.isRoot) {
68
+ provide('layoutRenderer', {
69
+ components: rootComponents,
70
+ schema: rootSchema,
71
+ formData: rootFormData,
72
+ context: rootContext,
73
+ errors: rootErrors,
74
+ updateField: (fieldName, value) => {
75
+ const currentFormData = unref(rootFormData) || {}
76
+ emit('update:formData', { ...currentFormData, [fieldName]: value })
77
+ }
78
+ })
79
+ }
80
+
81
+ // Inject from parent if not root
82
+ const injected = props.isRoot ? null : inject('layoutRenderer', null)
83
+ const components = computed(() => props.isRoot ? props.components : (injected ? (unref(injected.components) || props.components) : props.components))
84
+ const schema = computed(() => props.isRoot ? props.schema : (injected ? (unref(injected.schema) || props.schema) : props.schema))
85
+ const formData = computed(() => props.isRoot ? props.formData : (injected ? (unref(injected.formData) || props.formData) : props.formData))
86
+ const context = computed(() => props.isRoot ? props.context : (injected ? (unref(injected.context) || props.context) : props.context))
87
+ const errors = computed(() => props.isRoot ? props.errors : (injected ? (unref(injected.errors) || props.errors) : props.errors))
88
+ const updateField = props.isRoot
89
+ ? (fieldName, value) => emit('update:formData', { ...props.formData, [fieldName]: value })
90
+ : injected?.updateField || (() => {})
91
+
92
+ // Contract definitions for component validation
93
+ //
94
+ // Control: Polymorphic form field component (like AutoField)
95
+ // - Should check element.display_component for custom component override
96
+ // - Should use schema (type/format) to determine default field rendering
97
+ // - Receives: element (includes field_name, display_component, label, extra, etc.)
98
+ // modelValue, errors (array), context, formData, schema (field schema)
99
+ // - Emits: update:modelValue
100
+ //
101
+ // Display: Read-only component for showing context values
102
+ // - Receives: element (includes context_path, label, display_component), context, value (resolved)
103
+ // - No v-model binding, just displays value
104
+ //
105
+ const CONTRACTS = {
106
+ Control: {
107
+ props: ['element', 'modelValue', 'errors', 'context', 'formData', 'schema'],
108
+ emits: ['update:modelValue']
109
+ },
110
+ Display: {
111
+ props: ['element', 'context', 'value'],
112
+ emits: []
113
+ },
114
+ Alert: {
115
+ props: ['element', 'context', 'text'],
116
+ emits: []
117
+ },
118
+ Label: {
119
+ props: ['element'],
120
+ emits: []
121
+ },
122
+ Divider: {
123
+ props: [],
124
+ emits: []
125
+ },
126
+ Group: {
127
+ props: ['element'],
128
+ emits: []
129
+ },
130
+ Tabs: {
131
+ props: ['element'],
132
+ emits: []
133
+ },
134
+ ErrorBlock: {
135
+ props: ['errors'],
136
+ emits: []
137
+ }
138
+ }
139
+
140
+ // Validate components on mount (dev warning only)
141
+ function validateComponents(comps) {
142
+ for (const [type, component] of Object.entries(comps)) {
143
+ const contract = CONTRACTS[type]
144
+ if (!contract) continue
145
+ if (!component) {
146
+ console.warn(`[LayoutRenderer] Missing component for type: ${type}`)
147
+ continue
148
+ }
149
+
150
+ // Get component props (handle different formats)
151
+ let componentProps = []
152
+ if (component.props) {
153
+ if (Array.isArray(component.props)) {
154
+ componentProps = component.props
155
+ } else if (typeof component.props === 'object') {
156
+ componentProps = Object.keys(component.props)
157
+ }
158
+ }
159
+
160
+ // Get component emits
161
+ let componentEmits = []
162
+ if (component.emits) {
163
+ if (Array.isArray(component.emits)) {
164
+ componentEmits = component.emits
165
+ } else if (typeof component.emits === 'object') {
166
+ componentEmits = Object.keys(component.emits)
167
+ }
168
+ }
169
+
170
+ // Check required props
171
+ for (const prop of contract.props) {
172
+ if (!componentProps.includes(prop)) {
173
+ console.warn(`[LayoutRenderer] ${type} component missing required prop: "${prop}"`)
174
+ }
175
+ }
176
+
177
+ // Check required emits
178
+ for (const emitName of contract.emits) {
179
+ if (!componentEmits.includes(emitName)) {
180
+ console.warn(`[LayoutRenderer] ${type} component missing required emit: "${emitName}"`)
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ onMounted(() => {
187
+ if (props.isRoot && import.meta.env?.DEV) {
188
+ validateComponents(props.components)
189
+ }
190
+ })
191
+
192
+ // Evaluate conditional expressions
193
+ function evalCondition(expr) {
194
+ try {
195
+ return new Function('formData', 'context', `return ${expr}`)(formData.value, context.value)
196
+ } catch (e) {
197
+ console.warn('[LayoutRenderer] Conditional eval failed:', expr, e)
198
+ return false
199
+ }
200
+ }
201
+
202
+ // Get field schema from schema.properties or schema.input_properties
203
+ function getFieldSchema(fieldName) {
204
+ const props = schema.value?.properties || schema.value?.input_properties || {}
205
+ return props[fieldName] || {}
206
+ }
207
+
208
+ // Get errors for a specific field (excludes non_field_errors/__all__)
209
+ function getFieldErrors(fieldName) {
210
+ if (fieldName === 'non_field_errors' || fieldName === '__all__') return []
211
+ const fieldErrors = errors.value?.[fieldName]
212
+ if (!fieldErrors) return []
213
+ // Normalize to array of strings (DRF can return array of strings or objects with message)
214
+ if (Array.isArray(fieldErrors)) {
215
+ return fieldErrors.map(err => {
216
+ if (typeof err === 'string') return err
217
+ if (err?.message) return err.message
218
+ return String(err)
219
+ })
220
+ }
221
+ return [String(fieldErrors)]
222
+ }
223
+
224
+ // Resolve context path (dot notation) to value
225
+ function resolveContextPath(path) {
226
+ if (!path) return undefined
227
+ const parts = path.split('.')
228
+ let value = context.value
229
+ for (const part of parts) {
230
+ if (value === null || value === undefined) return undefined
231
+ value = value[part]
232
+ }
233
+ return value
234
+ }
235
+
236
+ </script>
237
+
238
+ <template>
239
+ <!-- ErrorBlock at root level -->
240
+ <component
241
+ v-if="isRoot && extractedNonFieldErrors.length > 0 && components.ErrorBlock"
242
+ :is="components.ErrorBlock"
243
+ :errors="extractedNonFieldErrors"
244
+ />
245
+
246
+ <!-- VerticalLayout -->
247
+ <div
248
+ v-if="layout.type === 'VerticalLayout'"
249
+ class="sz-layout sz-layout-vertical"
250
+ :class="`sz-gap-${layout.gap || 'md'}`"
251
+ >
252
+ <LayoutRenderer
253
+ v-for="(child, i) in layout.elements"
254
+ :key="i"
255
+ :layout="child"
256
+ :components="components"
257
+ :is-root="false"
258
+ />
259
+ </div>
260
+
261
+ <!-- HorizontalLayout -->
262
+ <div
263
+ v-else-if="layout.type === 'HorizontalLayout'"
264
+ class="sz-layout sz-layout-horizontal"
265
+ :class="[`sz-gap-${layout.gap || 'md'}`, `sz-align-${layout.align || 'start'}`]"
266
+ >
267
+ <LayoutRenderer
268
+ v-for="(child, i) in layout.elements"
269
+ :key="i"
270
+ :layout="child"
271
+ :components="components"
272
+ :is-root="false"
273
+ />
274
+ </div>
275
+
276
+ <!-- Group -->
277
+ <component
278
+ v-else-if="layout.type === 'Group'"
279
+ :is="components.Group"
280
+ :element="layout"
281
+ >
282
+ <LayoutRenderer
283
+ v-if="layout.layout"
284
+ :layout="layout.layout"
285
+ :components="components"
286
+ :is-root="false"
287
+ />
288
+ </component>
289
+
290
+ <!-- Tabs -->
291
+ <component
292
+ v-else-if="layout.type === 'Tabs'"
293
+ :is="components.Tabs"
294
+ :element="layout"
295
+ >
296
+ <template #tab="{ tab }">
297
+ <LayoutRenderer
298
+ :layout="tab.layout"
299
+ :components="components"
300
+ :is-root="false"
301
+ />
302
+ </template>
303
+ </component>
304
+
305
+ <!-- Conditional -->
306
+ <LayoutRenderer
307
+ v-else-if="layout.type === 'Conditional' && evalCondition(layout.when)"
308
+ :layout="layout.layout"
309
+ :components="components"
310
+ :is-root="false"
311
+ />
312
+
313
+ <!-- Control (form field) -->
314
+ <component
315
+ v-else-if="layout.type === 'Control'"
316
+ :is="components.Control"
317
+ :element="layout"
318
+ :model-value="formData[layout.field_name]"
319
+ :errors="getFieldErrors(layout.field_name)"
320
+ :context="context"
321
+ :form-data="formData"
322
+ :schema="getFieldSchema(layout.field_name)"
323
+ @update:model-value="updateField(layout.field_name, $event)"
324
+ />
325
+
326
+ <!-- Display -->
327
+ <component
328
+ v-else-if="layout.type === 'Display'"
329
+ :is="components.Display"
330
+ :element="layout"
331
+ :context="context"
332
+ :value="resolveContextPath(layout.context_path)"
333
+ />
334
+
335
+ <!-- Alert -->
336
+ <component
337
+ v-else-if="layout.type === 'Alert'"
338
+ :is="components.Alert"
339
+ :element="layout"
340
+ :context="context"
341
+ :text="layout.text || resolveContextPath(layout.context_path)"
342
+ />
343
+
344
+ <!-- Label -->
345
+ <component
346
+ v-else-if="layout.type === 'Label'"
347
+ :is="components.Label"
348
+ :element="layout"
349
+ />
350
+
351
+ <!-- Divider -->
352
+ <component
353
+ v-else-if="layout.type === 'Divider'"
354
+ :is="components.Divider"
355
+ />
356
+
357
+ <!-- Unknown type warning -->
358
+ <div v-else class="text-red-500 text-sm">
359
+ [LayoutRenderer] Unknown element type: {{ layout.type }}
360
+ </div>
361
+ </template>
@@ -0,0 +1,38 @@
1
+ <script setup>
2
+ /**
3
+ * Default Alert element for LayoutRenderer
4
+ *
5
+ * Contract:
6
+ * - props: element, context, text
7
+ * - emits: none
8
+ */
9
+ defineProps({
10
+ element: { type: Object, required: true },
11
+ context: { type: Object, default: () => ({}) },
12
+ text: { type: String, default: '' }
13
+ })
14
+
15
+ const severityClasses = {
16
+ info: 'bg-blue-500/10 border-blue-500/20 text-blue-400',
17
+ warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400',
18
+ error: 'bg-red-500/10 border-red-500/20 text-red-400',
19
+ success: 'bg-green-500/10 border-green-500/20 text-green-400'
20
+ }
21
+
22
+ const severityIcons = {
23
+ info: 'ℹ️',
24
+ warning: '⚠️',
25
+ error: '❌',
26
+ success: '✓'
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <div
32
+ class="flex items-start gap-3 p-3 rounded-lg border"
33
+ :class="severityClasses[element.severity] || severityClasses.info"
34
+ >
35
+ <span class="flex-shrink-0">{{ severityIcons[element.severity] || severityIcons.info }}</span>
36
+ <p class="text-sm">{{ text }}</p>
37
+ </div>
38
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup>
2
+ /**
3
+ * Default Display element for LayoutRenderer
4
+ *
5
+ * Renders context data in a display-only format.
6
+ * For custom rendering, use element.display_component and register custom components.
7
+ *
8
+ * Contract:
9
+ * - props: element, context, value
10
+ * - emits: none
11
+ */
12
+ defineProps({
13
+ element: { type: Object, required: true },
14
+ context: { type: Object, default: () => ({}) },
15
+ value: { default: null }
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <div class="space-y-1">
21
+ <label v-if="element.label" class="block text-sm font-medium text-muted-foreground">
22
+ {{ element.label }}
23
+ </label>
24
+ <div class="text-sm">
25
+ <!-- Default text display -->
26
+ <template v-if="!element.display_component || element.display_component === 'text'">
27
+ <span v-if="value !== null && value !== undefined">{{ value }}</span>
28
+ <span v-else class="text-muted-foreground italic">—</span>
29
+ </template>
30
+
31
+ <!-- Code display -->
32
+ <template v-else-if="element.display_component === 'code'">
33
+ <code class="px-2 py-1 bg-muted rounded font-mono text-sm">{{ value }}</code>
34
+ </template>
35
+
36
+ <!-- Copy URL display -->
37
+ <template v-else-if="element.display_component === 'copy-url'">
38
+ <div class="flex items-center gap-2">
39
+ <code class="flex-1 px-2 py-1 bg-muted rounded font-mono text-xs truncate">{{ value }}</code>
40
+ <button
41
+ type="button"
42
+ class="px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90"
43
+ @click="navigator.clipboard.writeText(value)"
44
+ >
45
+ Copy
46
+ </button>
47
+ </div>
48
+ </template>
49
+
50
+ <!-- Fallback for unknown display_component -->
51
+ <template v-else>
52
+ <span>{{ value }}</span>
53
+ <span class="text-xs text-muted-foreground ml-2">({{ element.display_component }})</span>
54
+ </template>
55
+ </div>
56
+ </div>
57
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup>
2
+ /**
3
+ * Default Divider element for LayoutRenderer
4
+ *
5
+ * Contract:
6
+ * - props: none required
7
+ * - emits: none
8
+ */
9
+ </script>
10
+
11
+ <template>
12
+ <hr class="border-t border-border my-2" />
13
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup>
2
+ /**
3
+ * Default ErrorBlock element for LayoutRenderer
4
+ *
5
+ * Displays non-field errors (global form errors).
6
+ *
7
+ * Contract:
8
+ * - props: errors (array of error strings)
9
+ * - emits: none
10
+ */
11
+ defineProps({
12
+ errors: { type: Array, required: true }
13
+ })
14
+ </script>
15
+
16
+ <template>
17
+ <div
18
+ v-if="errors.length > 0"
19
+ class="flex items-start gap-3 p-3 rounded-lg border bg-red-500/10 border-red-500/20"
20
+ >
21
+ <span class="flex-shrink-0 text-red-400">❌</span>
22
+ <div class="space-y-1">
23
+ <p v-for="(error, i) in errors" :key="i" class="text-sm text-red-400">
24
+ {{ error }}
25
+ </p>
26
+ </div>
27
+ </div>
28
+ </template>
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ import { ref } from 'vue'
3
+
4
+ /**
5
+ * Default Group element for LayoutRenderer
6
+ *
7
+ * A labeled section container. Children are rendered via default slot.
8
+ *
9
+ * Contract:
10
+ * - props: element
11
+ * - emits: none
12
+ * - slot: default (for nested layout content)
13
+ */
14
+ const props = defineProps({
15
+ element: { type: Object, required: true }
16
+ })
17
+
18
+ const isCollapsed = ref(props.element.collapsed || false)
19
+
20
+ function toggleCollapse() {
21
+ if (props.element.collapsible) {
22
+ isCollapsed.value = !isCollapsed.value
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <div class="border border-border rounded-lg overflow-hidden">
29
+ <!-- Header -->
30
+ <div
31
+ class="px-4 py-3 bg-muted/50"
32
+ :class="{ 'cursor-pointer hover:bg-muted/70': element.collapsible }"
33
+ @click="toggleCollapse"
34
+ >
35
+ <div class="flex items-center justify-between">
36
+ <div>
37
+ <h4 class="font-medium">{{ element.label }}</h4>
38
+ <p v-if="element.description" class="text-sm text-muted-foreground mt-0.5">
39
+ {{ element.description }}
40
+ </p>
41
+ </div>
42
+ <span v-if="element.collapsible" class="text-muted-foreground">
43
+ {{ isCollapsed ? '▶' : '▼' }}
44
+ </span>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Content -->
49
+ <div v-show="!isCollapsed" class="p-4">
50
+ <slot />
51
+ </div>
52
+ </div>
53
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup>
2
+ /**
3
+ * Default Label element for LayoutRenderer
4
+ *
5
+ * Contract:
6
+ * - props: element
7
+ * - emits: none
8
+ */
9
+ defineProps({
10
+ element: { type: Object, required: true }
11
+ })
12
+
13
+ const variantClasses = {
14
+ heading: 'text-lg font-semibold',
15
+ subheading: 'text-base font-medium text-muted-foreground',
16
+ body: 'text-sm',
17
+ caption: 'text-xs text-muted-foreground'
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <p :class="variantClasses[element.variant] || variantClasses.body">
23
+ {{ element.text }}
24
+ </p>
25
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup>
2
+ import { ref } from 'vue'
3
+
4
+ /**
5
+ * Default Tabs element for LayoutRenderer
6
+ *
7
+ * A tabbed container. Tab content is rendered via scoped slot.
8
+ *
9
+ * Contract:
10
+ * - props: element
11
+ * - emits: none
12
+ * - slot: #tab="{ tab }" (for rendering each tab's content)
13
+ */
14
+ const props = defineProps({
15
+ element: { type: Object, required: true }
16
+ })
17
+
18
+ const activeTab = ref(props.element.default_tab || 0)
19
+
20
+ function selectTab(index) {
21
+ activeTab.value = index
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div>
27
+ <!-- Tab buttons -->
28
+ <div class="flex border-b border-border">
29
+ <button
30
+ v-for="(tab, index) in element.tabs"
31
+ :key="index"
32
+ type="button"
33
+ class="px-4 py-2 text-sm font-medium transition-colors"
34
+ :class="[
35
+ activeTab === index
36
+ ? 'text-primary border-b-2 border-primary -mb-px'
37
+ : 'text-muted-foreground hover:text-foreground'
38
+ ]"
39
+ @click="selectTab(index)"
40
+ >
41
+ {{ tab.label }}
42
+ </button>
43
+ </div>
44
+
45
+ <!-- Tab content -->
46
+ <div class="pt-4">
47
+ <template v-for="(tab, index) in element.tabs" :key="index">
48
+ <div v-show="activeTab === index">
49
+ <slot name="tab" :tab="tab" :index="index" />
50
+ </div>
51
+ </template>
52
+ </div>
53
+ </div>
54
+ </template>
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Create a default components registry (without Control - user must provide)
3
+ *
4
+ * @param {Object} options - Override specific components
5
+ * @returns {Object} Components registry for LayoutRenderer
6
+ */
7
+ export function createDefaultComponents(options?: Object): Object;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Default component implementations for LayoutRenderer
3
+ *
4
+ * These are minimal, unstyled implementations that follow the contracts.
5
+ * Users can use these as-is or as reference for custom implementations.
6
+ */
7
+ export { default as AlertElement } from './AlertElement.vue';
8
+ export { default as LabelElement } from './LabelElement.vue';
9
+ export { default as DividerElement } from './DividerElement.vue';
10
+ export { default as DisplayElement } from './DisplayElement.vue';
11
+ export { default as GroupElement } from './GroupElement.vue';
12
+ export { default as TabsElement } from './TabsElement.vue';
13
+ export { default as ErrorBlock } from './ErrorBlock.vue';
14
+ /**
15
+ * Create a default components registry (without Control - user must provide)
16
+ *
17
+ * @param {Object} options - Override specific components
18
+ * @returns {Object} Components registry for LayoutRenderer
19
+ */
20
+ export function createDefaultComponents(options = {}) {
21
+ return {
22
+ Alert: AlertElement,
23
+ Label: LabelElement,
24
+ Divider: DividerElement,
25
+ Display: DisplayElement,
26
+ Group: GroupElement,
27
+ Tabs: TabsElement,
28
+ ErrorBlock: ErrorBlock,
29
+ ...options
30
+ };
31
+ }
@@ -0,0 +1 @@
1
+ export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from "./defaults/index.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Vue components for StateZero
3
+ */
4
+ // Main layout renderer
5
+ export { default as LayoutRenderer } from './LayoutRenderer.vue';
6
+ // Default component implementations
7
+ export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './defaults/index.js';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * StateZero Layout Renderer - Base CSS Styles
3
+ *
4
+ * Import this file to get default styling for layout components:
5
+ * import '@statezero/core/vue/layout.css'
6
+ *
7
+ * Or define your own styles targeting these class names.
8
+ */
9
+
10
+ /* Layout containers */
11
+ .sz-layout {
12
+ display: flex;
13
+ }
14
+
15
+ .sz-layout-vertical {
16
+ flex-direction: column;
17
+ }
18
+
19
+ .sz-layout-horizontal {
20
+ flex-direction: row;
21
+ }
22
+
23
+ /* Gap utilities */
24
+ .sz-gap-sm {
25
+ gap: 0.5rem;
26
+ }
27
+
28
+ .sz-gap-md {
29
+ gap: 1rem;
30
+ }
31
+
32
+ .sz-gap-lg {
33
+ gap: 1.5rem;
34
+ }
35
+
36
+ /* Horizontal alignment */
37
+ .sz-align-start {
38
+ align-items: flex-start;
39
+ }
40
+
41
+ .sz-align-center {
42
+ align-items: center;
43
+ }
44
+
45
+ .sz-align-end {
46
+ align-items: flex-end;
47
+ }
48
+
49
+ .sz-align-stretch {
50
+ align-items: stretch;
51
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * StateZero Layout Renderer - Tailwind CSS Styles
3
+ *
4
+ * Import this file if you're using Tailwind CSS:
5
+ * import '@statezero/core/vue/layout.tailwind.css'
6
+ *
7
+ * This maps sz-* classes to Tailwind utilities via @apply.
8
+ */
9
+
10
+ /* Layout containers */
11
+ .sz-layout {
12
+ @apply flex;
13
+ }
14
+
15
+ .sz-layout-vertical {
16
+ @apply flex-col;
17
+ }
18
+
19
+ .sz-layout-horizontal {
20
+ @apply flex-row;
21
+ }
22
+
23
+ /* Gap utilities */
24
+ .sz-gap-sm {
25
+ @apply gap-2;
26
+ }
27
+
28
+ .sz-gap-md {
29
+ @apply gap-4;
30
+ }
31
+
32
+ .sz-gap-lg {
33
+ @apply gap-6;
34
+ }
35
+
36
+ /* Horizontal alignment */
37
+ .sz-align-start {
38
+ @apply items-start;
39
+ }
40
+
41
+ .sz-align-center {
42
+ @apply items-center;
43
+ }
44
+
45
+ .sz-align-end {
46
+ @apply items-end;
47
+ }
48
+
49
+ .sz-align-stretch {
50
+ @apply items-stretch;
51
+ }
@@ -1,2 +1,3 @@
1
1
  export { useQueryset, querysets } from "./composables.js";
2
2
  export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from "./reactivity.js";
3
+ export { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from "./components/index.js";
@@ -1,2 +1,4 @@
1
1
  export { useQueryset, querysets } from './composables.js';
2
2
  export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from './reactivity.js';
3
+ // Layout components
4
+ export { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './components/index.js';
@@ -3,4 +3,13 @@ import { QuerySetAdaptor } from './adaptors/vue/index.js';
3
3
  import { MetricAdaptor } from './adaptors/vue/index.js';
4
4
  import { useQueryset } from './adaptors/vue/index.js';
5
5
  import { querysets } from './adaptors/vue/index.js';
6
- export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets };
6
+ import { LayoutRenderer } from './adaptors/vue/index.js';
7
+ import { AlertElement } from './adaptors/vue/index.js';
8
+ import { LabelElement } from './adaptors/vue/index.js';
9
+ import { DividerElement } from './adaptors/vue/index.js';
10
+ import { DisplayElement } from './adaptors/vue/index.js';
11
+ import { GroupElement } from './adaptors/vue/index.js';
12
+ import { TabsElement } from './adaptors/vue/index.js';
13
+ import { ErrorBlock } from './adaptors/vue/index.js';
14
+ import { createDefaultComponents } from './adaptors/vue/index.js';
15
+ export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets, LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
package/dist/vue-entry.js CHANGED
@@ -1,2 +1,7 @@
1
1
  import { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets } from './adaptors/vue/index.js';
2
- export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets };
2
+ import { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './adaptors/vue/index.js';
3
+ export {
4
+ // Reactivity
5
+ ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets,
6
+ // Layout components
7
+ LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.34",
3
+ "version": "0.2.37",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",
@@ -26,11 +26,14 @@
26
26
  "import": "./dist/vue-entry.js",
27
27
  "require": "./dist/vue-entry.js"
28
28
  },
29
+ "./vue/layout.css": "./dist/adaptors/vue/components/layout.css",
30
+ "./vue/layout.tailwind.css": "./dist/adaptors/vue/components/layout.tailwind.css",
29
31
  "./testing": {
30
32
  "import": "./dist/testing.js",
31
33
  "require": "./dist/testing.js"
32
34
  },
33
- "./dist/*": "./dist/*"
35
+ "./dist/*": "./dist/*",
36
+ "./src/*": "./src/*"
34
37
  },
35
38
  "scripts": {
36
39
  "test": "vitest run --config=vitest.base.config.ts",
@@ -39,7 +42,8 @@
39
42
  "generate:test-apps": "ts-node scripts/generate-test-apps.js",
40
43
  "test:adaptors": "playwright test tests/adaptors",
41
44
  "test:coverage": "vitest run --coverage",
42
- "build": "tsc",
45
+ "build": "tsc && npm run copy-vue",
46
+ "copy-vue": "cp -r src/adaptors/vue/components/*.vue dist/adaptors/vue/components/ && cp -r src/adaptors/vue/components/defaults/*.vue dist/adaptors/vue/components/defaults/ && cp src/adaptors/vue/components/*.css dist/adaptors/vue/components/",
43
47
  "parse-queries": "node scripts/perfect-query-parser.js",
44
48
  "sync": "node src/cli/index.js sync",
45
49
  "sync:dev": "npx cross-env NODE_ENV=test npm run sync",