@smartnet360/svelte-components 0.0.37 → 0.0.39
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/apps/site-check/SiteCheck.svelte +7 -6
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +2 -1
- package/dist/apps/site-check/default-cell-styling.d.ts +6 -0
- package/dist/apps/site-check/default-cell-styling.js +24 -0
- package/dist/apps/site-check/default-cell-styling.json +20 -0
- package/dist/apps/site-check/helper.d.ts +3 -2
- package/dist/apps/site-check/helper.js +16 -15
- package/dist/apps/site-check/index.d.ts +2 -1
- package/dist/apps/site-check/index.js +4 -2
- package/dist/apps/site-check/transforms.d.ts +32 -0
- package/dist/apps/site-check/transforms.js +101 -3
- package/dist/core/Charts/ChartCard.svelte +7 -5
- package/dist/core/Charts/ChartCard.svelte.d.ts +3 -1
- package/dist/core/Charts/ChartComponent.svelte +3 -1
- package/dist/core/Charts/adapt.js +19 -27
- package/dist/core/Charts/charts.model.d.ts +9 -0
- package/dist/core/Charts/data-utils.d.ts +4 -4
- package/dist/core/Charts/data-utils.js +69 -10
- package/dist/core/Charts/index.d.ts +1 -1
- package/dist/core/Settings/FieldRenderer.svelte +234 -0
- package/dist/core/Settings/FieldRenderer.svelte.d.ts +30 -0
- package/dist/core/Settings/Settings.svelte +199 -0
- package/dist/core/Settings/Settings.svelte.d.ts +24 -0
- package/dist/core/Settings/index.d.ts +9 -0
- package/dist/core/Settings/index.js +8 -0
- package/dist/core/Settings/store.d.ts +56 -0
- package/dist/core/Settings/store.js +184 -0
- package/dist/core/Settings/types.d.ts +162 -0
- package/dist/core/Settings/types.js +7 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/package.json +1 -1
|
@@ -11,6 +11,27 @@ const modernColors = [
|
|
|
11
11
|
'#EC4899', // Pink
|
|
12
12
|
'#6B7280' // Gray
|
|
13
13
|
];
|
|
14
|
+
/**
|
|
15
|
+
* Darken a hex color by a given amount
|
|
16
|
+
* @param color - Hex color string (e.g., '#3B82F6')
|
|
17
|
+
* @param amount - Amount to darken (0-1, where 0.2 = 20% darker)
|
|
18
|
+
* @returns Darkened hex color
|
|
19
|
+
*/
|
|
20
|
+
function darkenColor(color, amount) {
|
|
21
|
+
// Remove # if present
|
|
22
|
+
const hex = color.replace('#', '');
|
|
23
|
+
// Parse RGB components
|
|
24
|
+
const r = parseInt(hex.substr(0, 2), 16);
|
|
25
|
+
const g = parseInt(hex.substr(2, 2), 16);
|
|
26
|
+
const b = parseInt(hex.substr(4, 2), 16);
|
|
27
|
+
// Darken each component
|
|
28
|
+
const newR = Math.round(r * (1 - amount));
|
|
29
|
+
const newG = Math.round(g * (1 - amount));
|
|
30
|
+
const newB = Math.round(b * (1 - amount));
|
|
31
|
+
// Convert back to hex
|
|
32
|
+
const toHex = (n) => n.toString(16).padStart(2, '0');
|
|
33
|
+
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
|
34
|
+
}
|
|
14
35
|
// Cache for processed KPI data to avoid reprocessing on every render
|
|
15
36
|
const dataCache = new WeakMap();
|
|
16
37
|
export function processKPIData(data, kpi) {
|
|
@@ -72,7 +93,7 @@ export function calculateMovingAverage(values, window) {
|
|
|
72
93
|
maCache.set(cacheKey, result);
|
|
73
94
|
return result;
|
|
74
95
|
}
|
|
75
|
-
export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup) {
|
|
96
|
+
export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup, coloredHover = true) {
|
|
76
97
|
// Use KPI color if provided, otherwise cycle through modern colors
|
|
77
98
|
const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
|
|
78
99
|
// Base trace configuration
|
|
@@ -85,6 +106,18 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
85
106
|
`Value: %{y:,.2f} ${kpi.unit}<br>` +
|
|
86
107
|
'<extra></extra>'
|
|
87
108
|
};
|
|
109
|
+
// Add colored hover styling if enabled
|
|
110
|
+
if (coloredHover) {
|
|
111
|
+
baseTrace.hoverlabel = {
|
|
112
|
+
bgcolor: traceColor,
|
|
113
|
+
bordercolor: traceColor,
|
|
114
|
+
font: {
|
|
115
|
+
color: '#ffffff', // White text for better contrast
|
|
116
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
117
|
+
size: 11
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
88
121
|
// Configure based on chart type
|
|
89
122
|
switch (chartType) {
|
|
90
123
|
case 'stacked-area':
|
|
@@ -94,7 +127,10 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
94
127
|
mode: 'lines',
|
|
95
128
|
fill: 'tonexty',
|
|
96
129
|
stackgroup: stackGroup || 'one',
|
|
97
|
-
line: {
|
|
130
|
+
line: {
|
|
131
|
+
width: 1.5, // Visible border width
|
|
132
|
+
color: darkenColor(traceColor, 0.25) // 25% darker border for better separation
|
|
133
|
+
},
|
|
98
134
|
fillcolor: traceColor
|
|
99
135
|
};
|
|
100
136
|
case 'stacked-percentage':
|
|
@@ -105,7 +141,10 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
105
141
|
fill: 'tonexty',
|
|
106
142
|
stackgroup: stackGroup || 'one',
|
|
107
143
|
groupnorm: 'percent',
|
|
108
|
-
line: {
|
|
144
|
+
line: {
|
|
145
|
+
width: 1.5, // Visible border width
|
|
146
|
+
color: darkenColor(traceColor, 0.25) // 25% darker border for better separation
|
|
147
|
+
},
|
|
109
148
|
fillcolor: traceColor,
|
|
110
149
|
hovertemplate: `<b>${kpi.name}</b><br>` +
|
|
111
150
|
`Percentage: %{y:.1f}%<br>` +
|
|
@@ -138,7 +177,7 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
138
177
|
width: 3,
|
|
139
178
|
shape: 'spline',
|
|
140
179
|
smoothing: 0.3,
|
|
141
|
-
dash: yaxis === 'y1' ? 'solid' : 'dot'
|
|
180
|
+
dash: kpi.lineStyle || (yaxis === 'y1' ? 'solid' : 'dot')
|
|
142
181
|
}
|
|
143
182
|
};
|
|
144
183
|
}
|
|
@@ -154,12 +193,12 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
154
193
|
* @param stackGroup - Optional stack group identifier
|
|
155
194
|
* @returns Array of traces (original + MA if configured)
|
|
156
195
|
*/
|
|
157
|
-
export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup) {
|
|
196
|
+
export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup, coloredHover = true) {
|
|
158
197
|
const traces = [];
|
|
159
198
|
const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
|
|
160
199
|
// Add original trace (unless explicitly disabled)
|
|
161
200
|
if (!kpi.movingAverage || kpi.movingAverage.showOriginal !== false) {
|
|
162
|
-
const originalTrace = createTimeSeriesTrace(values, timestamps, kpi, yaxis, colorIndex, chartType, stackGroup);
|
|
201
|
+
const originalTrace = createTimeSeriesTrace(values, timestamps, kpi, yaxis, colorIndex, chartType, stackGroup, coloredHover);
|
|
163
202
|
// If MA is enabled, make the original line slightly transparent
|
|
164
203
|
if (kpi.movingAverage?.enabled) {
|
|
165
204
|
originalTrace.opacity = 0.4;
|
|
@@ -190,7 +229,18 @@ export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1
|
|
|
190
229
|
},
|
|
191
230
|
hovertemplate: `<b>${maLabel}</b><br>` +
|
|
192
231
|
`Value: %{y:,.2f} ${kpi.unit}<br>` +
|
|
193
|
-
'<extra></extra>'
|
|
232
|
+
'<extra></extra>',
|
|
233
|
+
...(coloredHover && {
|
|
234
|
+
hoverlabel: {
|
|
235
|
+
bgcolor: traceColor,
|
|
236
|
+
bordercolor: traceColor,
|
|
237
|
+
font: {
|
|
238
|
+
color: '#ffffff',
|
|
239
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
240
|
+
size: 11
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
})
|
|
194
244
|
};
|
|
195
245
|
traces.push(maTrace);
|
|
196
246
|
}
|
|
@@ -209,7 +259,7 @@ export function formatValue(value, scale, unit) {
|
|
|
209
259
|
}
|
|
210
260
|
return `${value.toLocaleString()}${unit}`;
|
|
211
261
|
}
|
|
212
|
-
export function createDefaultPlotlyLayout(title) {
|
|
262
|
+
export function createDefaultPlotlyLayout(title, hoverMode, coloredHover = true) {
|
|
213
263
|
return {
|
|
214
264
|
title: title ? {
|
|
215
265
|
text: title,
|
|
@@ -254,8 +304,17 @@ export function createDefaultPlotlyLayout(title) {
|
|
|
254
304
|
paper_bgcolor: 'rgba(0,0,0,0)',
|
|
255
305
|
plot_bgcolor: 'rgba(0,0,0,0)',
|
|
256
306
|
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' },
|
|
257
|
-
hovermode: 'x',
|
|
258
|
-
hoverlabel: {
|
|
307
|
+
hovermode: hoverMode !== undefined ? hoverMode : 'x',
|
|
308
|
+
hoverlabel: coloredHover ? {
|
|
309
|
+
// When coloredHover is enabled, let each trace control its own hover colors
|
|
310
|
+
font: {
|
|
311
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
312
|
+
size: 11
|
|
313
|
+
// color will be set per trace when coloredHover is true
|
|
314
|
+
}
|
|
315
|
+
// bgcolor and bordercolor will be set per trace when coloredHover is true
|
|
316
|
+
} : {
|
|
317
|
+
// Default hover styling when coloredHover is disabled
|
|
259
318
|
font: {
|
|
260
319
|
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
261
320
|
size: 11,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { default as ChartComponent } from './ChartComponent.svelte';
|
|
2
2
|
export { default as ChartCard } from './ChartCard.svelte';
|
|
3
|
-
export type { Layout, Section, Chart, KPI, Mode, Scale, ChartMarker, ChartGrid, ChartPosition } from './charts.model.js';
|
|
3
|
+
export type { Layout, Section, Chart, KPI, Mode, Scale, ChartMarker, ChartGrid, ChartPosition, HoverMode, LineStyle, CellStylingConfig } from './charts.model.js';
|
|
4
4
|
export { createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
|
|
5
5
|
export { adaptPlotlyLayout, getSizeCategory, createMarkerShapes, createMarkerAnnotations, addMarkersToLayout } from './adapt.js';
|
|
6
6
|
export type { ContainerSize, ChartInfo, AdaptationConfig } from './adapt.js';
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FieldDefinition } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic Field Renderer
|
|
6
|
+
*
|
|
7
|
+
* Renders a form field based on its type definition.
|
|
8
|
+
* Uses Bootstrap form components for styling.
|
|
9
|
+
* Works with hierarchical store structure.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export let field: FieldDefinition;
|
|
13
|
+
export let segmentId: string;
|
|
14
|
+
export let store: any; // The hierarchical settings store
|
|
15
|
+
export let value: any;
|
|
16
|
+
|
|
17
|
+
// Generate unique ID for form elements
|
|
18
|
+
const inputId = `field-${segmentId}-${field.id}`;
|
|
19
|
+
|
|
20
|
+
// Handle value changes - update the hierarchical store
|
|
21
|
+
function handleChange(newValue: any) {
|
|
22
|
+
store.update(segmentId, field.id, newValue);
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="field-wrapper mb-3">
|
|
27
|
+
{#if field.type === 'boolean'}
|
|
28
|
+
<div class="form-check form-switch">
|
|
29
|
+
<input
|
|
30
|
+
id={inputId}
|
|
31
|
+
type="checkbox"
|
|
32
|
+
class="form-check-input"
|
|
33
|
+
checked={value}
|
|
34
|
+
disabled={field.disabled}
|
|
35
|
+
on:change={(e) => handleChange(e.currentTarget.checked)}
|
|
36
|
+
/>
|
|
37
|
+
<label class="form-check-label" for={inputId}>
|
|
38
|
+
{field.label}
|
|
39
|
+
</label>
|
|
40
|
+
{#if field.description}
|
|
41
|
+
<div class="form-text">{field.description}</div>
|
|
42
|
+
{/if}
|
|
43
|
+
</div>
|
|
44
|
+
{:else if field.type === 'text'}
|
|
45
|
+
<label for={inputId} class="form-label">
|
|
46
|
+
{field.label}
|
|
47
|
+
{#if field.tooltip}
|
|
48
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
49
|
+
{/if}
|
|
50
|
+
</label>
|
|
51
|
+
<input
|
|
52
|
+
id={inputId}
|
|
53
|
+
type="text"
|
|
54
|
+
class="form-control"
|
|
55
|
+
value={value}
|
|
56
|
+
placeholder={field.placeholder}
|
|
57
|
+
maxlength={field.maxLength}
|
|
58
|
+
pattern={field.pattern}
|
|
59
|
+
disabled={field.disabled}
|
|
60
|
+
on:input={(e) => handleChange(e.currentTarget.value)}
|
|
61
|
+
/>
|
|
62
|
+
{#if field.description}
|
|
63
|
+
<div class="form-text">{field.description}</div>
|
|
64
|
+
{/if}
|
|
65
|
+
{:else if field.type === 'number'}
|
|
66
|
+
<label for={inputId} class="form-label">
|
|
67
|
+
{field.label}
|
|
68
|
+
{#if field.tooltip}
|
|
69
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
70
|
+
{/if}
|
|
71
|
+
</label>
|
|
72
|
+
<div class="input-group">
|
|
73
|
+
<input
|
|
74
|
+
id={inputId}
|
|
75
|
+
type="number"
|
|
76
|
+
class="form-control"
|
|
77
|
+
value={value}
|
|
78
|
+
min={field.min}
|
|
79
|
+
max={field.max}
|
|
80
|
+
step={field.step}
|
|
81
|
+
disabled={field.disabled}
|
|
82
|
+
on:input={(e) => handleChange(parseFloat(e.currentTarget.value))}
|
|
83
|
+
/>
|
|
84
|
+
{#if field.unit}
|
|
85
|
+
<span class="input-group-text">{field.unit}</span>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
{#if field.description}
|
|
89
|
+
<div class="form-text">{field.description}</div>
|
|
90
|
+
{/if}
|
|
91
|
+
{:else if field.type === 'range'}
|
|
92
|
+
<label for={inputId} class="form-label">
|
|
93
|
+
{field.label}
|
|
94
|
+
{#if field.showValue !== false}
|
|
95
|
+
<span class="badge bg-secondary ms-2">{value}{field.unit || ''}</span>
|
|
96
|
+
{/if}
|
|
97
|
+
{#if field.tooltip}
|
|
98
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
99
|
+
{/if}
|
|
100
|
+
</label>
|
|
101
|
+
<input
|
|
102
|
+
id={inputId}
|
|
103
|
+
type="range"
|
|
104
|
+
class="form-range"
|
|
105
|
+
value={value}
|
|
106
|
+
min={field.min}
|
|
107
|
+
max={field.max}
|
|
108
|
+
step={field.step || 1}
|
|
109
|
+
disabled={field.disabled}
|
|
110
|
+
on:input={(e) => handleChange(parseFloat(e.currentTarget.value))}
|
|
111
|
+
/>
|
|
112
|
+
{#if field.description}
|
|
113
|
+
<div class="form-text">{field.description}</div>
|
|
114
|
+
{/if}
|
|
115
|
+
{:else if field.type === 'color'}
|
|
116
|
+
<label for={inputId} class="form-label">
|
|
117
|
+
{field.label}
|
|
118
|
+
{#if field.tooltip}
|
|
119
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
120
|
+
{/if}
|
|
121
|
+
</label>
|
|
122
|
+
<div class="input-group" style="max-width: 200px;">
|
|
123
|
+
<input
|
|
124
|
+
id={inputId}
|
|
125
|
+
type="color"
|
|
126
|
+
class="form-control form-control-color"
|
|
127
|
+
value={value}
|
|
128
|
+
disabled={field.disabled}
|
|
129
|
+
on:input={(e) => handleChange(e.currentTarget.value)}
|
|
130
|
+
/>
|
|
131
|
+
<input
|
|
132
|
+
type="text"
|
|
133
|
+
class="form-control"
|
|
134
|
+
value={value}
|
|
135
|
+
disabled={field.disabled}
|
|
136
|
+
on:input={(e) => handleChange(e.currentTarget.value)}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
{#if field.description}
|
|
140
|
+
<div class="form-text">{field.description}</div>
|
|
141
|
+
{/if}
|
|
142
|
+
{:else if field.type === 'select'}
|
|
143
|
+
<label for={inputId} class="form-label">
|
|
144
|
+
{field.label}
|
|
145
|
+
{#if field.tooltip}
|
|
146
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
147
|
+
{/if}
|
|
148
|
+
</label>
|
|
149
|
+
<select
|
|
150
|
+
id={inputId}
|
|
151
|
+
class="form-select"
|
|
152
|
+
value={value}
|
|
153
|
+
disabled={field.disabled}
|
|
154
|
+
on:change={(e) => handleChange(e.currentTarget.value)}
|
|
155
|
+
>
|
|
156
|
+
{#each field.options as option}
|
|
157
|
+
<option value={option.value}>
|
|
158
|
+
{option.label}
|
|
159
|
+
</option>
|
|
160
|
+
{/each}
|
|
161
|
+
</select>
|
|
162
|
+
{#if field.description}
|
|
163
|
+
<div class="form-text">{field.description}</div>
|
|
164
|
+
{/if}
|
|
165
|
+
{:else if field.type === 'radio'}
|
|
166
|
+
<label class="form-label">
|
|
167
|
+
{field.label}
|
|
168
|
+
{#if field.tooltip}
|
|
169
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
170
|
+
{/if}
|
|
171
|
+
</label>
|
|
172
|
+
{#each field.options as option, index}
|
|
173
|
+
<div class="form-check">
|
|
174
|
+
<input
|
|
175
|
+
id={`${inputId}-${index}`}
|
|
176
|
+
type="radio"
|
|
177
|
+
class="form-check-input"
|
|
178
|
+
name={field.id}
|
|
179
|
+
value={option.value}
|
|
180
|
+
checked={value === option.value}
|
|
181
|
+
disabled={field.disabled}
|
|
182
|
+
on:change={() => handleChange(option.value)}
|
|
183
|
+
/>
|
|
184
|
+
<label class="form-check-label" for={`${inputId}-${index}`}>
|
|
185
|
+
{option.label}
|
|
186
|
+
{#if option.description}
|
|
187
|
+
<small class="text-muted d-block">{option.description}</small>
|
|
188
|
+
{/if}
|
|
189
|
+
</label>
|
|
190
|
+
</div>
|
|
191
|
+
{/each}
|
|
192
|
+
{#if field.description}
|
|
193
|
+
<div class="form-text mt-2">{field.description}</div>
|
|
194
|
+
{/if}
|
|
195
|
+
{:else if field.type === 'custom'}
|
|
196
|
+
<label class="form-label">
|
|
197
|
+
{field.label}
|
|
198
|
+
{#if field.tooltip}
|
|
199
|
+
<span class="text-muted" title={field.tooltip}>ⓘ</span>
|
|
200
|
+
{/if}
|
|
201
|
+
</label>
|
|
202
|
+
<svelte:component
|
|
203
|
+
this={field.component}
|
|
204
|
+
{value}
|
|
205
|
+
disabled={field.disabled}
|
|
206
|
+
on:change={(e: CustomEvent) => handleChange(e.detail)}
|
|
207
|
+
{...field.componentProps}
|
|
208
|
+
/>
|
|
209
|
+
{#if field.description}
|
|
210
|
+
<div class="form-text">{field.description}</div>
|
|
211
|
+
{/if}
|
|
212
|
+
{/if}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<style>
|
|
216
|
+
.field-wrapper {
|
|
217
|
+
animation: fadeIn 0.2s ease-in;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@keyframes fadeIn {
|
|
221
|
+
from {
|
|
222
|
+
opacity: 0;
|
|
223
|
+
transform: translateY(-5px);
|
|
224
|
+
}
|
|
225
|
+
to {
|
|
226
|
+
opacity: 1;
|
|
227
|
+
transform: translateY(0);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.text-muted {
|
|
232
|
+
cursor: help;
|
|
233
|
+
}
|
|
234
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FieldDefinition } from './types';
|
|
2
|
+
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> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: Props & {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const FieldRenderer: $$__sveltets_2_IsomorphicComponent<{
|
|
16
|
+
/**
|
|
17
|
+
* Generic Field Renderer
|
|
18
|
+
*
|
|
19
|
+
* Renders a form field based on its type definition.
|
|
20
|
+
* Uses Bootstrap form components for styling.
|
|
21
|
+
* Works with hierarchical store structure.
|
|
22
|
+
*/ field: FieldDefinition;
|
|
23
|
+
segmentId: string;
|
|
24
|
+
store: any;
|
|
25
|
+
value: any;
|
|
26
|
+
}, {
|
|
27
|
+
[evt: string]: CustomEvent<any>;
|
|
28
|
+
}, {}, {}, string>;
|
|
29
|
+
type FieldRenderer = InstanceType<typeof FieldRenderer>;
|
|
30
|
+
export default FieldRenderer;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { SettingsSchema } from './types';
|
|
4
|
+
import { createSettingsStore } from './store';
|
|
5
|
+
import FieldRenderer from './FieldRenderer.svelte';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Settings Component
|
|
9
|
+
*
|
|
10
|
+
* Reusable settings panel that accepts a schema and renders
|
|
11
|
+
* a data-driven UI with automatic persistence.
|
|
12
|
+
* Provides hierarchical access: $settings.segment.field
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```svelte
|
|
16
|
+
* const settings = createSettingsStore(mySchema, 'myApp');
|
|
17
|
+
* <Settings schema={mySchema} {settings} />
|
|
18
|
+
*
|
|
19
|
+
* // Access settings in parent:
|
|
20
|
+
* $settings.appearance.theme
|
|
21
|
+
* $settings.editor.fontSize
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The settings schema defining all segments and fields */
|
|
26
|
+
export let schema: SettingsSchema;
|
|
27
|
+
|
|
28
|
+
/** The settings store instance (create externally for access in parent) */
|
|
29
|
+
export let settings: ReturnType<typeof createSettingsStore>;
|
|
30
|
+
|
|
31
|
+
/** Optional: Callback when settings change */
|
|
32
|
+
export let onChange: ((values: any) => void) | undefined = undefined;
|
|
33
|
+
|
|
34
|
+
// Current values (hierarchical structure: { segment: { field: value } })
|
|
35
|
+
let currentValues: Record<string, Record<string, any>> = {};
|
|
36
|
+
|
|
37
|
+
// Track collapsed state of segments
|
|
38
|
+
let collapsedSegments = new Map<string, boolean>();
|
|
39
|
+
|
|
40
|
+
onMount(() => {
|
|
41
|
+
// Initialize collapsed states from schema
|
|
42
|
+
schema.segments.forEach((segment) => {
|
|
43
|
+
collapsedSegments.set(segment.id, segment.collapsed || false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Subscribe to store changes
|
|
47
|
+
const unsubscribe = settings.subscribe((values) => {
|
|
48
|
+
currentValues = values;
|
|
49
|
+
if (onChange) {
|
|
50
|
+
onChange(values);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return unsubscribe;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function toggleSegment(segmentId: string) {
|
|
58
|
+
const current = collapsedSegments.get(segmentId) || false;
|
|
59
|
+
collapsedSegments.set(segmentId, !current);
|
|
60
|
+
collapsedSegments = collapsedSegments; // Trigger reactivity
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resetSegmentHandler(segmentId: string) {
|
|
64
|
+
if (confirm(`Reset "${segmentId}" settings to defaults?`)) {
|
|
65
|
+
settings.resetSegment(segmentId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resetAll() {
|
|
70
|
+
if (confirm('Reset all settings to defaults?')) {
|
|
71
|
+
settings.resetAll();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if a field should be visible based on visibleIf condition
|
|
76
|
+
function isFieldVisible(field: any, segmentId: string): boolean {
|
|
77
|
+
if (!field.visibleIf) return true;
|
|
78
|
+
// Pass the entire hierarchical structure to visibleIf
|
|
79
|
+
return field.visibleIf(currentValues);
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class="settings-container">
|
|
84
|
+
<!-- Header -->
|
|
85
|
+
<div class="settings-header mb-4">
|
|
86
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
87
|
+
<div>
|
|
88
|
+
<h2 class="mb-1">Settings</h2>
|
|
89
|
+
{#if schema.version}
|
|
90
|
+
<small class="text-muted">Schema v{schema.version}</small>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
<button class="btn btn-outline-secondary btn-sm" on:click={resetAll}>
|
|
94
|
+
Reset All
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Settings Cards -->
|
|
100
|
+
<div class="settings-grid">
|
|
101
|
+
{#each schema.segments as segment (segment.id)}
|
|
102
|
+
<div class="card settings-card">
|
|
103
|
+
<!-- Card Header -->
|
|
104
|
+
<div class="card-header">
|
|
105
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
106
|
+
<div class="d-flex align-items-center gap-2">
|
|
107
|
+
{#if segment.icon}
|
|
108
|
+
<span class="segment-icon">{segment.icon}</span>
|
|
109
|
+
{/if}
|
|
110
|
+
<div>
|
|
111
|
+
<h5 class="mb-0">{segment.title}</h5>
|
|
112
|
+
{#if segment.description}
|
|
113
|
+
<small class="text-muted">{segment.description}</small>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="d-flex gap-2">
|
|
118
|
+
<button
|
|
119
|
+
class="btn btn-sm btn-link text-decoration-none p-0"
|
|
120
|
+
on:click={() => resetSegmentHandler(segment.id)}
|
|
121
|
+
title="Reset to defaults"
|
|
122
|
+
>
|
|
123
|
+
↺
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
class="btn btn-sm btn-link text-decoration-none p-0"
|
|
127
|
+
on:click={() => toggleSegment(segment.id)}
|
|
128
|
+
title={collapsedSegments.get(segment.id) ? 'Expand' : 'Collapse'}
|
|
129
|
+
>
|
|
130
|
+
{collapsedSegments.get(segment.id) ? '▶' : '▼'}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Card Body -->
|
|
137
|
+
{#if !collapsedSegments.get(segment.id)}
|
|
138
|
+
<div class="card-body">
|
|
139
|
+
{#each segment.fields as field (field.id)}
|
|
140
|
+
{#if isFieldVisible(field, segment.id)}
|
|
141
|
+
<FieldRenderer
|
|
142
|
+
{field}
|
|
143
|
+
segmentId={segment.id}
|
|
144
|
+
store={settings}
|
|
145
|
+
value={currentValues[segment.id]?.[field.id]}
|
|
146
|
+
/>
|
|
147
|
+
{/if}
|
|
148
|
+
{/each}
|
|
149
|
+
</div>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
152
|
+
{/each}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<style>
|
|
157
|
+
.settings-container {
|
|
158
|
+
max-width: 1200px;
|
|
159
|
+
margin: 0 auto;
|
|
160
|
+
padding: 1rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.settings-grid {
|
|
164
|
+
display: grid;
|
|
165
|
+
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
166
|
+
gap: 1.5rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.settings-card {
|
|
170
|
+
border: 1px solid var(--bs-border-color);
|
|
171
|
+
border-radius: 0.5rem;
|
|
172
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
173
|
+
transition: box-shadow 0.2s ease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.settings-card:hover {
|
|
177
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.card-header {
|
|
181
|
+
background-color: var(--bs-light);
|
|
182
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
183
|
+
padding: 1rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.segment-icon {
|
|
187
|
+
font-size: 1.5rem;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.card-body {
|
|
191
|
+
padding: 1.5rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@media (max-width: 768px) {
|
|
195
|
+
.settings-grid {
|
|
196
|
+
grid-template-columns: 1fr;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SettingsSchema } from './types';
|
|
2
|
+
import { createSettingsStore } from './store';
|
|
3
|
+
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> {
|
|
4
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
5
|
+
$$bindings?: Bindings;
|
|
6
|
+
} & Exports;
|
|
7
|
+
(internal: unknown, props: Props & {
|
|
8
|
+
$$events?: Events;
|
|
9
|
+
$$slots?: Slots;
|
|
10
|
+
}): Exports & {
|
|
11
|
+
$set?: any;
|
|
12
|
+
$on?: any;
|
|
13
|
+
};
|
|
14
|
+
z_$$bindings?: Bindings;
|
|
15
|
+
}
|
|
16
|
+
declare const Settings: $$__sveltets_2_IsomorphicComponent<{
|
|
17
|
+
/** The settings schema defining all segments and fields */ schema: SettingsSchema;
|
|
18
|
+
/** The settings store instance (create externally for access in parent) */ settings: ReturnType<typeof createSettingsStore>;
|
|
19
|
+
/** Optional: Callback when settings change */ onChange?: ((values: any) => void) | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
[evt: string]: CustomEvent<any>;
|
|
22
|
+
}, {}, {}, string>;
|
|
23
|
+
type Settings = InstanceType<typeof Settings>;
|
|
24
|
+
export default Settings;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings System - Public API
|
|
3
|
+
*
|
|
4
|
+
* A data-driven, reusable settings component for SvelteKit applications.
|
|
5
|
+
* Supports hierarchical access with TypeScript autocomplete.
|
|
6
|
+
*/
|
|
7
|
+
export { default as Settings } from './Settings.svelte';
|
|
8
|
+
export { createSettingsStore, createFieldMap, type InferSettingsType } from './store';
|
|
9
|
+
export type { SettingsSchema, SettingsStore, SettingsValues, SegmentDefinition, FieldDefinition, BaseFieldDefinition, BooleanFieldDefinition, TextFieldDefinition, NumberFieldDefinition, RangeFieldDefinition, ColorFieldDefinition, SelectFieldDefinition, RadioFieldDefinition, CustomFieldDefinition } from './types';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings System - Public API
|
|
3
|
+
*
|
|
4
|
+
* A data-driven, reusable settings component for SvelteKit applications.
|
|
5
|
+
* Supports hierarchical access with TypeScript autocomplete.
|
|
6
|
+
*/
|
|
7
|
+
export { default as Settings } from './Settings.svelte';
|
|
8
|
+
export { createSettingsStore, createFieldMap } from './store';
|