@zentto/studio 0.1.0

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 (93) hide show
  1. package/dist/designer/zs-app-wizard.d.ts +33 -0
  2. package/dist/designer/zs-app-wizard.d.ts.map +1 -0
  3. package/dist/designer/zs-app-wizard.js +493 -0
  4. package/dist/designer/zs-app-wizard.js.map +1 -0
  5. package/dist/designer/zs-page-designer.d.ts +30 -0
  6. package/dist/designer/zs-page-designer.d.ts.map +1 -0
  7. package/dist/designer/zs-page-designer.js +466 -0
  8. package/dist/designer/zs-page-designer.js.map +1 -0
  9. package/dist/fields/zs-field-address.d.ts +28 -0
  10. package/dist/fields/zs-field-address.d.ts.map +1 -0
  11. package/dist/fields/zs-field-address.js +129 -0
  12. package/dist/fields/zs-field-address.js.map +1 -0
  13. package/dist/fields/zs-field-chart.d.ts +27 -0
  14. package/dist/fields/zs-field-chart.d.ts.map +1 -0
  15. package/dist/fields/zs-field-chart.js +192 -0
  16. package/dist/fields/zs-field-chart.js.map +1 -0
  17. package/dist/fields/zs-field-checkbox.d.ts +33 -0
  18. package/dist/fields/zs-field-checkbox.d.ts.map +1 -0
  19. package/dist/fields/zs-field-checkbox.js +172 -0
  20. package/dist/fields/zs-field-checkbox.js.map +1 -0
  21. package/dist/fields/zs-field-chips.d.ts +38 -0
  22. package/dist/fields/zs-field-chips.d.ts.map +1 -0
  23. package/dist/fields/zs-field-chips.js +257 -0
  24. package/dist/fields/zs-field-chips.js.map +1 -0
  25. package/dist/fields/zs-field-date.d.ts +23 -0
  26. package/dist/fields/zs-field-date.d.ts.map +1 -0
  27. package/dist/fields/zs-field-date.js +98 -0
  28. package/dist/fields/zs-field-date.js.map +1 -0
  29. package/dist/fields/zs-field-file.d.ts +26 -0
  30. package/dist/fields/zs-field-file.d.ts.map +1 -0
  31. package/dist/fields/zs-field-file.js +190 -0
  32. package/dist/fields/zs-field-file.js.map +1 -0
  33. package/dist/fields/zs-field-heading.d.ts +14 -0
  34. package/dist/fields/zs-field-heading.d.ts.map +1 -0
  35. package/dist/fields/zs-field-heading.js +54 -0
  36. package/dist/fields/zs-field-heading.js.map +1 -0
  37. package/dist/fields/zs-field-html.d.ts +12 -0
  38. package/dist/fields/zs-field-html.d.ts.map +1 -0
  39. package/dist/fields/zs-field-html.js +38 -0
  40. package/dist/fields/zs-field-html.js.map +1 -0
  41. package/dist/fields/zs-field-lookup.d.ts +37 -0
  42. package/dist/fields/zs-field-lookup.d.ts.map +1 -0
  43. package/dist/fields/zs-field-lookup.js +204 -0
  44. package/dist/fields/zs-field-lookup.js.map +1 -0
  45. package/dist/fields/zs-field-media.d.ts +15 -0
  46. package/dist/fields/zs-field-media.d.ts.map +1 -0
  47. package/dist/fields/zs-field-media.js +59 -0
  48. package/dist/fields/zs-field-media.js.map +1 -0
  49. package/dist/fields/zs-field-number.d.ts +35 -0
  50. package/dist/fields/zs-field-number.d.ts.map +1 -0
  51. package/dist/fields/zs-field-number.js +192 -0
  52. package/dist/fields/zs-field-number.js.map +1 -0
  53. package/dist/fields/zs-field-select.d.ts +34 -0
  54. package/dist/fields/zs-field-select.d.ts.map +1 -0
  55. package/dist/fields/zs-field-select.js +237 -0
  56. package/dist/fields/zs-field-select.js.map +1 -0
  57. package/dist/fields/zs-field-separator.d.ts +12 -0
  58. package/dist/fields/zs-field-separator.d.ts.map +1 -0
  59. package/dist/fields/zs-field-separator.js +37 -0
  60. package/dist/fields/zs-field-separator.js.map +1 -0
  61. package/dist/fields/zs-field-signature.d.ts +34 -0
  62. package/dist/fields/zs-field-signature.d.ts.map +1 -0
  63. package/dist/fields/zs-field-signature.js +191 -0
  64. package/dist/fields/zs-field-signature.js.map +1 -0
  65. package/dist/fields/zs-field-switch.d.ts +19 -0
  66. package/dist/fields/zs-field-switch.d.ts.map +1 -0
  67. package/dist/fields/zs-field-switch.js +100 -0
  68. package/dist/fields/zs-field-switch.js.map +1 -0
  69. package/dist/fields/zs-field-text.d.ts +24 -0
  70. package/dist/fields/zs-field-text.d.ts.map +1 -0
  71. package/dist/fields/zs-field-text.js +115 -0
  72. package/dist/fields/zs-field-text.js.map +1 -0
  73. package/dist/fields/zs-field-treeview.d.ts +37 -0
  74. package/dist/fields/zs-field-treeview.d.ts.map +1 -0
  75. package/dist/fields/zs-field-treeview.js +234 -0
  76. package/dist/fields/zs-field-treeview.js.map +1 -0
  77. package/dist/styles/tokens.d.ts +3 -0
  78. package/dist/styles/tokens.d.ts.map +1 -0
  79. package/dist/styles/tokens.js +159 -0
  80. package/dist/styles/tokens.js.map +1 -0
  81. package/dist/zentto-studio-app.d.ts +55 -0
  82. package/dist/zentto-studio-app.d.ts.map +1 -0
  83. package/dist/zentto-studio-app.js +748 -0
  84. package/dist/zentto-studio-app.js.map +1 -0
  85. package/dist/zentto-studio-designer.d.ts +13 -0
  86. package/dist/zentto-studio-designer.d.ts.map +1 -0
  87. package/dist/zentto-studio-designer.js +75 -0
  88. package/dist/zentto-studio-designer.js.map +1 -0
  89. package/dist/zentto-studio-renderer.d.ts +53 -0
  90. package/dist/zentto-studio-renderer.d.ts.map +1 -0
  91. package/dist/zentto-studio-renderer.js +636 -0
  92. package/dist/zentto-studio-renderer.js.map +1 -0
  93. package/package.json +56 -0
