@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.
- package/dist/core/Charts/ChartCard.svelte +26 -4
- package/dist/core/Charts/editor/ChartLayoutEditor.svelte +281 -0
- package/dist/core/Charts/editor/ChartLayoutEditor.svelte.d.ts +3 -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 +207 -0
- package/dist/core/Charts/editor/KPIPicker.svelte.d.ts +24 -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/index.d.ts +3 -0
- package/dist/core/Charts/index.js +3 -0
- package/package.json +1 -1
@@ -0,0 +1,207 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import { createEventDispatcher } from 'svelte';
|
5
|
+
import { availableKPIs, searchKPIs } from '../../../../routes/charts/schemas/available-kpis.js';
|
6
|
+
import type { KPI } from '../charts.model.js';
|
7
|
+
|
8
|
+
interface Props {
|
9
|
+
show: boolean;
|
10
|
+
}
|
11
|
+
|
12
|
+
let { show }: Props = $props();
|
13
|
+
|
14
|
+
const dispatch = createEventDispatcher();
|
15
|
+
|
16
|
+
let searchQuery = $state('');
|
17
|
+
let selectedKPI = $state<KPI | null>(null);
|
18
|
+
|
19
|
+
let filteredKPIs = $derived(
|
20
|
+
searchQuery ? searchKPIs(searchQuery) : availableKPIs
|
21
|
+
);
|
22
|
+
|
23
|
+
function handleSelect(kpi: KPI) {
|
24
|
+
selectedKPI = kpi;
|
25
|
+
}
|
26
|
+
|
27
|
+
function handleConfirm() {
|
28
|
+
if (selectedKPI) {
|
29
|
+
dispatch('select', selectedKPI);
|
30
|
+
handleClose();
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
function handleClose() {
|
35
|
+
searchQuery = '';
|
36
|
+
selectedKPI = null;
|
37
|
+
dispatch('close');
|
38
|
+
}
|
39
|
+
|
40
|
+
function handleKeydown(e: KeyboardEvent) {
|
41
|
+
if (e.key === 'Escape') {
|
42
|
+
handleClose();
|
43
|
+
} else if (e.key === 'Enter' && selectedKPI) {
|
44
|
+
handleConfirm();
|
45
|
+
}
|
46
|
+
}
|
47
|
+
</script>
|
48
|
+
|
49
|
+
<svelte:window on:keydown={handleKeydown} />
|
50
|
+
|
51
|
+
{#if show}
|
52
|
+
<!-- Bootstrap Modal -->
|
53
|
+
<div class="modal fade show d-block" tabindex="-1" role="dialog" style="background-color: rgba(0,0,0,0.5);">
|
54
|
+
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
|
55
|
+
<div class="modal-content">
|
56
|
+
<!-- Header -->
|
57
|
+
<div class="modal-header">
|
58
|
+
<h5 class="modal-title">
|
59
|
+
<i class="bi bi-search me-2"></i>
|
60
|
+
Select KPI
|
61
|
+
</h5>
|
62
|
+
<button type="button" class="btn-close" onclick={handleClose}></button>
|
63
|
+
</div>
|
64
|
+
|
65
|
+
<!-- Body -->
|
66
|
+
<div class="modal-body">
|
67
|
+
<!-- Search Input -->
|
68
|
+
<div class="mb-3">
|
69
|
+
<div class="input-group">
|
70
|
+
<span class="input-group-text">
|
71
|
+
<i class="bi bi-search"></i>
|
72
|
+
</span>
|
73
|
+
<input
|
74
|
+
type="text"
|
75
|
+
class="form-control"
|
76
|
+
placeholder="Search KPIs by name or raw name..."
|
77
|
+
bind:value={searchQuery}
|
78
|
+
autofocus
|
79
|
+
/>
|
80
|
+
{#if searchQuery}
|
81
|
+
<button
|
82
|
+
class="btn btn-outline-secondary"
|
83
|
+
onclick={() => (searchQuery = '')}
|
84
|
+
>
|
85
|
+
<i class="bi bi-x"></i>
|
86
|
+
</button>
|
87
|
+
{/if}
|
88
|
+
</div>
|
89
|
+
<small class="text-muted">
|
90
|
+
Showing {filteredKPIs.length} of {availableKPIs.length} KPIs
|
91
|
+
</small>
|
92
|
+
</div>
|
93
|
+
|
94
|
+
<!-- KPI List -->
|
95
|
+
<div class="kpi-list-container">
|
96
|
+
{#if filteredKPIs.length > 0}
|
97
|
+
<div class="list-group">
|
98
|
+
{#each filteredKPIs as kpi}
|
99
|
+
<button
|
100
|
+
class="list-group-item list-group-item-action"
|
101
|
+
class:active={selectedKPI?.rawName === kpi.rawName}
|
102
|
+
onclick={() => handleSelect(kpi)}
|
103
|
+
>
|
104
|
+
<div class="d-flex w-100 justify-content-between align-items-start">
|
105
|
+
<div class="flex-grow-1">
|
106
|
+
<h6 class="mb-1">{kpi.name}</h6>
|
107
|
+
<p class="mb-1 text-muted small">
|
108
|
+
<code>{kpi.rawName}</code>
|
109
|
+
</p>
|
110
|
+
<div class="d-flex gap-2 align-items-center">
|
111
|
+
<span class="badge bg-secondary">{kpi.scale}</span>
|
112
|
+
<span class="badge bg-info">{kpi.unit}</span>
|
113
|
+
{#if kpi.color}
|
114
|
+
<span class="color-preview" style="background-color: {kpi.color}"></span>
|
115
|
+
{/if}
|
116
|
+
</div>
|
117
|
+
</div>
|
118
|
+
{#if selectedKPI?.rawName === kpi.rawName}
|
119
|
+
<i class="bi bi-check-circle-fill text-success fs-4"></i>
|
120
|
+
{/if}
|
121
|
+
</div>
|
122
|
+
</button>
|
123
|
+
{/each}
|
124
|
+
</div>
|
125
|
+
{:else}
|
126
|
+
<div class="alert alert-warning">
|
127
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
128
|
+
No KPIs found matching "{searchQuery}"
|
129
|
+
</div>
|
130
|
+
{/if}
|
131
|
+
</div>
|
132
|
+
</div>
|
133
|
+
|
134
|
+
<!-- Footer -->
|
135
|
+
<div class="modal-footer">
|
136
|
+
<button type="button" class="btn btn-secondary" onclick={handleClose}>
|
137
|
+
Cancel
|
138
|
+
</button>
|
139
|
+
<button
|
140
|
+
type="button"
|
141
|
+
class="btn btn-primary"
|
142
|
+
onclick={handleConfirm}
|
143
|
+
disabled={!selectedKPI}
|
144
|
+
>
|
145
|
+
<i class="bi bi-check-lg me-1"></i>
|
146
|
+
Add KPI
|
147
|
+
</button>
|
148
|
+
</div>
|
149
|
+
</div>
|
150
|
+
</div>
|
151
|
+
</div>
|
152
|
+
{/if}
|
153
|
+
|
154
|
+
<style>
|
155
|
+
.modal {
|
156
|
+
overflow-y: auto;
|
157
|
+
}
|
158
|
+
|
159
|
+
.kpi-list-container {
|
160
|
+
max-height: 400px;
|
161
|
+
overflow-y: auto;
|
162
|
+
}
|
163
|
+
|
164
|
+
.list-group-item {
|
165
|
+
cursor: pointer;
|
166
|
+
transition: all 0.15s;
|
167
|
+
}
|
168
|
+
|
169
|
+
.list-group-item:hover:not(.active) {
|
170
|
+
background-color: #f8f9fa;
|
171
|
+
}
|
172
|
+
|
173
|
+
.list-group-item.active {
|
174
|
+
background-color: #0d6efd;
|
175
|
+
border-color: #0d6efd;
|
176
|
+
}
|
177
|
+
|
178
|
+
.list-group-item.active h6,
|
179
|
+
.list-group-item.active p,
|
180
|
+
.list-group-item.active .text-muted {
|
181
|
+
color: white !important;
|
182
|
+
}
|
183
|
+
|
184
|
+
.list-group-item.active code {
|
185
|
+
background-color: rgba(255, 255, 255, 0.2);
|
186
|
+
color: white;
|
187
|
+
}
|
188
|
+
|
189
|
+
.color-preview {
|
190
|
+
display: inline-block;
|
191
|
+
width: 20px;
|
192
|
+
height: 20px;
|
193
|
+
border-radius: 4px;
|
194
|
+
border: 2px solid #dee2e6;
|
195
|
+
}
|
196
|
+
|
197
|
+
code {
|
198
|
+
background-color: #f8f9fa;
|
199
|
+
padding: 0.2rem 0.4rem;
|
200
|
+
border-radius: 3px;
|
201
|
+
font-size: 0.875rem;
|
202
|
+
}
|
203
|
+
|
204
|
+
.badge {
|
205
|
+
font-size: 0.75rem;
|
206
|
+
}
|
207
|
+
</style>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
interface Props {
|
2
|
+
show: boolean;
|
3
|
+
}
|
4
|
+
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> {
|
5
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
6
|
+
$$bindings?: Bindings;
|
7
|
+
} & Exports;
|
8
|
+
(internal: unknown, props: Props & {
|
9
|
+
$$events?: Events;
|
10
|
+
$$slots?: Slots;
|
11
|
+
}): Exports & {
|
12
|
+
$set?: any;
|
13
|
+
$on?: any;
|
14
|
+
};
|
15
|
+
z_$$bindings?: Bindings;
|
16
|
+
}
|
17
|
+
declare const KpiPicker: $$__sveltets_2_IsomorphicComponent<Props, {
|
18
|
+
select: CustomEvent<any>;
|
19
|
+
close: CustomEvent<any>;
|
20
|
+
} & {
|
21
|
+
[evt: string]: CustomEvent<any>;
|
22
|
+
}, {}, {}, "">;
|
23
|
+
type KpiPicker = InstanceType<typeof KpiPicker>;
|
24
|
+
export default KpiPicker;
|
@@ -0,0 +1,180 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import { editorStore, currentLayout, selection } from './editorState.js';
|
5
|
+
|
6
|
+
function handleSelectLayout() {
|
7
|
+
editorStore.select({ type: 'layout' });
|
8
|
+
}
|
9
|
+
|
10
|
+
function handleSelectSection(sectionId: string) {
|
11
|
+
editorStore.select({ type: 'section', sectionId });
|
12
|
+
}
|
13
|
+
|
14
|
+
function handleSelectChart(sectionId: string, chartIndex: number) {
|
15
|
+
editorStore.select({ type: 'chart', sectionId, chartIndex });
|
16
|
+
}
|
17
|
+
|
18
|
+
function handleAddSection() {
|
19
|
+
editorStore.addSection();
|
20
|
+
}
|
21
|
+
|
22
|
+
function handleAddChart(sectionId: string) {
|
23
|
+
editorStore.addChart(sectionId);
|
24
|
+
}
|
25
|
+
|
26
|
+
function isSelected(type: string, sectionId?: string, chartIndex?: number): boolean {
|
27
|
+
if ($selection.type !== type) return false;
|
28
|
+
if (type === 'section') return $selection.sectionId === sectionId;
|
29
|
+
if (type === 'chart') return $selection.sectionId === sectionId && $selection.chartIndex === chartIndex;
|
30
|
+
return type === 'layout';
|
31
|
+
}
|
32
|
+
</script>
|
33
|
+
|
34
|
+
<div class="tree-view h-100 d-flex flex-column">
|
35
|
+
<!-- Header -->
|
36
|
+
<div class="p-3 border-bottom bg-light">
|
37
|
+
<h6 class="mb-0">
|
38
|
+
<i class="bi bi-diagram-3"></i> Structure
|
39
|
+
</h6>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<!-- Tree Content -->
|
43
|
+
<div class="flex-grow-1 overflow-auto p-2">
|
44
|
+
{#if $currentLayout}
|
45
|
+
<!-- Layout Node -->
|
46
|
+
<div class="list-group mb-2">
|
47
|
+
<div
|
48
|
+
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
49
|
+
class:active={isSelected('layout')}
|
50
|
+
role="button"
|
51
|
+
tabindex="0"
|
52
|
+
onclick={handleSelectLayout}
|
53
|
+
onkeydown={(e) => e.key === 'Enter' && handleSelectLayout()}
|
54
|
+
>
|
55
|
+
<div>
|
56
|
+
<i class="bi bi-file-earmark-text me-2"></i>
|
57
|
+
<strong>{$currentLayout.layoutName}</strong>
|
58
|
+
</div>
|
59
|
+
<button
|
60
|
+
class="btn btn-sm btn-primary"
|
61
|
+
onclick={(e) => {
|
62
|
+
e.stopPropagation();
|
63
|
+
handleAddSection();
|
64
|
+
}}
|
65
|
+
title="Add Section"
|
66
|
+
>
|
67
|
+
<i class="bi bi-plus-lg me-1"></i>
|
68
|
+
<span>Section</span>
|
69
|
+
</button>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
|
73
|
+
<!-- Sections -->
|
74
|
+
{#each $currentLayout.sections as section, sIdx}
|
75
|
+
<div class="ms-3 mb-2">
|
76
|
+
<div class="list-group">
|
77
|
+
<!-- Section Node -->
|
78
|
+
<div
|
79
|
+
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
80
|
+
class:active={isSelected('section', section.id)}
|
81
|
+
role="button"
|
82
|
+
tabindex="0"
|
83
|
+
onclick={() => handleSelectSection(section.id)}
|
84
|
+
onkeydown={(e) => e.key === 'Enter' && handleSelectSection(section.id)}
|
85
|
+
>
|
86
|
+
<div class="flex-grow-1">
|
87
|
+
<i class="bi bi-grid-3x3-gap me-2"></i>
|
88
|
+
<span>{section.title}</span>
|
89
|
+
<span class="badge bg-secondary ms-2">{section.grid || '2x2'}</span>
|
90
|
+
</div>
|
91
|
+
<button
|
92
|
+
class="btn btn-sm btn-success"
|
93
|
+
onclick={(e) => {
|
94
|
+
e.stopPropagation();
|
95
|
+
handleAddChart(section.id);
|
96
|
+
}}
|
97
|
+
title="Add Chart"
|
98
|
+
>
|
99
|
+
<i class="bi bi-plus-lg me-1"></i>
|
100
|
+
<span>Chart</span>
|
101
|
+
</button>
|
102
|
+
</div>
|
103
|
+
|
104
|
+
<!-- Charts in Section -->
|
105
|
+
{#if section.charts && section.charts.length > 0}
|
106
|
+
<div class="ms-3 mt-1">
|
107
|
+
{#each section.charts as chart, cIdx}
|
108
|
+
<button
|
109
|
+
class="list-group-item list-group-item-action mb-1"
|
110
|
+
class:active={isSelected('chart', section.id, cIdx)}
|
111
|
+
onclick={() => handleSelectChart(section.id, cIdx)}
|
112
|
+
>
|
113
|
+
<div class="d-flex align-items-start">
|
114
|
+
<i class="bi bi-bar-chart me-2 mt-1"></i>
|
115
|
+
<div class="flex-grow-1">
|
116
|
+
<div class="fw-bold">{chart.title}</div>
|
117
|
+
<small class="text-muted">
|
118
|
+
{#if chart.pos}
|
119
|
+
<span class="badge bg-info me-1">Pos: {chart.pos}</span>
|
120
|
+
{/if}
|
121
|
+
<span class="badge bg-success me-1">L: {chart.yLeft.length}</span>
|
122
|
+
<span class="badge bg-warning">R: {chart.yRight.length}</span>
|
123
|
+
</small>
|
124
|
+
</div>
|
125
|
+
</div>
|
126
|
+
</button>
|
127
|
+
{/each}
|
128
|
+
</div>
|
129
|
+
{:else}
|
130
|
+
<div class="ms-3 mt-1">
|
131
|
+
<div class="text-muted small p-2">
|
132
|
+
<i class="bi bi-info-circle me-1"></i>
|
133
|
+
No charts yet
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
{/if}
|
137
|
+
</div>
|
138
|
+
</div>
|
139
|
+
{:else}
|
140
|
+
<div class="alert alert-info small m-2">
|
141
|
+
<i class="bi bi-info-circle me-1"></i>
|
142
|
+
No sections yet. Click the blue <strong>"+ Section"</strong> button above.
|
143
|
+
</div>
|
144
|
+
{/each}
|
145
|
+
{/if}
|
146
|
+
</div>
|
147
|
+
</div>
|
148
|
+
|
149
|
+
<style>
|
150
|
+
.tree-view {
|
151
|
+
font-size: 0.9rem;
|
152
|
+
}
|
153
|
+
|
154
|
+
.list-group-item {
|
155
|
+
padding: 0.5rem 0.75rem;
|
156
|
+
border-radius: 0.25rem;
|
157
|
+
margin-bottom: 0.25rem;
|
158
|
+
cursor: pointer;
|
159
|
+
}
|
160
|
+
|
161
|
+
.list-group-item.active {
|
162
|
+
background-color: #0d6efd;
|
163
|
+
border-color: #0d6efd;
|
164
|
+
color: white;
|
165
|
+
}
|
166
|
+
|
167
|
+
.list-group-item:not(.active):hover {
|
168
|
+
background-color: #f8f9fa;
|
169
|
+
}
|
170
|
+
|
171
|
+
.badge {
|
172
|
+
font-size: 0.7rem;
|
173
|
+
padding: 0.25rem 0.5rem;
|
174
|
+
}
|
175
|
+
|
176
|
+
.btn-sm {
|
177
|
+
padding: 0.15rem 0.4rem;
|
178
|
+
font-size: 0.75rem;
|
179
|
+
}
|
180
|
+
</style>
|
@@ -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>
|