@smartnet360/svelte-components 0.0.17 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/Charts/ChartCard.svelte +25 -4
- package/dist/core/Charts/editor/ChartLayoutEditor.svelte +289 -0
- package/dist/core/Charts/editor/ChartLayoutEditor.svelte.d.ts +7 -0
- package/dist/core/Charts/editor/GridPreview.svelte +212 -0
- package/dist/core/Charts/editor/GridPreview.svelte.d.ts +3 -0
- package/dist/core/Charts/editor/KPIPicker.svelte +217 -0
- package/dist/core/Charts/editor/KPIPicker.svelte.d.ts +26 -0
- package/dist/core/Charts/editor/LayoutTreeView.svelte +180 -0
- package/dist/core/Charts/editor/LayoutTreeView.svelte.d.ts +3 -0
- package/dist/core/Charts/editor/PropertiesPanel.svelte +286 -0
- package/dist/core/Charts/editor/PropertiesPanel.svelte.d.ts +20 -0
- package/dist/core/Charts/editor/editorState.d.ts +41 -0
- package/dist/core/Charts/editor/editorState.js +320 -0
- package/dist/core/Charts/editor/exampleKPIs.d.ts +38 -0
- package/dist/core/Charts/editor/exampleKPIs.js +66 -0
- package/dist/core/Charts/index.d.ts +4 -0
- package/dist/core/Charts/index.js +4 -0
- package/package.json +1 -1
@@ -0,0 +1,286 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import { createEventDispatcher } from 'svelte';
|
5
|
+
import { editorStore, currentLayout, selection, selectedItem } from './editorState.js';
|
6
|
+
import type { ChartGrid, Section, Chart } from '../charts.model.js';
|
7
|
+
|
8
|
+
const dispatch = createEventDispatcher();
|
9
|
+
|
10
|
+
const gridOptions: ChartGrid[] = ['2x2', '3x3', '4x4', '1x2', '1x4', '1x8'];
|
11
|
+
|
12
|
+
function handleOpenKPIPicker(side: 'yLeft' | 'yRight') {
|
13
|
+
if ($selection.type === 'chart' && $selection.sectionId && $selection.chartIndex !== undefined) {
|
14
|
+
dispatch('openkpipicker', {
|
15
|
+
sectionId: $selection.sectionId,
|
16
|
+
chartIndex: $selection.chartIndex,
|
17
|
+
side
|
18
|
+
});
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
function handleRemoveKPI(side: 'yLeft' | 'yRight', kpiIndex: number) {
|
23
|
+
if ($selection.type === 'chart' && $selection.sectionId && $selection.chartIndex !== undefined) {
|
24
|
+
editorStore.removeKPI($selection.sectionId, $selection.chartIndex, side, kpiIndex);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
function handleDeleteSection() {
|
29
|
+
if ($selection.sectionId && confirm('Delete this section and all its charts?')) {
|
30
|
+
editorStore.deleteSection($selection.sectionId);
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
function handleDeleteChart() {
|
35
|
+
if ($selection.sectionId && $selection.chartIndex !== undefined && confirm('Delete this chart?')) {
|
36
|
+
editorStore.deleteChart($selection.sectionId, $selection.chartIndex);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
</script>
|
40
|
+
|
41
|
+
<div class="properties-panel h-100 d-flex flex-column">
|
42
|
+
<!-- Header -->
|
43
|
+
<div class="p-3 border-bottom bg-light">
|
44
|
+
<h6 class="mb-0">
|
45
|
+
<i class="bi bi-sliders"></i> Properties
|
46
|
+
</h6>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<!-- Content -->
|
50
|
+
<div class="flex-grow-1 overflow-auto p-3">
|
51
|
+
{#if !$selection.type}
|
52
|
+
<div class="text-center text-muted mt-5">
|
53
|
+
<i class="bi bi-hand-index display-4"></i>
|
54
|
+
<p class="mt-3">Select an item to edit its properties</p>
|
55
|
+
</div>
|
56
|
+
{:else if $selection.type === 'layout'}
|
57
|
+
<!-- Layout Properties -->
|
58
|
+
<div class="mb-3">
|
59
|
+
<label class="form-label fw-bold">Layout Name</label>
|
60
|
+
<input
|
61
|
+
type="text"
|
62
|
+
class="form-control"
|
63
|
+
value={$currentLayout?.layoutName || ''}
|
64
|
+
oninput={(e) => editorStore.updateLayoutName(e.currentTarget.value)}
|
65
|
+
/>
|
66
|
+
</div>
|
67
|
+
|
68
|
+
<div class="alert alert-info small">
|
69
|
+
<i class="bi bi-info-circle me-1"></i>
|
70
|
+
<strong>{$currentLayout?.sections.length || 0}</strong> sections in this layout
|
71
|
+
</div>
|
72
|
+
{:else if $selection.type === 'section'}
|
73
|
+
{@const section = $selectedItem as Section}
|
74
|
+
{#if section}
|
75
|
+
<!-- Section Properties -->
|
76
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
77
|
+
<h6 class="mb-0">Section</h6>
|
78
|
+
<button class="btn btn-sm btn-outline-danger" onclick={handleDeleteSection}>
|
79
|
+
<i class="bi bi-trash"></i>
|
80
|
+
</button>
|
81
|
+
</div>
|
82
|
+
|
83
|
+
<div class="mb-3">
|
84
|
+
<label class="form-label">Title</label>
|
85
|
+
<input
|
86
|
+
type="text"
|
87
|
+
class="form-control"
|
88
|
+
value={section.title}
|
89
|
+
oninput={(e) =>
|
90
|
+
editorStore.updateSection(section.id, { title: e.currentTarget.value })}
|
91
|
+
/>
|
92
|
+
</div>
|
93
|
+
|
94
|
+
<div class="mb-3">
|
95
|
+
<label class="form-label">Section ID</label>
|
96
|
+
<input
|
97
|
+
type="text"
|
98
|
+
class="form-control"
|
99
|
+
value={section.id}
|
100
|
+
oninput={(e) => editorStore.updateSection(section.id, { id: e.currentTarget.value })}
|
101
|
+
/>
|
102
|
+
<small class="form-text text-muted">Unique identifier for this section</small>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<div class="mb-3">
|
106
|
+
<label class="form-label">Grid Layout</label>
|
107
|
+
<select
|
108
|
+
class="form-select"
|
109
|
+
value={section.grid || '2x2'}
|
110
|
+
onchange={(e) => editorStore.updateSection(section.id, { grid: e.currentTarget.value as ChartGrid })}
|
111
|
+
>
|
112
|
+
{#each gridOptions as grid}
|
113
|
+
<option value={grid}>{grid}</option>
|
114
|
+
{/each}
|
115
|
+
</select>
|
116
|
+
</div>
|
117
|
+
|
118
|
+
<div class="alert alert-info small">
|
119
|
+
<i class="bi bi-bar-chart me-1"></i>
|
120
|
+
<strong>{section.charts?.length || 0}</strong> charts in this section
|
121
|
+
</div>
|
122
|
+
{/if}
|
123
|
+
{:else if $selection.type === 'chart'}
|
124
|
+
{@const chart = $selectedItem as Chart}
|
125
|
+
{#if chart && $selection.sectionId && $selection.chartIndex !== undefined}
|
126
|
+
<!-- Chart Properties -->
|
127
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
128
|
+
<h6 class="mb-0">Chart</h6>
|
129
|
+
<button class="btn btn-sm btn-outline-danger" onclick={handleDeleteChart}>
|
130
|
+
<i class="bi bi-trash"></i>
|
131
|
+
</button>
|
132
|
+
</div>
|
133
|
+
|
134
|
+
<div class="mb-3">
|
135
|
+
<label class="form-label">Title</label>
|
136
|
+
<input
|
137
|
+
type="text"
|
138
|
+
class="form-control"
|
139
|
+
value={chart.title}
|
140
|
+
oninput={(e) =>
|
141
|
+
editorStore.updateChart($selection.sectionId!, $selection.chartIndex!, {
|
142
|
+
title: e.currentTarget.value
|
143
|
+
})}
|
144
|
+
/>
|
145
|
+
</div>
|
146
|
+
|
147
|
+
<div class="mb-3">
|
148
|
+
<label class="form-label">Position (optional)</label>
|
149
|
+
<input
|
150
|
+
type="number"
|
151
|
+
class="form-control"
|
152
|
+
min="1"
|
153
|
+
max="9"
|
154
|
+
value={chart.pos || ''}
|
155
|
+
oninput={(e) => {
|
156
|
+
const val = e.currentTarget.value;
|
157
|
+
const numVal = val ? parseInt(val) : undefined;
|
158
|
+
editorStore.updateChart($selection.sectionId!, $selection.chartIndex!, {
|
159
|
+
pos: (numVal && numVal >= 1 && numVal <= 9) ? numVal as any : undefined
|
160
|
+
});
|
161
|
+
}}
|
162
|
+
/>
|
163
|
+
<small class="form-text text-muted">Grid position (1-9)</small>
|
164
|
+
</div>
|
165
|
+
|
166
|
+
<!-- Left Y-Axis KPIs -->
|
167
|
+
<div class="mb-3">
|
168
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
169
|
+
<label class="form-label mb-0">Left Y-Axis KPIs</label>
|
170
|
+
<button
|
171
|
+
class="btn btn-sm btn-outline-success"
|
172
|
+
onclick={() => handleOpenKPIPicker('yLeft')}
|
173
|
+
>
|
174
|
+
<i class="bi bi-plus"></i> Add
|
175
|
+
</button>
|
176
|
+
</div>
|
177
|
+
|
178
|
+
{#if chart.yLeft.length > 0}
|
179
|
+
<div class="list-group">
|
180
|
+
{#each chart.yLeft as kpi, idx}
|
181
|
+
<div class="list-group-item d-flex justify-content-between align-items-start">
|
182
|
+
<div class="flex-grow-1">
|
183
|
+
<div class="fw-bold">{kpi.name}</div>
|
184
|
+
<small class="text-muted">
|
185
|
+
{kpi.rawName} | {kpi.unit}
|
186
|
+
{#if kpi.color}
|
187
|
+
<span class="color-badge" style="background-color: {kpi.color}"></span>
|
188
|
+
{/if}
|
189
|
+
</small>
|
190
|
+
</div>
|
191
|
+
<button
|
192
|
+
class="btn btn-sm btn-outline-danger"
|
193
|
+
onclick={() => handleRemoveKPI('yLeft', idx)}
|
194
|
+
>
|
195
|
+
<i class="bi bi-x"></i>
|
196
|
+
</button>
|
197
|
+
</div>
|
198
|
+
{/each}
|
199
|
+
</div>
|
200
|
+
{:else}
|
201
|
+
<div class="alert alert-secondary small mb-0">
|
202
|
+
No KPIs on left axis
|
203
|
+
</div>
|
204
|
+
{/if}
|
205
|
+
</div>
|
206
|
+
|
207
|
+
<!-- Right Y-Axis KPIs -->
|
208
|
+
<div class="mb-3">
|
209
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
210
|
+
<label class="form-label mb-0">Right Y-Axis KPIs</label>
|
211
|
+
<button
|
212
|
+
class="btn btn-sm btn-outline-warning"
|
213
|
+
onclick={() => handleOpenKPIPicker('yRight')}
|
214
|
+
>
|
215
|
+
<i class="bi bi-plus"></i> Add
|
216
|
+
</button>
|
217
|
+
</div>
|
218
|
+
|
219
|
+
{#if chart.yRight.length > 0}
|
220
|
+
<div class="list-group">
|
221
|
+
{#each chart.yRight as kpi, idx}
|
222
|
+
<div class="list-group-item d-flex justify-content-between align-items-start">
|
223
|
+
<div class="flex-grow-1">
|
224
|
+
<div class="fw-bold">{kpi.name}</div>
|
225
|
+
<small class="text-muted">
|
226
|
+
{kpi.rawName} | {kpi.unit}
|
227
|
+
{#if kpi.color}
|
228
|
+
<span class="color-badge" style="background-color: {kpi.color}"></span>
|
229
|
+
{/if}
|
230
|
+
</small>
|
231
|
+
</div>
|
232
|
+
<button
|
233
|
+
class="btn btn-sm btn-outline-danger"
|
234
|
+
onclick={() => handleRemoveKPI('yRight', idx)}
|
235
|
+
>
|
236
|
+
<i class="bi bi-x"></i>
|
237
|
+
</button>
|
238
|
+
</div>
|
239
|
+
{/each}
|
240
|
+
</div>
|
241
|
+
{:else}
|
242
|
+
<div class="alert alert-secondary small mb-0">
|
243
|
+
No KPIs on right axis
|
244
|
+
</div>
|
245
|
+
{/if}
|
246
|
+
</div>
|
247
|
+
{/if}
|
248
|
+
{/if}
|
249
|
+
</div>
|
250
|
+
</div>
|
251
|
+
|
252
|
+
<style>
|
253
|
+
.properties-panel {
|
254
|
+
font-size: 0.9rem;
|
255
|
+
}
|
256
|
+
|
257
|
+
.form-label {
|
258
|
+
font-weight: 600;
|
259
|
+
font-size: 0.85rem;
|
260
|
+
color: #495057;
|
261
|
+
}
|
262
|
+
|
263
|
+
.form-control,
|
264
|
+
.form-select {
|
265
|
+
font-size: 0.875rem;
|
266
|
+
}
|
267
|
+
|
268
|
+
.list-group-item {
|
269
|
+
padding: 0.5rem 0.75rem;
|
270
|
+
font-size: 0.85rem;
|
271
|
+
}
|
272
|
+
|
273
|
+
.color-badge {
|
274
|
+
display: inline-block;
|
275
|
+
width: 12px;
|
276
|
+
height: 12px;
|
277
|
+
border-radius: 2px;
|
278
|
+
border: 1px solid #dee2e6;
|
279
|
+
margin-left: 0.25rem;
|
280
|
+
}
|
281
|
+
|
282
|
+
.btn-sm {
|
283
|
+
padding: 0.15rem 0.4rem;
|
284
|
+
font-size: 0.75rem;
|
285
|
+
}
|
286
|
+
</style>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
3
|
+
$$bindings?: Bindings;
|
4
|
+
} & Exports;
|
5
|
+
(internal: unknown, props: {
|
6
|
+
$$events?: Events;
|
7
|
+
$$slots?: Slots;
|
8
|
+
}): Exports & {
|
9
|
+
$set?: any;
|
10
|
+
$on?: any;
|
11
|
+
};
|
12
|
+
z_$$bindings?: Bindings;
|
13
|
+
}
|
14
|
+
declare const PropertiesPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
15
|
+
openkpipicker: CustomEvent<any>;
|
16
|
+
} & {
|
17
|
+
[evt: string]: CustomEvent<any>;
|
18
|
+
}, {}, {}, "">;
|
19
|
+
type PropertiesPanel = InstanceType<typeof PropertiesPanel>;
|
20
|
+
export default PropertiesPanel;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import type { Layout, Section, Chart, KPI } from '../charts.model.js';
|
2
|
+
export type SelectionType = 'layout' | 'section' | 'chart' | 'kpi' | null;
|
3
|
+
export interface Selection {
|
4
|
+
type: SelectionType;
|
5
|
+
layoutId?: string;
|
6
|
+
sectionId?: string;
|
7
|
+
chartIndex?: number;
|
8
|
+
kpiSide?: 'yLeft' | 'yRight';
|
9
|
+
kpiIndex?: number;
|
10
|
+
}
|
11
|
+
export interface EditorState {
|
12
|
+
currentLayout: Layout | null;
|
13
|
+
savedLayouts: Layout[];
|
14
|
+
selection: Selection;
|
15
|
+
isDirty: boolean;
|
16
|
+
}
|
17
|
+
export declare const editorStore: {
|
18
|
+
subscribe: (this: void, run: import("svelte/store").Subscriber<EditorState>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
|
19
|
+
newLayout: (name?: string) => void;
|
20
|
+
loadLayout: (layout: Layout) => void;
|
21
|
+
updateLayoutName: (name: string) => void;
|
22
|
+
addSection: () => void;
|
23
|
+
updateSection: (sectionId: string, updates: Partial<Section>) => void;
|
24
|
+
deleteSection: (sectionId: string) => void;
|
25
|
+
addChart: (sectionId: string) => void;
|
26
|
+
updateChart: (sectionId: string, chartIndex: number, updates: Partial<Chart>) => void;
|
27
|
+
deleteChart: (sectionId: string, chartIndex: number) => void;
|
28
|
+
addKPI: (sectionId: string, chartIndex: number, side: "yLeft" | "yRight", kpi: KPI) => void;
|
29
|
+
removeKPI: (sectionId: string, chartIndex: number, side: "yLeft" | "yRight", kpiIndex: number) => void;
|
30
|
+
select: (selection: Selection) => void;
|
31
|
+
saveToLibrary: () => void;
|
32
|
+
deleteFromLibrary: (layoutName: string) => void;
|
33
|
+
exportToJSON: () => string;
|
34
|
+
importFromJSON: (json: string) => boolean;
|
35
|
+
reset: () => void;
|
36
|
+
};
|
37
|
+
export declare const currentLayout: import("svelte/store").Readable<Layout | null>;
|
38
|
+
export declare const savedLayouts: import("svelte/store").Readable<Layout[]>;
|
39
|
+
export declare const selection: import("svelte/store").Readable<Selection>;
|
40
|
+
export declare const isDirty: import("svelte/store").Readable<boolean>;
|
41
|
+
export declare const selectedItem: import("svelte/store").Readable<KPI | Chart | Section | Layout | null | undefined>;
|
@@ -0,0 +1,320 @@
|
|
1
|
+
import { writable, derived, get } from 'svelte/store';
|
2
|
+
const STORAGE_KEY = 'chartLayoutEditor:layouts';
|
3
|
+
const CURRENT_LAYOUT_KEY = 'chartLayoutEditor:currentLayout';
|
4
|
+
// Initialize state from localStorage
|
5
|
+
function loadFromStorage() {
|
6
|
+
if (typeof window === 'undefined') {
|
7
|
+
return {
|
8
|
+
currentLayout: null,
|
9
|
+
savedLayouts: [],
|
10
|
+
selection: { type: null },
|
11
|
+
isDirty: false
|
12
|
+
};
|
13
|
+
}
|
14
|
+
const savedLayoutsJson = localStorage.getItem(STORAGE_KEY);
|
15
|
+
const currentLayoutJson = localStorage.getItem(CURRENT_LAYOUT_KEY);
|
16
|
+
return {
|
17
|
+
currentLayout: currentLayoutJson ? JSON.parse(currentLayoutJson) : null,
|
18
|
+
savedLayouts: savedLayoutsJson ? JSON.parse(savedLayoutsJson) : [],
|
19
|
+
selection: { type: null },
|
20
|
+
isDirty: false
|
21
|
+
};
|
22
|
+
}
|
23
|
+
// Create the main store
|
24
|
+
function createEditorStore() {
|
25
|
+
const { subscribe, set, update } = writable(loadFromStorage());
|
26
|
+
return {
|
27
|
+
subscribe,
|
28
|
+
// Layout operations
|
29
|
+
newLayout: (name = 'New Layout') => {
|
30
|
+
const newLayout = {
|
31
|
+
layoutName: name,
|
32
|
+
sections: []
|
33
|
+
};
|
34
|
+
update(state => ({
|
35
|
+
...state,
|
36
|
+
currentLayout: newLayout,
|
37
|
+
selection: { type: 'layout' },
|
38
|
+
isDirty: true
|
39
|
+
}));
|
40
|
+
},
|
41
|
+
loadLayout: (layout) => {
|
42
|
+
// Use JSON parse/stringify to create a clean copy
|
43
|
+
const cleanLayout = JSON.parse(JSON.stringify(layout));
|
44
|
+
update(state => ({
|
45
|
+
...state,
|
46
|
+
currentLayout: cleanLayout,
|
47
|
+
selection: { type: 'layout' },
|
48
|
+
isDirty: false
|
49
|
+
}));
|
50
|
+
if (typeof window !== 'undefined') {
|
51
|
+
localStorage.setItem(CURRENT_LAYOUT_KEY, JSON.stringify(cleanLayout));
|
52
|
+
}
|
53
|
+
},
|
54
|
+
updateLayoutName: (name) => {
|
55
|
+
update(state => {
|
56
|
+
if (!state.currentLayout)
|
57
|
+
return state;
|
58
|
+
const updated = { ...state.currentLayout, layoutName: name };
|
59
|
+
return { ...state, currentLayout: updated, isDirty: true };
|
60
|
+
});
|
61
|
+
},
|
62
|
+
// Section operations
|
63
|
+
addSection: () => {
|
64
|
+
update(state => {
|
65
|
+
if (!state.currentLayout)
|
66
|
+
return state;
|
67
|
+
const newSection = {
|
68
|
+
id: `section-${Date.now()}`,
|
69
|
+
title: 'New Section',
|
70
|
+
charts: [],
|
71
|
+
grid: '2x2'
|
72
|
+
};
|
73
|
+
const updated = {
|
74
|
+
...state.currentLayout,
|
75
|
+
sections: [...state.currentLayout.sections, newSection]
|
76
|
+
};
|
77
|
+
return {
|
78
|
+
...state,
|
79
|
+
currentLayout: updated,
|
80
|
+
selection: { type: 'section', sectionId: newSection.id },
|
81
|
+
isDirty: true
|
82
|
+
};
|
83
|
+
});
|
84
|
+
},
|
85
|
+
updateSection: (sectionId, updates) => {
|
86
|
+
update(state => {
|
87
|
+
if (!state.currentLayout)
|
88
|
+
return state;
|
89
|
+
const sections = state.currentLayout.sections.map(s => s.id === sectionId ? { ...s, ...updates } : s);
|
90
|
+
return {
|
91
|
+
...state,
|
92
|
+
currentLayout: { ...state.currentLayout, sections },
|
93
|
+
isDirty: true
|
94
|
+
};
|
95
|
+
});
|
96
|
+
},
|
97
|
+
deleteSection: (sectionId) => {
|
98
|
+
update(state => {
|
99
|
+
if (!state.currentLayout)
|
100
|
+
return state;
|
101
|
+
const sections = state.currentLayout.sections.filter(s => s.id !== sectionId);
|
102
|
+
return {
|
103
|
+
...state,
|
104
|
+
currentLayout: { ...state.currentLayout, sections },
|
105
|
+
selection: { type: 'layout' },
|
106
|
+
isDirty: true
|
107
|
+
};
|
108
|
+
});
|
109
|
+
},
|
110
|
+
// Chart operations
|
111
|
+
addChart: (sectionId) => {
|
112
|
+
update(state => {
|
113
|
+
if (!state.currentLayout)
|
114
|
+
return state;
|
115
|
+
const newChart = {
|
116
|
+
title: 'New Chart',
|
117
|
+
yLeft: [],
|
118
|
+
yRight: []
|
119
|
+
};
|
120
|
+
const sections = state.currentLayout.sections.map(section => {
|
121
|
+
if (section.id === sectionId) {
|
122
|
+
return {
|
123
|
+
...section,
|
124
|
+
charts: [...section.charts, newChart]
|
125
|
+
};
|
126
|
+
}
|
127
|
+
return section;
|
128
|
+
});
|
129
|
+
return {
|
130
|
+
...state,
|
131
|
+
currentLayout: { ...state.currentLayout, sections },
|
132
|
+
isDirty: true
|
133
|
+
};
|
134
|
+
});
|
135
|
+
},
|
136
|
+
updateChart: (sectionId, chartIndex, updates) => {
|
137
|
+
update(state => {
|
138
|
+
if (!state.currentLayout)
|
139
|
+
return state;
|
140
|
+
const sections = state.currentLayout.sections.map(section => {
|
141
|
+
if (section.id === sectionId) {
|
142
|
+
const charts = section.charts.map((chart, idx) => idx === chartIndex ? { ...chart, ...updates } : chart);
|
143
|
+
return { ...section, charts };
|
144
|
+
}
|
145
|
+
return section;
|
146
|
+
});
|
147
|
+
return {
|
148
|
+
...state,
|
149
|
+
currentLayout: { ...state.currentLayout, sections },
|
150
|
+
isDirty: true
|
151
|
+
};
|
152
|
+
});
|
153
|
+
},
|
154
|
+
deleteChart: (sectionId, chartIndex) => {
|
155
|
+
update(state => {
|
156
|
+
if (!state.currentLayout)
|
157
|
+
return state;
|
158
|
+
const sections = state.currentLayout.sections.map(section => {
|
159
|
+
if (section.id === sectionId) {
|
160
|
+
const charts = section.charts.filter((_, idx) => idx !== chartIndex);
|
161
|
+
return { ...section, charts };
|
162
|
+
}
|
163
|
+
return section;
|
164
|
+
});
|
165
|
+
return {
|
166
|
+
...state,
|
167
|
+
currentLayout: { ...state.currentLayout, sections },
|
168
|
+
selection: { type: 'section', sectionId },
|
169
|
+
isDirty: true
|
170
|
+
};
|
171
|
+
});
|
172
|
+
},
|
173
|
+
// KPI operations
|
174
|
+
addKPI: (sectionId, chartIndex, side, kpi) => {
|
175
|
+
update(state => {
|
176
|
+
if (!state.currentLayout)
|
177
|
+
return state;
|
178
|
+
const sections = state.currentLayout.sections.map(section => {
|
179
|
+
if (section.id === sectionId) {
|
180
|
+
const charts = section.charts.map((chart, idx) => {
|
181
|
+
if (idx === chartIndex) {
|
182
|
+
return {
|
183
|
+
...chart,
|
184
|
+
[side]: [...chart[side], kpi]
|
185
|
+
};
|
186
|
+
}
|
187
|
+
return chart;
|
188
|
+
});
|
189
|
+
return { ...section, charts };
|
190
|
+
}
|
191
|
+
return section;
|
192
|
+
});
|
193
|
+
return {
|
194
|
+
...state,
|
195
|
+
currentLayout: { ...state.currentLayout, sections },
|
196
|
+
isDirty: true
|
197
|
+
};
|
198
|
+
});
|
199
|
+
},
|
200
|
+
removeKPI: (sectionId, chartIndex, side, kpiIndex) => {
|
201
|
+
update(state => {
|
202
|
+
if (!state.currentLayout)
|
203
|
+
return state;
|
204
|
+
const sections = state.currentLayout.sections.map(section => {
|
205
|
+
if (section.id === sectionId) {
|
206
|
+
const charts = section.charts.map((chart, idx) => {
|
207
|
+
if (idx === chartIndex) {
|
208
|
+
return {
|
209
|
+
...chart,
|
210
|
+
[side]: chart[side].filter((_, i) => i !== kpiIndex)
|
211
|
+
};
|
212
|
+
}
|
213
|
+
return chart;
|
214
|
+
});
|
215
|
+
return { ...section, charts };
|
216
|
+
}
|
217
|
+
return section;
|
218
|
+
});
|
219
|
+
return {
|
220
|
+
...state,
|
221
|
+
currentLayout: { ...state.currentLayout, sections },
|
222
|
+
isDirty: true
|
223
|
+
};
|
224
|
+
});
|
225
|
+
},
|
226
|
+
// Selection management
|
227
|
+
select: (selection) => {
|
228
|
+
update(state => ({ ...state, selection }));
|
229
|
+
},
|
230
|
+
// Save/Load operations
|
231
|
+
saveToLibrary: () => {
|
232
|
+
update(state => {
|
233
|
+
if (!state.currentLayout)
|
234
|
+
return state;
|
235
|
+
// Serialize and deserialize to create a clean copy without component references
|
236
|
+
const cleanLayout = JSON.parse(JSON.stringify(state.currentLayout));
|
237
|
+
// Check if layout already exists and update it
|
238
|
+
const existingIndex = state.savedLayouts.findIndex(l => l.layoutName === cleanLayout.layoutName);
|
239
|
+
let savedLayouts;
|
240
|
+
if (existingIndex >= 0) {
|
241
|
+
savedLayouts = state.savedLayouts.map((l, idx) => idx === existingIndex ? cleanLayout : l);
|
242
|
+
}
|
243
|
+
else {
|
244
|
+
savedLayouts = [...state.savedLayouts, cleanLayout];
|
245
|
+
}
|
246
|
+
// Save to localStorage
|
247
|
+
if (typeof window !== 'undefined') {
|
248
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedLayouts));
|
249
|
+
localStorage.setItem(CURRENT_LAYOUT_KEY, JSON.stringify(state.currentLayout));
|
250
|
+
}
|
251
|
+
return { ...state, savedLayouts, isDirty: false };
|
252
|
+
});
|
253
|
+
},
|
254
|
+
deleteFromLibrary: (layoutName) => {
|
255
|
+
update(state => {
|
256
|
+
const savedLayouts = state.savedLayouts.filter(l => l.layoutName !== layoutName);
|
257
|
+
if (typeof window !== 'undefined') {
|
258
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedLayouts));
|
259
|
+
}
|
260
|
+
return { ...state, savedLayouts };
|
261
|
+
});
|
262
|
+
},
|
263
|
+
// Export/Import
|
264
|
+
exportToJSON: () => {
|
265
|
+
const state = get(editorStore);
|
266
|
+
if (!state.currentLayout)
|
267
|
+
return '';
|
268
|
+
return JSON.stringify(state.currentLayout, null, 2);
|
269
|
+
},
|
270
|
+
importFromJSON: (json) => {
|
271
|
+
try {
|
272
|
+
const layout = JSON.parse(json);
|
273
|
+
update(state => ({
|
274
|
+
...state,
|
275
|
+
currentLayout: layout,
|
276
|
+
selection: { type: 'layout' },
|
277
|
+
isDirty: true
|
278
|
+
}));
|
279
|
+
return true;
|
280
|
+
}
|
281
|
+
catch (error) {
|
282
|
+
console.error('Failed to import layout:', error);
|
283
|
+
return false;
|
284
|
+
}
|
285
|
+
},
|
286
|
+
// Reset
|
287
|
+
reset: () => {
|
288
|
+
set(loadFromStorage());
|
289
|
+
}
|
290
|
+
};
|
291
|
+
}
|
292
|
+
export const editorStore = createEditorStore();
|
293
|
+
// Derived stores for convenience
|
294
|
+
export const currentLayout = derived(editorStore, $state => $state.currentLayout);
|
295
|
+
export const savedLayouts = derived(editorStore, $state => $state.savedLayouts);
|
296
|
+
export const selection = derived(editorStore, $state => $state.selection);
|
297
|
+
export const isDirty = derived(editorStore, $state => $state.isDirty);
|
298
|
+
// Helper to get selected item
|
299
|
+
export const selectedItem = derived(editorStore, $state => {
|
300
|
+
if (!$state.currentLayout || !$state.selection.type)
|
301
|
+
return null;
|
302
|
+
switch ($state.selection.type) {
|
303
|
+
case 'layout':
|
304
|
+
return $state.currentLayout;
|
305
|
+
case 'section':
|
306
|
+
return $state.currentLayout.sections.find(s => s.id === $state.selection.sectionId);
|
307
|
+
case 'chart': {
|
308
|
+
const section = $state.currentLayout.sections.find(s => s.id === $state.selection.sectionId);
|
309
|
+
return section?.charts[$state.selection.chartIndex];
|
310
|
+
}
|
311
|
+
case 'kpi': {
|
312
|
+
const section = $state.currentLayout.sections.find(s => s.id === $state.selection.sectionId);
|
313
|
+
const chart = section?.charts[$state.selection.chartIndex];
|
314
|
+
const side = $state.selection.kpiSide;
|
315
|
+
return chart?.[side][$state.selection.kpiIndex];
|
316
|
+
}
|
317
|
+
default:
|
318
|
+
return null;
|
319
|
+
}
|
320
|
+
});
|