@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.
@@ -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
+ });