@smartnet360/svelte-components 0.0.16 → 0.0.19

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.
@@ -41,6 +41,27 @@
41
41
  });
42
42
  }
43
43
 
44
+ // Deep merge helper function for layout objects
45
+ function deepMerge(target: any, source: any): any {
46
+ if (!source) return target;
47
+
48
+ const output = { ...target };
49
+
50
+ for (const key in source) {
51
+ if (source.hasOwnProperty(key)) {
52
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
53
+ // Recursively merge nested objects
54
+ output[key] = deepMerge(output[key] || {}, source[key]);
55
+ } else {
56
+ // Direct assignment for primitives and arrays
57
+ output[key] = source[key];
58
+ }
59
+ }
60
+ }
61
+
62
+ return output;
63
+ }
64
+
44
65
  function renderChart() {
45
66
  if (!chartDiv || !data?.length) return;
46
67
 
@@ -85,10 +106,11 @@
85
106
  };
86
107
  }
87
108
 
88
- // Merge external layout with defaults, then apply size adaptations
89
- let finalLayout = plotlyLayout ?
90
- { ...defaultLayout, ...plotlyLayout } :
91
- defaultLayout;
109
+ // Merge external layout with defaults using deep merge
110
+ // Use structuredClone for deep copy to prevent mutation of defaultLayout
111
+ let finalLayout = structuredClone(
112
+ plotlyLayout ? deepMerge(defaultLayout, plotlyLayout) : defaultLayout
113
+ );
92
114
 
93
115
  // Apply size-based adaptations using helper