@@ -0,0 +1,748 @@
1
+ // @zentto/studio — Full Application Shell Web Component
2
+ // <zentto-studio-app> is a mini-framework: give it a JSON config, get a full app
3
+ // Sidebar + Header + Router + Pages + Data Sources + Forms + Grids + Reports
4
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
5
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
7
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
8
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
9
+ };
10
+ import { LitElement, html, css, nothing } from 'lit';
11
+ import { customElement, property, state } from 'lit/decorators.js';
12
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
13
+ import { studioTokens } from './styles/tokens.js';
14
+ import { EventBus, fetchAllDataSources, } from '@zentto/studio-core';
15
+ // Import renderer (which imports all field components)
16
+ import './zentto-studio-renderer.js';
17
+ let ZenttoStudioApp = class ZenttoStudioApp extends LitElement {
18
+ constructor() {
19
+ super(...arguments);
20
+ // ─── Properties ───────────────────────────────────
21
+ this.config = null;
22
+ this.customCss = ''; // inject custom CSS string
23
+ this.cssVars = {}; // override --zs-* tokens
24
+ // ─── Internal State ───────────────────────────────
25
+ this.currentSegment = '';
26
+ this.sidebarCollapsed = false;
27
+ this.expandedNavItems = new Set();
28
+ this.pageData = {};
29
+ this.pageLoading = false;
30
+ this.renderKey = 0;
31
+ this.eventBus = new EventBus();
32
+ this.history = [];
33
+ // ─── Tabs Page ────────────────────────────────────
34
+ this.activeTab = 0;
35
+ }
36
+ static { this.styles = [studioTokens, css `
37
+ :host { display: block; height: 100%; font-family: var(--zs-font-family); }
38
+
39
+ /* ─── Shell Layout ──────────────────────────── */
40
+ .zs-app { display: flex; height: 100%; overflow: hidden; background: var(--zs-bg-secondary); }
41
+
42
+ /* ─── Sidebar ───────────────────────────────── */
43
+ .zs-sidebar {
44
+ width: 260px; min-width: 260px;
45
+ background: #1e1e2d;
46
+ color: #a2a3b7;
47
+ display: flex; flex-direction: column;
48
+ overflow: hidden;
49
+ transition: width 200ms ease, min-width 200ms ease;
50
+ z-index: 10;
51
+ }
52
+ .zs-sidebar--collapsed { width: 68px; min-width: 68px; }
53
+ .zs-sidebar--light { background: var(--zs-bg); color: var(--zs-text); border-right: 1px solid var(--zs-border); }
54
+ .zs-sidebar-brand {
55
+ display: flex; align-items: center; gap: 12px;
56
+ padding: 16px 20px; min-height: 64px;
57
+ cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.06);
58
+ }
59
+ .zs-sidebar--light .zs-sidebar-brand { border-bottom-color: var(--zs-border); }
60
+ .zs-sidebar-logo {
61
+ width: 36px; height: 36px; border-radius: 8px;
62
+ display: flex; align-items: center; justify-content: center;
63
+ background: var(--zs-primary, #e67e22); color: white;
64
+ font-weight: 700; font-size: 16px; flex-shrink: 0;
65
+ overflow: hidden;
66
+ }
67
+ .zs-sidebar-logo img { width: 100%; height: 100%; object-fit: contain; }
68
+ .zs-sidebar-title {
69
+ font-size: 16px; font-weight: 600; color: white;
70
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
71
+ }
72
+ .zs-sidebar--light .zs-sidebar-title { color: var(--zs-text); }
73
+ .zs-sidebar-subtitle { font-size: 11px; color: #a2a3b7; }
74
+ .zs-sidebar--collapsed .zs-sidebar-title,
75
+ .zs-sidebar--collapsed .zs-sidebar-subtitle { display: none; }
76
+
77
+ .zs-sidebar-nav {
78
+ flex: 1; overflow-y: auto; padding: 8px 0;
79
+ scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent;
80
+ }
81
+
82
+ /* Nav items */
83
+ .zs-nav-header {
84
+ padding: 16px 20px 6px; font-size: 11px; font-weight: 600;
85
+ text-transform: uppercase; letter-spacing: 0.5px; color: #636674;
86
+ }
87
+ .zs-sidebar--collapsed .zs-nav-header { text-align: center; padding: 12px 4px 4px; font-size: 0; }
88
+ .zs-nav-divider { height: 1px; background: rgba(255,255,255,0.06); margin: 8px 16px; }
89
+ .zs-sidebar--light .zs-nav-divider { background: var(--zs-border); }
90
+
91
+ .zs-nav-item {
92
+ display: flex; align-items: center; gap: 12px;
93
+ padding: 9px 20px; cursor: pointer;
94
+ color: #a2a3b7; font-size: 14px;
95
+ transition: all 150ms; border-left: 3px solid transparent;
96
+ text-decoration: none; user-select: none;
97
+ white-space: nowrap; overflow: hidden;
98
+ }
99
+ .zs-nav-item:hover { color: white; background: rgba(255,255,255,0.04); }
100
+ .zs-sidebar--light .zs-nav-item { color: var(--zs-text-secondary); }
101
+ .zs-sidebar--light .zs-nav-item:hover { color: var(--zs-text); background: var(--zs-bg-hover); }
102
+ .zs-nav-item--active {
103
+ color: white; background: rgba(255,255,255,0.06);
104
+ border-left-color: var(--zs-primary, #e67e22);
105
+ }
106
+ .zs-sidebar--light .zs-nav-item--active {
107
+ color: var(--zs-primary); background: var(--zs-primary-light);
108
+ border-left-color: var(--zs-primary);
109
+ }
110
+ .zs-nav-icon {
111
+ width: 22px; height: 22px; flex-shrink: 0;
112
+ display: flex; align-items: center; justify-content: center;
113
+ font-size: 18px;
114
+ }
115
+ .zs-nav-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
116
+ .zs-sidebar--collapsed .zs-nav-label { display: none; }
117
+ .zs-nav-badge {
118
+ padding: 1px 7px; border-radius: 10px; font-size: 11px;
119
+ font-weight: 600; background: var(--zs-primary); color: white;
120
+ }
121
+ .zs-sidebar--collapsed .zs-nav-badge { display: none; }
122
+ .zs-nav-arrow { font-size: 10px; transition: transform 200ms; color: #636674; }
123
+ .zs-nav-arrow--open { transform: rotate(90deg); }
124
+ .zs-sidebar--collapsed .zs-nav-arrow { display: none; }
125
+ .zs-nav-children { overflow: hidden; }
126
+ .zs-nav-children .zs-nav-item { padding-left: 54px; font-size: 13px; }
127
+ .zs-sidebar--collapsed .zs-nav-children { display: none; }
128
+
129
+ /* Sidebar footer */
130
+ .zs-sidebar-footer {
131
+ padding: 12px 16px; border-top: 1px solid rgba(255,255,255,0.06);
132
+ display: flex; align-items: center; gap: 10px; cursor: pointer;
133
+ }
134
+ .zs-sidebar--light .zs-sidebar-footer { border-top-color: var(--zs-border); }
135
+ .zs-sidebar-avatar {
136
+ width: 36px; height: 36px; border-radius: 50%;
137
+ background: var(--zs-primary); color: white;
138
+ display: flex; align-items: center; justify-content: center;
139
+ font-weight: 600; font-size: 14px; flex-shrink: 0;
140
+ overflow: hidden;
141
+ }
142
+ .zs-sidebar-avatar img { width: 100%; height: 100%; object-fit: cover; }
143
+ .zs-sidebar-user-name { font-size: 13px; color: white; font-weight: 500; }
144
+ .zs-sidebar--light .zs-sidebar-user-name { color: var(--zs-text); }
145
+ .zs-sidebar-user-role { font-size: 11px; color: #636674; }
146
+ .zs-sidebar--collapsed .zs-sidebar-user-name,
147
+ .zs-sidebar--collapsed .zs-sidebar-user-role { display: none; }
148
+
149
+ /* ─── Main Area ──────────────────────────────── */
150
+ .zs-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
151
+
152
+ /* Header */
153
+ .zs-header {
154
+ height: 56px; min-height: 56px;
155
+ display: flex; align-items: center;
156
+ padding: 0 24px; gap: 12px;
157
+ background: var(--zs-bg); border-bottom: 1px solid var(--zs-border);
158
+ }
159
+ .zs-header--dark { background: #1e1e2d; border-bottom-color: rgba(255,255,255,0.06); }
160
+ .zs-header-toggle {
161
+ border: none; background: none; cursor: pointer;
162
+ font-size: 20px; color: var(--zs-text-secondary); padding: 4px;
163
+ }
164
+ .zs-header--dark .zs-header-toggle { color: #a2a3b7; }
165
+ .zs-breadcrumbs {
166
+ display: flex; align-items: center; gap: 6px;
167
+ font-size: 13px; color: var(--zs-text-secondary);
168
+ }
169
+ .zs-breadcrumb-link {
170
+ color: var(--zs-text-secondary); cursor: pointer; text-decoration: none;
171
+ }
172
+ .zs-breadcrumb-link:hover { color: var(--zs-primary); }
173
+ .zs-breadcrumb-sep { color: var(--zs-text-muted); }
174
+ .zs-breadcrumb-current { color: var(--zs-text); font-weight: 500; }
175
+ .zs-header--dark .zs-breadcrumbs { color: #a2a3b7; }
176
+ .zs-header--dark .zs-breadcrumb-current { color: white; }
177
+ .zs-header-spacer { flex: 1; }
178
+ .zs-header-actions { display: flex; align-items: center; gap: 8px; }
179
+ .zs-header-btn {
180
+ border: none; background: none; cursor: pointer;
181
+ font-size: 18px; color: var(--zs-text-secondary); padding: 6px;
182
+ border-radius: 6px; transition: all 150ms; position: relative;
183
+ }
184
+ .zs-header-btn:hover { background: var(--zs-bg-hover); color: var(--zs-text); }
185
+ .zs-header--dark .zs-header-btn { color: #a2a3b7; }
186
+ .zs-header--dark .zs-header-btn:hover { background: rgba(255,255,255,0.06); color: white; }
187
+ .zs-notif-dot {
188
+ position: absolute; top: 2px; right: 2px;
189
+ width: 8px; height: 8px; border-radius: 50%;
190
+ background: var(--zs-danger);
191
+ }
192
+
193
+ /* Content */
194
+ .zs-content { flex: 1; overflow-y: auto; padding: 24px; }
195
+
196
+ /* Page header */
197
+ .zs-page-header {
198
+ display: flex; align-items: center; gap: 16px;
199
+ margin-bottom: 24px;
200
+ }
201
+ .zs-page-title {
202
+ font-size: 22px; font-weight: 600; color: var(--zs-text); margin: 0;
203
+ }
204
+ .zs-page-subtitle {
205
+ font-size: 14px; color: var(--zs-text-secondary); margin: 2px 0 0;
206
+ }
207
+ .zs-page-actions { display: flex; gap: 8px; margin-left: auto; }
208
+ .zs-page-btn {
209
+ padding: 8px 18px; border-radius: var(--zs-radius);
210
+ font-family: var(--zs-font-family); font-size: 13px;
211
+ font-weight: 500; cursor: pointer; border: 1px solid transparent;
212
+ transition: all 150ms;
213
+ }
214
+ .zs-page-btn--primary { background: var(--zs-primary); color: white; }
215
+ .zs-page-btn--primary:hover { background: var(--zs-primary-hover); }
216
+ .zs-page-btn--secondary { background: var(--zs-bg); color: var(--zs-text); border-color: var(--zs-border); }
217
+ .zs-page-btn--secondary:hover { background: var(--zs-bg-hover); }
218
+
219
+ /* Cards grid (dashboard) */
220
+ .zs-cards-grid { display: grid; gap: 24px; }
221
+ .zs-card {
222
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
223
+ padding: 28px 16px; border-radius: 12px;
224
+ background: var(--zs-bg); cursor: pointer;
225
+ transition: all 200ms; text-decoration: none;
226
+ border: 1px solid var(--zs-border);
227
+ gap: 12px;
228
+ }
229
+ .zs-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px var(--zs-shadow); }
230
+ .zs-card-icon {
231
+ width: 64px; height: 64px; border-radius: 50%;
232
+ display: flex; align-items: center; justify-content: center;
233
+ font-size: 28px; color: white;
234
+ }
235
+ .zs-card-title { font-size: 14px; font-weight: 500; color: var(--zs-text); text-align: center; }
236
+ .zs-card-subtitle { font-size: 12px; color: var(--zs-text-muted); text-align: center; }
237
+ .zs-card-badge {
238
+ position: absolute; top: 8px; right: 8px;
239
+ padding: 2px 8px; border-radius: 10px;
240
+ background: var(--zs-danger); color: white;
241
+ font-size: 11px; font-weight: 600;
242
+ }
243
+
244
+ /* KPI cards */
245
+ .zs-kpi-card {
246
+ padding: 20px 24px; border-radius: 12px;
247
+ background: var(--zs-bg); border: 1px solid var(--zs-border);
248
+ }
249
+ .zs-kpi-label { font-size: 13px; color: var(--zs-text-secondary); margin-bottom: 4px; }
250
+ .zs-kpi-value { font-size: 28px; font-weight: 700; color: var(--zs-text); }
251
+ .zs-kpi-trend { font-size: 13px; margin-top: 4px; }
252
+ .zs-kpi-trend--up { color: var(--zs-success); }
253
+ .zs-kpi-trend--down { color: var(--zs-danger); }
254
+
255
+ /* Grid placeholder */
256
+ .zs-grid-page { width: 100%; }
257
+
258
+ /* Empty state */
259
+ .zs-empty {
260
+ display: flex; flex-direction: column; align-items: center;
261
+ justify-content: center; height: 300px;
262
+ color: var(--zs-text-muted); gap: 8px;
263
+ }
264
+ .zs-empty-icon { font-size: 48px; opacity: 0.3; }
265
+ .zs-empty-text { font-size: 16px; }
266
+
267
+ /* ─── Responsive ─────────────────────────────── */
268
+ @media (max-width: 768px) {
269
+ .zs-sidebar { position: absolute; height: 100%; z-index: 100; }
270
+ .zs-sidebar--collapsed { width: 0; min-width: 0; overflow: hidden; }
271
+ .zs-content { padding: 16px; }
272
+ .zs-cards-grid { gap: 12px; }
273
+ }
274
+ `]; }
275
+ // ─── Lifecycle ────────────────────────────────────
276
+ connectedCallback() {
277
+ super.connectedCallback();
278
+ // Set initial page
279
+ if (this.config?.branding.homeSegment) {
280
+ this.currentSegment = this.config.branding.homeSegment;
281
+ }
282
+ else if (this.config?.pages.length) {
283
+ this.currentSegment = this.config.pages[0].segment;
284
+ }
285
+ this.loadPageData();
286
+ }
287
+ updated(changed) {
288
+ if (changed.has('config') && this.config) {
289
+ if (!this.currentSegment && this.config.pages.length > 0) {
290
+ this.currentSegment = this.config.branding.homeSegment ?? this.config.pages[0].segment;
291
+ }
292
+ this.loadPageData();
293
+ }
294
+ }
295
+ // ─── Navigation ───────────────────────────────────
296
+ navigate(segment) {
297
+ if (segment === this.currentSegment)
298
+ return;
299
+ this.history.push(this.currentSegment);
300
+ this.currentSegment = segment;
301
+ this.loadPageData();
302
+ this.dispatchEvent(new CustomEvent('app-navigate', {
303
+ detail: { segment, page: this.currentPage },
304
+ bubbles: true, composed: true,
305
+ }));
306
+ }
307
+ goBack() {
308
+ const prev = this.history.pop();
309
+ if (prev) {
310
+ this.currentSegment = prev;
311
+ this.loadPageData();
312
+ }
313
+ }
314
+ get currentPage() {
315
+ return this.config?.pages.find(p => p.segment === this.currentSegment);
316
+ }
317
+ get breadcrumbs() {
318
+ const crumbs = [];
319
+ if (!this.config)
320
+ return crumbs;
321
+ // Find nav path to current segment
322
+ const findPath = (items, path) => {
323
+ for (const item of items) {
324
+ if (item.segment === this.currentSegment)
325
+ return [...path, item];
326
+ if (item.children) {
327
+ const result = findPath(item.children, [...path, item]);
328
+ if (result)
329
+ return result;
330
+ }
331
+ }
332
+ return null;
333
+ };
334
+ const path = findPath(this.config.navigation, []);
335
+ if (path) {
336
+ for (const item of path) {
337
+ if (item.segment && item.title) {
338
+ crumbs.push({ title: item.title, segment: item.segment });
339
+ }
340
+ }
341
+ }
342
+ return crumbs;
343
+ }
344
+ // ─── Data Loading ─────────────────────────────────
345
+ async loadPageData() {
346
+ const page = this.currentPage;
347
+ if (!page?.dataSources || page.autoFetch === false)
348
+ return;
349
+ this.pageLoading = true;
350
+ try {
351
+ const globalSources = this.config?.dataSources ?? [];
352
+ const pageSources = page.dataSources ?? [];
353
+ const allSources = [...globalSources, ...pageSources];
354
+ const ctx = {
355
+ formData: {},
356
+ dataSources: this.pageData,
357
+ user: this.config?.user,
358
+ };
359
+ const data = await fetchAllDataSources(allSources, ctx, this.eventBus, this.config?.headers);
360
+ this.pageData = { ...this.pageData, ...data };
361
+ }
362
+ catch { /* silently fail */ }
363
+ this.pageLoading = false;
364
+ this.renderKey++;
365
+ }
366
+ // ─── Role Checks ─────────────────────────────────
367
+ hasRole(roles) {
368
+ if (!roles || roles.length === 0)
369
+ return true;
370
+ const userRoles = this.config?.user?.roles ?? [];
371
+ return roles.some(r => userRoles.includes(r));
372
+ }
373
+ // ─── Render ───────────────────────────────────────
374
+ render() {
375
+ if (!this.config) {
376
+ return html `<div class="zs-empty"><span class="zs-empty-icon">⚙️</span><span class="zs-empty-text">No app config provided</span></div>`;
377
+ }
378
+ const sidebarStyle = this.config.branding.sidebarStyle ?? 'dark';
379
+ const headerStyle = this.config.branding.headerStyle ?? 'light';
380
+ const sidebarClass = `zs-sidebar ${this.sidebarCollapsed ? 'zs-sidebar--collapsed' : ''} ${sidebarStyle === 'light' ? 'zs-sidebar--light' : ''}`;
381
+ const headerClass = `zs-header ${headerStyle === 'dark' ? 'zs-header--dark' : ''}`;
382
+ // Apply custom CSS variables
383
+ const cssVarStyle = Object.entries(this.cssVars).map(([k, v]) => `${k}:${v}`).join(';');
384
+ return html `
385
+ ${this.customCss ? html `<style>${this.customCss}</style>` : ''}
386
+ <div class="zs-app" style="${cssVarStyle}">
387
+ <!-- Sidebar -->
388
+ <aside class="${sidebarClass}">
389
+ ${this.renderSidebarBrand()}
390
+ <nav class="zs-sidebar-nav">
391
+ ${this.config.navigation.map(item => this.renderNavItem(item))}
392
+ </nav>
393
+ ${this.renderSidebarFooter()}
394
+ </aside>
395
+
396
+ <!-- Main -->
397
+ <div class="zs-main">
398
+ <header class="${headerClass}">
399
+ <button class="zs-header-toggle" @click="${() => { this.sidebarCollapsed = !this.sidebarCollapsed; }}">☰</button>
400
+ ${this.renderBreadcrumbs()}
401
+ <div class="zs-header-spacer"></div>
402
+ ${this.renderHeaderActions()}
403
+ </header>
404
+ <main class="zs-content">
405
+ ${this.renderPage()}
406
+ </main>
407
+ </div>
408
+ </div>
409
+ `;
410
+ }
411
+ // ─── Sidebar Brand ────────────────────────────────
412
+ renderSidebarBrand() {
413
+ const b = this.config.branding;
414
+ const home = b.homeSegment ?? this.config.pages[0]?.segment ?? '';
415
+ return html `
416
+ <div class="zs-sidebar-brand" @click="${() => this.navigate(home)}">
417
+ <div class="zs-sidebar-logo">
418
+ ${b.logo ? (b.logo.startsWith('<') ? unsafeHTML(b.logo) : html `<img src="${b.logo}" alt="" />`) : (b.title?.[0] ?? 'Z')}
419
+ </div>
420
+ <div>
421
+ <div class="zs-sidebar-title">${b.title ?? 'Studio App'}</div>
422
+ ${b.subtitle ? html `<div class="zs-sidebar-subtitle">${b.subtitle}</div>` : ''}
423
+ </div>
424
+ </div>
425
+ `;
426
+ }
427
+ // ─── Nav Items ────────────────────────────────────
428
+ renderNavItem(item) {
429
+ if (item.hidden || !this.hasRole(item.roles))
430
+ return nothing;
431
+ if (item.kind === 'header') {
432
+ return html `<div class="zs-nav-header">${item.title}</div>`;
433
+ }
434
+ if (item.kind === 'divider') {
435
+ return html `<div class="zs-nav-divider"></div>`;
436
+ }
437
+ const hasChildren = item.children && item.children.length > 0;
438
+ const isActive = this.currentSegment === item.segment;
439
+ const isExpanded = this.expandedNavItems.has(item.segment ?? '');
440
+ const isChildActive = hasChildren && item.children.some(c => c.segment === this.currentSegment);
441
+ const itemClass = `zs-nav-item ${isActive || isChildActive ? 'zs-nav-item--active' : ''}`;
442
+ return html `
443
+ <div class="${itemClass}" @click="${() => {
444
+ if (hasChildren) {
445
+ const next = new Set(this.expandedNavItems);
446
+ if (isExpanded)
447
+ next.delete(item.segment);
448
+ else
449
+ next.add(item.segment);
450
+ this.expandedNavItems = next;
451
+ }
452
+ if (item.segment && !hasChildren)
453
+ this.navigate(item.segment);
454
+ }}">
455
+ <span class="zs-nav-icon">${item.icon ?? '📄'}</span>
456
+ <span class="zs-nav-label">${item.title}</span>
457
+ ${item.badge != null ? html `<span class="zs-nav-badge" style="${item.badgeColor ? `background:${item.badgeColor}` : ''}">${item.badge}</span>` : ''}
458
+ ${hasChildren ? html `<span class="zs-nav-arrow ${isExpanded ? 'zs-nav-arrow--open' : ''}">▶</span>` : ''}
459
+ </div>
460
+ ${hasChildren && isExpanded ? html `
461
+ <div class="zs-nav-children">
462
+ ${item.children.map(child => this.renderNavItem(child))}
463
+ </div>
464
+ ` : ''}
465
+ `;
466
+ }
467
+ // ─── Sidebar Footer ──────────────────────────────
468
+ renderSidebarFooter() {
469
+ const user = this.config?.user;
470
+ if (!user)
471
+ return nothing;
472
+ const initials = (user.name ?? 'U').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
473
+ return html `
474
+ <div class="zs-sidebar-footer">
475
+ <div class="zs-sidebar-avatar">
476
+ ${user.avatar ? html `<img src="${user.avatar}" alt="" />` : initials}
477
+ </div>
478
+ <div>
479
+ <div class="zs-sidebar-user-name">${user.name ?? 'Usuario'}</div>
480
+ <div class="zs-sidebar-user-role">${user.company ?? user.email ?? ''}</div>
481
+ </div>
482
+ </div>
483
+ `;
484
+ }
485
+ // ─── Breadcrumbs ──────────────────────────────────
486
+ renderBreadcrumbs() {
487
+ const crumbs = this.breadcrumbs;
488
+ if (crumbs.length === 0)
489
+ return html `<div class="zs-breadcrumbs"><span class="zs-breadcrumb-current">${this.config?.branding.title ?? 'Home'}</span></div>`;
490
+ return html `
491
+ <div class="zs-breadcrumbs">
492
+ <span class="zs-breadcrumb-link" @click="${() => this.navigate(this.config.branding.homeSegment ?? this.config.pages[0]?.segment ?? '')}">Home</span>
493
+ ${crumbs.map((c, i) => html `
494
+ <span class="zs-breadcrumb-sep">/</span>
495
+ ${i < crumbs.length - 1
496
+ ? html `<span class="zs-breadcrumb-link" @click="${() => this.navigate(c.segment)}">${c.title}</span>`
497
+ : html `<span class="zs-breadcrumb-current">${c.title}</span>`}
498
+ `)}
499
+ </div>
500
+ `;
501
+ }
502
+ // ─── Header Actions ──────────────────────────────
503
+ renderHeaderActions() {
504
+ const notifCount = this.config?.notifications?.filter(n => !n.read).length ?? 0;
505
+ return html `
506
+ <div class="zs-header-actions">
507
+ <button class="zs-header-btn" title="Buscar" @click="${() => this.dispatchEvent(new CustomEvent('app-search', { bubbles: true, composed: true }))}">🔍</button>
508
+ <button class="zs-header-btn" title="Notificaciones" @click="${() => this.dispatchEvent(new CustomEvent('app-notifications', { bubbles: true, composed: true }))}">
509
+ 🔔${notifCount > 0 ? html `<span class="zs-notif-dot"></span>` : ''}
510
+ </button>
511
+ <button class="zs-header-btn" title="Ajustes" @click="${() => this.dispatchEvent(new CustomEvent('app-settings', { bubbles: true, composed: true }))}">⚙️</button>
512
+ </div>
513
+ `;
514
+ }
515
+ // ─── Page Renderer ────────────────────────────────
516
+ renderPage() {
517
+ const page = this.currentPage;
518
+ if (!page) {
519
+ return html `<div class="zs-empty"><span class="zs-empty-icon">📄</span><span class="zs-empty-text">Pagina no encontrada: ${this.currentSegment}</span></div>`;
520
+ }
521
+ if (!this.hasRole(page.roles)) {
522
+ return html `<div class="zs-empty"><span class="zs-empty-icon">🔒</span><span class="zs-empty-text">Sin acceso a esta pagina</span></div>`;
523
+ }
524
+ return html `
525
+ <!-- Page Header -->
526
+ ${page.title ? html `
527
+ <div class="zs-page-header">
528
+ <div>
529
+ <h1 class="zs-page-title">${page.title}</h1>
530
+ ${page.subtitle ? html `<p class="zs-page-subtitle">${page.subtitle}</p>` : ''}
531
+ </div>
532
+ ${page.actions ? html `
533
+ <div class="zs-page-actions">
534
+ ${page.actions.map(a => html `
535
+ <button class="zs-page-btn zs-page-btn--${a.variant ?? 'secondary'}"
536
+ @click="${() => this.dispatchEvent(new CustomEvent('app-action', { detail: { actionId: a.id, page: page.segment, data: this.pageData }, bubbles: true, composed: true }))}"
537
+ >${a.label}</button>
538
+ `)}
539
+ </div>
540
+ ` : ''}
541
+ </div>
542
+ ` : ''}
543
+
544
+ <!-- Page Content -->
545
+ ${this.pageLoading ? html `<div class="zs-empty"><span>Cargando...</span></div>` : this.renderPageContent(page)}
546
+ `;
547
+ }
548
+ renderPageContent(page) {
549
+ switch (page.content) {
550
+ case 'cards': return this.renderCardsPage(page);
551
+ case 'schema': return this.renderSchemaPage(page);
552
+ case 'datagrid': return this.renderGridPage(page);
553
+ case 'html': return html `<div>${unsafeHTML(page.htmlContent ?? '')}</div>`;
554
+ case 'iframe': return html `<iframe src="${page.iframeUrl ?? ''}" style="width:100%;height:calc(100vh - 200px);border:1px solid var(--zs-border);border-radius:8px;" frameborder="0"></iframe>`;
555
+ case 'chart': return this.renderChartPage(page);
556
+ case 'tabs': return this.renderTabsPage(page);
557
+ case 'empty':
558
+ this.dispatchEvent(new CustomEvent('app-render-page', { detail: { page }, bubbles: true, composed: true }));
559
+ return html `<slot name="${page.segment}"></slot>`;
560
+ case 'custom':
561
+ this.dispatchEvent(new CustomEvent('app-render-page', { detail: { page, data: this.pageData }, bubbles: true, composed: true }));
562
+ return html `<slot name="${page.segment}"></slot>`;
563
+ default:
564
+ return html `<div class="zs-empty"><span class="zs-empty-icon">📋</span><span class="zs-empty-text">Tipo de contenido: ${page.content}</span></div>`;
565
+ }
566
+ }
567
+ // ─── Cards Page (Dashboard) ───────────────────────
568
+ renderCardsPage(page) {
569
+ const cfg = page.cardsConfig;
570
+ if (!cfg)
571
+ return html ``;
572
+ const cols = cfg.columns ?? 6;
573
+ const variant = cfg.variant ?? 'icon';
574
+ if (variant === 'kpi') {
575
+ return html `
576
+ <div class="zs-cards-grid" style="grid-template-columns: repeat(${cols}, 1fr);">
577
+ ${cfg.items.filter(c => this.hasRole(c.roles)).map(card => html `
578
+ <div class="zs-kpi-card" @click="${() => card.segment ? this.navigate(card.segment) : null}" style="${card.segment ? 'cursor:pointer' : ''}">
579
+ <div class="zs-kpi-label">${card.title}</div>
580
+ <div class="zs-kpi-value">${card.value ?? '—'}</div>
581
+ ${card.trend ? html `<div class="zs-kpi-trend zs-kpi-trend--${card.trend}">${card.trend === 'up' ? '↑' : card.trend === 'down' ? '↓' : '→'} ${card.trendValue ?? ''}</div>` : ''}
582
+ </div>
583
+ `)}
584
+ </div>
585
+ `;
586
+ }
587
+ return html `
588
+ <div class="zs-cards-grid" style="grid-template-columns: repeat(${cols}, 1fr); gap: ${cfg.gap ?? 24}px;">
589
+ ${cfg.items.filter(c => this.hasRole(c.roles)).map(card => html `
590
+ <div class="zs-card" @click="${() => card.segment ? this.navigate(card.segment) : card.url ? window.open(card.url) : null}"
591
+ style="position:relative;">
592
+ ${card.badge != null ? html `<span class="zs-card-badge">${card.badge}</span>` : ''}
593
+ <div class="zs-card-icon" style="background: ${card.iconBg ?? 'var(--zs-primary)'}">
594
+ ${card.icon ?? '📦'}
595
+ </div>
596
+ <div class="zs-card-title">${card.title}</div>
597
+ ${card.subtitle ? html `<div class="zs-card-subtitle">${card.subtitle}</div>` : ''}
598
+ </div>
599
+ `)}
600
+ </div>
601
+ `;
602
+ }
603
+ // ─── Schema Page (Form) ───────────────────────────
604
+ renderSchemaPage(page) {
605
+ if (!page.schema)
606
+ return html `<div>Sin schema configurado</div>`;
607
+ return html `
608
+ <zentto-studio-renderer
609
+ .schema="${page.schema}"
610
+ .data="${this.pageData}"
611
+ @studio-change="${(e) => { this.pageData = { ...this.pageData, ...e.detail.data }; }}"
612
+ @studio-submit="${(e) => { this.dispatchEvent(new CustomEvent('app-submit', { detail: { page: page.segment, ...e.detail }, bubbles: true, composed: true })); }}"
613
+ ></zentto-studio-renderer>
614
+ `;
615
+ }
616
+ // ─── Grid Page ────────────────────────────────────
617
+ renderGridPage(page) {
618
+ const cfg = page.gridConfig;
619
+ if (!cfg)
620
+ return html `<div>Sin grid configurado</div>`;
621
+ const rows = this.pageData[cfg.dataSourceId] ?? [];
622
+ // Emit event for host to render zentto-grid
623
+ this.dispatchEvent(new CustomEvent('app-render-grid', {
624
+ detail: { page: page.segment, config: cfg, rows, data: this.pageData },
625
+ bubbles: true, composed: true,
626
+ }));
627
+ // Simple table fallback if zentto-grid not available
628
+ return html `
629
+ <div class="zs-grid-page">
630
+ <slot name="grid-${page.segment}">
631
+ <table style="width:100%;border-collapse:collapse;font-size:14px;">
632
+ <thead>
633
+ <tr>${cfg.columns.filter(c => !c.hidden).map(c => html `<th style="text-align:left;padding:10px 12px;border-bottom:2px solid var(--zs-border);font-weight:600;color:var(--zs-text-secondary);font-size:12px;text-transform:uppercase;">${c.header}</th>`)}</tr>
634
+ </thead>
635
+ <tbody>
636
+ ${rows.slice(0, 100).map(row => html `
637
+ <tr style="cursor:pointer;transition:background 100ms;" @click="${() => {
638
+ if (cfg.onRowClick === 'navigate' && cfg.rowClickSegment) {
639
+ this.navigate(`${cfg.rowClickSegment}/${row[cfg.rowKey ?? 'id']}`);
640
+ }
641
+ this.dispatchEvent(new CustomEvent('app-row-click', { detail: { page: page.segment, row }, bubbles: true, composed: true }));
642
+ }}">
643
+ ${cfg.columns.filter(c => !c.hidden).map(c => html `<td style="padding:10px 12px;border-bottom:1px solid var(--zs-border);">${row[c.field] ?? ''}</td>`)}
644
+ </tr>
645
+ `)}
646
+ ${rows.length === 0 ? html `<tr><td colspan="${cfg.columns.length}" style="padding:24px;text-align:center;color:var(--zs-text-muted);">Sin datos</td></tr>` : ''}
647
+ </tbody>
648
+ </table>
649
+ </slot>
650
+ </div>
651
+ `;
652
+ }
653
+ // ─── Chart Page ───────────────────────────────────
654
+ renderChartPage(page) {
655
+ const cfg = page.chartConfig;
656
+ if (!cfg)
657
+ return html ``;
658
+ return html `
659
+ <div class="zs-cards-grid" style="grid-template-columns: repeat(${cfg.columns ?? 2}, 1fr);">
660
+ ${cfg.charts.map(chart => {
661
+ const dsData = this.pageData[chart.dataSourceId];
662
+ const chartData = Array.isArray(dsData)
663
+ ? dsData.map((item) => ({
664
+ label: String(item[chart.labelField] ?? ''),
665
+ value: Number(item[chart.valueFields[0]] ?? 0),
666
+ }))
667
+ : [];
668
+ return html `
669
+ <div style="${chart.colSpan ? `grid-column: span ${chart.colSpan}` : ''}">
670
+ <zs-field-chart
671
+ .label="${chart.title}"
672
+ .chartType="${chart.type}"
673
+ .data="${chartData}"
674
+ .height="${chart.height ?? 250}"
675
+ ></zs-field-chart>
676
+ </div>
677
+ `;
678
+ })}
679
+ </div>
680
+ `;
681
+ }
682
+ renderTabsPage(page) {
683
+ const cfg = page.tabsConfig;
684
+ if (!cfg)
685
+ return html ``;
686
+ const tab = cfg.tabs[this.activeTab];
687
+ const tabPage = this.config?.pages.find(p => p.id === tab?.pageId);
688
+ return html `
689
+ <div style="display:flex;gap:0;border-bottom:2px solid var(--zs-border);margin-bottom:16px;">
690
+ ${cfg.tabs.map((t, i) => html `
691
+ <button style="padding:10px 20px;cursor:pointer;border:none;background:none;font-family:var(--zs-font-family);font-size:14px;color:${i === this.activeTab ? 'var(--zs-primary)' : 'var(--zs-text-secondary)'};border-bottom:2px solid ${i === this.activeTab ? 'var(--zs-primary)' : 'transparent'};margin-bottom:-2px;font-weight:${i === this.activeTab ? '500' : '400'};"
692
+ @click="${() => { this.activeTab = i; }}"
693
+ >${t.icon ? `${t.icon} ` : ''}${t.title}</button>
694
+ `)}
695
+ </div>
696
+ ${tabPage ? this.renderPageContent(tabPage) : html `<div>Tab page not found: ${tab?.pageId}</div>`}
697
+ `;
698
+ }
699
+ // ─── Public API ───────────────────────────────────
700
+ /** Navigate to a page segment programmatically */
701
+ navigateTo(segment) { this.navigate(segment); }
702
+ /** Get current page segment */
703
+ getCurrentSegment() { return this.currentSegment; }
704
+ /** Get fetched data for current page */
705
+ getPageData() { return { ...this.pageData }; }
706
+ /** Reload current page data */
707
+ async reloadData() { await this.loadPageData(); }
708
+ /** Update page data externally */
709
+ setPageData(data) {
710
+ this.pageData = { ...this.pageData, ...data };
711
+ this.renderKey++;
712
+ }
713
+ };
714
+ __decorate([
715
+ property({ type: Object })
716
+ ], ZenttoStudioApp.prototype, "config", void 0);
717
+ __decorate([
718
+ property({ type: String })
719
+ ], ZenttoStudioApp.prototype, "customCss", void 0);
720
+ __decorate([
721
+ property({ type: Object })
722
+ ], ZenttoStudioApp.prototype, "cssVars", void 0);
723
+ __decorate([
724
+ state()
725
+ ], ZenttoStudioApp.prototype, "currentSegment", void 0);
726
+ __decorate([
727
+ state()
728
+ ], ZenttoStudioApp.prototype, "sidebarCollapsed", void 0);
729
+ __decorate([
730
+ state()
731
+ ], ZenttoStudioApp.prototype, "expandedNavItems", void 0);
732
+ __decorate([
733
+ state()
734
+ ], ZenttoStudioApp.prototype, "pageData", void 0);
735
+ __decorate([
736
+ state()
737
+ ], ZenttoStudioApp.prototype, "pageLoading", void 0);
738
+ __decorate([
739
+ state()
740
+ ], ZenttoStudioApp.prototype, "renderKey", void 0);
741
+ __decorate([
742
+ state()
743
+ ], ZenttoStudioApp.prototype, "activeTab", void 0);
744
+ ZenttoStudioApp = __decorate([
745
+ customElement('zentto-studio-app')
746
+ ], ZenttoStudioApp);
747
+ export { ZenttoStudioApp };
748
+ //# sourceMappingURL=zentto-studio-app.js.map