94
116
  finalLayout = adaptPlotlyLayout(
@@ -0,0 +1,281 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import { editorStore, currentLayout, isDirty, savedLayouts } from './editorState.js';
5
+ import LayoutTreeView from './LayoutTreeView.svelte';
6
+ import GridPreview from './GridPreview.svelte';
7
+ import PropertiesPanel from './PropertiesPanel.svelte';
8
+ import KPIPicker from './KPIPicker.svelte';
9
+
10
+ let showKPIPicker = $state(false);
11
+ let showLibrary = $state(false);
12
+ let kpiPickerContext = $state<{
13
+ sectionId: string;
14
+ chartIndex: number;
15
+ side: 'yLeft' | 'yRight';
16
+ } | null>(null);
17
+
18
+ function handleNewLayout() {
19
+ const name = prompt('Enter layout name:', 'New Layout');
20
+ if (name) {
21
+ editorStore.newLayout(name);
22
+ }
23
+ }
24
+
25
+ function handleSave() {
26
+ editorStore.saveToLibrary();
27
+ alert('Layout saved to library!');
28
+ }
29
+
30
+ function handleExport() {
31
+ const json = editorStore.exportToJSON();
32
+ if (!json) {
33
+ alert('No layout to export');
34
+ return;
35
+ }
36
+
37
+ // Download as JSON file
38
+ const blob = new Blob([json], { type: 'application/json' });
39
+ const url = URL.createObjectURL(blob);
40
+ const a = document.createElement('a');
41
+ a.href = url;
42
+ a.download = `${$currentLayout?.layoutName || 'layout'}.json`;
43
+ a.click();
44
+ URL.revokeObjectURL(url);
45
+ }
46
+
47
+ function handleImport() {
48
+ const input = document.createElement('input');
49
+ input.type = 'file';
50
+ input.accept = '.json';
51
+ input.onchange = (e) => {
52
+ const file = (e.target as HTMLInputElement).files?.[0];
53
+ if (file) {
54
+ const reader = new FileReader();
55
+ reader.onload = (event) => {
56
+ const json = event.target?.result as string;
57
+ if (editorStore.importFromJSON(json)) {
58
+ alert('Layout imported successfully!');
59
+ } else {
60
+ alert('Failed to import layout. Please check the file format.');
61
+ }
62
+ };
63
+ reader.readAsText(file);
64
+ }
65
+ };
66
+ input.click();
67
+ }
68
+
69
+ function handleOpenKPIPicker(event: CustomEvent<{ sectionId: string; chartIndex: number; side: 'yLeft' | 'yRight' }>) {
70
+ kpiPickerContext = event.detail;
71
+ showKPIPicker = true;
72
+ }
73
+
74
+ function handleKPISelected(event: CustomEvent<any>) {
75
+ if (kpiPickerContext) {
76
+ editorStore.addKPI(
77
+ kpiPickerContext.sectionId,
78
+ kpiPickerContext.chartIndex,
79
+ kpiPickerContext.side,
80
+ event.detail
81
+ );
82
+ }
83
+ showKPIPicker = false;
84
+ kpiPickerContext = null;
85
+ }
86
+
87
+ function handleLoadFromLibrary(layoutName: string) {
88
+ const layout = $savedLayouts.find(l => l.layoutName === layoutName);
89
+ if (layout) {
90
+ editorStore.loadLayout(layout);
91
+ showLibrary = false;
92
+ }
93
+ }
94
+
95
+ function handleDeleteFromLibrary(layoutName: string) {
96
+ if (confirm(`Delete "${layoutName}" from library?`)) {
97
+ editorStore.deleteFromLibrary(layoutName);
98
+ }
99
+ }
100
+ </script>
101
+
102
+ <div class="editor-container d-flex flex-column vh-100">
103
+ <!-- Top Toolbar -->
104
+ <nav class="navbar navbar-dark bg-dark border-bottom">
105
+ <div class="container-fluid">
106
+ <span class="navbar-brand mb-0 h1">
107
+ <i class="bi bi-bar-chart-line-fill me-2"></i>
108
+ Chart Layout Editor
109
+ </span>
110
+ <div class="d-flex gap-2">
111
+ <button class="btn btn-sm btn-outline-light" onclick={handleNewLayout}>
112
+ <i class="bi bi-file-plus"></i> New
113
+ </button>
114
+ <button class="btn btn-sm btn-outline-light" onclick={() => (showLibrary = true)}>
115
+ <i class="bi bi-folder2-open"></i> Library
116
+ </button>
117
+ <button class="btn btn-sm btn-outline-light" onclick={handleImport}>
118
+ <i class="bi bi-upload"></i> Import
119
+ </button>
120
+ <button
121
+ class="btn btn-sm btn-primary"
122
+ onclick={handleSave}
123
+ disabled={!$currentLayout}
124
+ >
125
+ <i class="bi bi-save"></i> Save
126
+ {#if $isDirty}
127
+ <span class="badge bg-warning ms-1">*</span>
128
+ {/if}
129
+ </button>
130
+ <button
131
+ class="btn btn-sm btn-success"
132
+ onclick={handleExport}
133
+ disabled={!$currentLayout}
134
+ >
135
+ <i class="bi bi-download"></i> Export JSON
136
+ </button>
137
+ </div>
138
+ </div>
139
+ </nav>
140
+
141
+ {#if !$currentLayout}
142
+ <!-- Empty State -->
143
+ <div class="flex-grow-1 d-flex align-items-center justify-content-center bg-light">
144
+ <div class="text-center">
145
+ <i class="bi bi-file-earmark-bar-graph display-1 text-muted"></i>
146
+ <h3 class="mt-3">No Layout Open</h3>
147
+ <p class="text-muted">Create a new layout or import an existing one to get started</p>
148
+ <div class="d-flex gap-2 justify-content-center mt-4">
149
+ <button class="btn btn-primary" onclick={handleNewLayout}>
150
+ <i class="bi bi-plus-circle"></i> Create New Layout
151
+ </button>
152
+ <button class="btn btn-outline-secondary" onclick={handleImport}>
153
+ <i class="bi bi-upload"></i> Import Layout
154
+ </button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ {:else}
159
+ <!-- Main Editor Layout -->
160
+ <div class="editor-main d-flex flex-grow-1 overflow-hidden">
161
+ <!-- Left Panel: Tree View -->
162
+ <div class="panel-left border-end bg-white overflow-auto" style="width: 280px; min-width: 280px;">
163
+ <LayoutTreeView />
164
+ </div>
165
+
166
+ <!-- Center Panel: Grid Preview -->
167
+ <div class="panel-center flex-grow-1 bg-light overflow-auto">
168
+ <GridPreview on:openkpipicker={handleOpenKPIPicker} />
169
+ </div>
170
+
171
+ <!-- Right Panel: Properties -->
172
+ <div class="panel-right border-start bg-white overflow-auto" style="width: 320px; min-width: 320px;">
173
+ <PropertiesPanel on:openkpipicker={handleOpenKPIPicker} />
174
+ </div>
175
+ </div>
176
+ {/if}
177
+ </div>
178
+
179
+ <!-- KPI Picker Modal -->
180
+ {#if showKPIPicker}
181
+ <KPIPicker
182
+ show={showKPIPicker}
183
+ on:select={handleKPISelected}
184
+ on:close={() => { showKPIPicker = false; kpiPickerContext = null; }}
185
+ />
186
+ {/if}
187
+
188
+ <!-- Library Modal -->
189
+ {#if showLibrary}
190
+ <div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
191
+ <div class="modal-dialog modal-dialog-scrollable">
192
+ <div class="modal-content">
193
+ <div class="modal-header">
194
+ <h5 class="modal-title">
195
+ <i class="bi bi-folder2-open me-2"></i>
196
+ Saved Layouts
197
+ </h5>
198
+ <button type="button" class="btn-close" onclick={() => (showLibrary = false)}></button>
199
+ </div>
200
+ <div class="modal-body">
201
+ {#if $savedLayouts.length > 0}
202
+ <div class="list-group">
203
+ {#each $savedLayouts as layout}
204
+ <div class="list-group-item">
205
+ <div class="d-flex justify-content-between align-items-center">
206
+ <div class="flex-grow-1">
207
+ <h6 class="mb-1">{layout.layoutName}</h6>
208
+ <small class="text-muted">
209
+ {layout.sections.length} sections
210
+ </small>
211
+ </div>
212
+ <div class="btn-group btn-group-sm">
213
+ <button
214
+ class="btn btn-outline-primary"
215
+ onclick={() => handleLoadFromLibrary(layout.layoutName)}
216
+ >
217
+ <i class="bi bi-folder-open"></i> Load
218
+ </button>
219
+ <button
220
+ class="btn btn-outline-danger"
221
+ onclick={() => handleDeleteFromLibrary(layout.layoutName)}
222
+ >
223
+ <i class="bi bi-trash"></i>
224
+ </button>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ {/each}
229
+ </div>
230
+ {:else}
231
+ <div class="alert alert-info">
232
+ <i class="bi bi-info-circle me-2"></i>
233
+ No saved layouts yet. Create and save a layout to see it here.
234
+ </div>
235
+ {/if}
236
+ </div>
237
+ <div class="modal-footer">
238
+ <button class="btn btn-secondary" onclick={() => (showLibrary = false)}>
239
+ Close
240
+ </button>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ {/if}
246
+
247
+ <style>
248
+ .editor-container {
249
+ background: #f8f9fa;
250
+ }
251
+
252
+ .panel-left,
253
+ .panel-center,
254
+ .panel-right {
255
+ height: 100%;
256
+ }
257
+
258
+ /* Ensure scrolling works properly */
259
+ .overflow-auto {
260
+ overflow-y: auto;
261
+ overflow-x: hidden;
262
+ }
263
+
264
+ /* Custom scrollbar styling */
265
+ .overflow-auto::-webkit-scrollbar {
266
+ width: 8px;
267
+ }
268
+
269
+ .overflow-auto::-webkit-scrollbar-track {
270
+ background: #f1f1f1;
271
+ }
272
+
273
+ .overflow-auto::-webkit-scrollbar-thumb {
274
+ background: #888;
275
+ border-radius: 4px;
276
+ }
277
+
278
+ .overflow-auto::-webkit-scrollbar-thumb:hover {
279
+ background: #555;
280
+ }
281
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const ChartLayoutEditor: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ChartLayoutEditor = ReturnType<typeof ChartLayoutEditor>;
3
+ export default ChartLayoutEditor;
@@ -0,0 +1,212 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import { createEventDispatcher } from 'svelte';
5
+ import { editorStore, currentLayout, selection } from './editorState.js';
6
+ import type { ChartGrid } from '../charts.model.js';
7
+
8
+ const dispatch = createEventDispatcher();
9
+
10
+ function getGridDimensions(grid: ChartGrid): { rows: number; cols: number } {
11
+ const map: Record<ChartGrid, { rows: number; cols: number }> = {
12
+ '2x2': { rows: 2, cols: 2 },
13
+ '3x3': { rows: 3, cols: 3 },
14
+ '4x4': { rows: 4, cols: 4 },
15
+ '1x2': { rows: 1, cols: 2 },
16
+ '1x4': { rows: 1, cols: 4 },
17
+ '1x8': { rows: 1, cols: 8 }
18
+ };
19
+ return map[grid] || { rows: 2, cols: 2 };
20
+ }
21
+
22
+ function handleChartClick(sectionId: string, chartIndex: number) {
23
+ editorStore.select({ type: 'chart', sectionId, chartIndex });
24
+ }
25
+
26
+ function isChartSelected(sectionId: string, chartIndex: number): boolean {
27
+ return (
28
+ $selection.type === 'chart' &&
29
+ $selection.sectionId === sectionId &&
30
+ $selection.chartIndex === chartIndex
31
+ );
32
+ }
33
+ </script>
34
+
35
+ <div class="grid-preview h-100 p-4 overflow-auto">
36
+ {#if $currentLayout}
37
+ <div class="mb-3">
38
+ <h5 class="text-muted">
39
+ <i class="bi bi-eye"></i> Preview: {$currentLayout.layoutName}
40
+ </h5>
41
+ </div>
42
+
43
+ {#each $currentLayout.sections as section}
44
+ {@const { rows, cols } = getGridDimensions(section.grid || '2x2')}
45
+
46
+ <div class="section-preview mb-4 border rounded bg-white p-3 shadow-sm">
47
+ <!-- Section Header -->
48
+ <div class="d-flex justify-content-between align-items-center mb-3">
49
+ <h6 class="mb-0">
50
+ <i class="bi bi-grid-3x3-gap me-2"></i>
51
+ {section.title}
52
+ </h6>
53
+ <span class="badge bg-secondary">{section.grid || '2x2'}</span>
54
+ </div>
55
+
56
+ <!-- Grid -->
57
+ <div
58
+ class="grid-container"
59
+ style="
60
+ display: grid;
61
+ grid-template-columns: repeat({cols}, 1fr);
62
+ grid-template-rows: repeat({rows}, 200px);
63
+ gap: 0.75rem;
64
+ "
65
+ >
66
+ {#each section.charts as chart, cIdx}
67
+ <button
68
+ class="grid-cell border rounded p-2 position-relative"
69
+ class:selected={isChartSelected(section.id, cIdx)}
70
+ onclick={() => handleChartClick(section.id, cIdx)}
71
+ >
72
+ {#if chart.pos}
73
+ <div class="position-badge">{chart.pos}</div>
74
+ {/if}
75
+
76
+ <div class="h-100 d-flex flex-column">
77
+ <div class="chart-title fw-bold mb-2 text-truncate">
78
+ {chart.title}
79
+ </div>
80
+
81
+ <div class="flex-grow-1 overflow-auto">
82
+ {#if chart.yLeft.length > 0}
83
+ <div class="kpi-list mb-2">
84
+ <small class="text-muted d-block mb-1">
85
+ <i class="bi bi-bar-chart-line"></i> Left Axis
86
+ </small>
87
+ {#each chart.yLeft as kpi}
88
+ <div class="kpi-badge badge bg-success text-truncate mb-1">
89
+ {kpi.name}
90
+ </div>
91
+ {/each}
92
+ </div>
93
+ {/if}
94
+
95
+ {#if chart.yRight.length > 0}
96
+ <div class="kpi-list">
97
+ <small class="text-muted d-block mb-1">
98
+ <i class="bi bi-bar-chart-line-fill"></i> Right Axis
99
+ </small>
100
+ {#each chart.yRight as kpi}
101
+ <div class="kpi-badge badge bg-warning text-truncate mb-1">
102
+ {kpi.name}
103
+ </div>
104
+ {/each}
105
+ </div>
106
+ {/if}
107
+
108
+ {#if chart.yLeft.length === 0 && chart.yRight.length === 0}
109
+ <div class="text-center text-muted small mt-3">
110
+ <i class="bi bi-plus-circle"></i>
111
+ <div>No KPIs</div>
112
+ </div>
113
+ {/if}
114
+ </div>
115
+ </div>
116
+ </button>
117
+ {/each}
118
+
119
+ <!-- Empty cells if needed -->
120
+ {#each Array(rows * cols - section.charts.length) as _, idx}
121
+ <div class="grid-cell-empty border rounded d-flex align-items-center justify-content-center text-muted">
122
+ <i class="bi bi-dash-circle"></i>
123
+ </div>
124
+ {/each}
125
+ </div>
126
+ </div>
127
+ {:else}
128
+ <div class="alert alert-info">
129
+ <i class="bi bi-info-circle me-2"></i>
130
+ No sections in this layout. Use the tree view to add sections.
131
+ </div>
132
+ {/each}
133
+ {:else}
134
+ <div class="text-center text-muted mt-5">
135
+ <i class="bi bi-eye-slash display-4"></i>
136
+ <p class="mt-3">No layout to preview</p>
137
+ </div>
138
+ {/if}
139
+ </div>
140
+
141
+ <style>
142
+ .grid-preview {
143
+ background: #f8f9fa;
144
+ }
145
+
146
+ .section-preview {
147
+ transition: box-shadow 0.2s;
148
+ }
149
+
150
+ .section-preview:hover {
151
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1) !important;
152
+ }
153
+
154
+ .grid-cell {
155
+ background: white;
156
+ border: 2px solid #dee2e6;
157
+ cursor: pointer;
158
+ transition: all 0.2s;
159
+ text-align: left;
160
+ overflow: hidden;
161
+ }
162
+
163
+ .grid-cell:hover {
164
+ border-color: #0d6efd;
165
+ box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
166
+ transform: translateY(-2px);
167
+ }
168
+
169
+ .grid-cell.selected {
170
+ border-color: #0d6efd;
171
+ border-width: 3px;
172
+ background-color: #e7f1ff;
173
+ }
174
+
175
+ .grid-cell-empty {
176
+ background: #f8f9fa;
177
+ border: 2px dashed #dee2e6;
178
+ }
179
+
180
+ .position-badge {
181
+ position: absolute;
182
+ top: 0.25rem;
183
+ right: 0.25rem;
184
+ background: #6c757d;
185
+ color: white;
186
+ width: 24px;
187
+ height: 24px;
188
+ border-radius: 50%;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ font-size: 0.75rem;
193
+ font-weight: bold;
194
+ }
195
+
196
+ .chart-title {
197
+ font-size: 0.9rem;
198
+ color: #212529;
199
+ }
200
+
201
+ .kpi-list {
202
+ font-size: 0.75rem;
203
+ }
204
+
205
+ .kpi-badge {
206
+ display: block;
207
+ width: 100%;
208
+ text-align: left;
209
+ font-size: 0.7rem;
210
+ padding: 0.25rem 0.5rem;
211
+ }
212
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const GridPreview: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type GridPreview = ReturnType<typeof GridPreview>;
3
+ export default GridPreview;