@sensolus/create-snt-agent-app 0.1.0 → 0.1.1
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/package.json +1 -1
- package/template/CLAUDE.md +218 -0
- package/template/Dockerfile +32 -0
- package/template/Jenkinsfile +28 -0
- package/template/README.md +477 -16
- package/template/_env.example +4 -0
- package/template/backend/app.py +630 -49
- package/template/backend/db_config.py +16 -0
- package/template/backend/extensions.py +3 -0
- package/template/backend/init_db.py +75 -0
- package/template/backend/migrations/README +1 -0
- package/template/backend/migrations/alembic.ini +50 -0
- package/template/backend/migrations/env.py +113 -0
- package/template/backend/migrations/script.py.mako +24 -0
- package/template/backend/migrations/versions/001_add_favourite_organisations.py +31 -0
- package/template/backend/migrations/versions/002_add_org_daily_stats.py +36 -0
- package/template/backend/models.py +31 -0
- package/template/backend/requirements.txt +8 -2
- package/template/eslint.config.js +6 -2
- package/template/index.html +11 -8
- package/template/infra/docker-compose.yml +15 -0
- package/template/openapi.json +40357 -0
- package/template/package.json +8 -1
- package/template/scripts/create-ecr-repo.sh +42 -0
- package/template/src/App.jsx +12 -34
- package/template/src/hooks/useFavourites.js +44 -0
- package/template/src/i18n/index.js +3 -0
- package/template/src/i18n/messages.js +8 -14
- package/template/src/i18n/translations/de.js +96 -0
- package/template/src/i18n/translations/en.js +103 -0
- package/template/src/i18n/translations/es.js +96 -0
- package/template/src/i18n/translations/fr.js +96 -0
- package/template/src/i18n/translations/nl.js +96 -0
- package/template/src/main.jsx +2 -3
- package/template/src/pages/Home.jsx +170 -0
- package/template/src/pages/OrganisationDetail.jsx +259 -0
- package/template/src/pages/OrganisationList.jsx +457 -0
- package/template/src/pages/Overview.jsx +199 -0
- package/template/src/pages/WidgetShowcase.jsx +522 -0
- package/template/src/styles/app.css +543 -4
- package/template/start-backend.sh +4 -0
- package/template/start-frontend.sh +3 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
SntBadge,
|
|
4
|
+
SntButton,
|
|
5
|
+
SntButtonGroup,
|
|
6
|
+
SntCard,
|
|
7
|
+
SntCheckboxList,
|
|
8
|
+
SntColors,
|
|
9
|
+
SntDateRangePicker,
|
|
10
|
+
SntDialog,
|
|
11
|
+
SntGrid,
|
|
12
|
+
SntGridItem,
|
|
13
|
+
SntHistogram,
|
|
14
|
+
SntInput,
|
|
15
|
+
SntLoadingOverlay,
|
|
16
|
+
SntMap,
|
|
17
|
+
SntProgressBar,
|
|
18
|
+
SntSelect,
|
|
19
|
+
SntSidepanel,
|
|
20
|
+
SntFilterSection,
|
|
21
|
+
SntSpinner,
|
|
22
|
+
SntSummaryStat,
|
|
23
|
+
SntSwitch,
|
|
24
|
+
SntTable,
|
|
25
|
+
SntTabs,
|
|
26
|
+
SntTabPanel,
|
|
27
|
+
SntToolbar,
|
|
28
|
+
SntToolbarSpacer,
|
|
29
|
+
getDefaultDateRange,
|
|
30
|
+
} from '@sensolus/snt-agent-kit'
|
|
31
|
+
|
|
32
|
+
function Section({ title, description, children }) {
|
|
33
|
+
return (
|
|
34
|
+
<SntCard title={title}>
|
|
35
|
+
{description && (
|
|
36
|
+
<p style={{ color: 'var(--snt-grey)', marginBottom: 16, fontSize: 14 }}>
|
|
37
|
+
{description}
|
|
38
|
+
</p>
|
|
39
|
+
)}
|
|
40
|
+
<div className="showcase-section-body">{children}</div>
|
|
41
|
+
</SntCard>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Example({ label, children }) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="showcase-example">
|
|
48
|
+
<div className="showcase-example-label">{label}</div>
|
|
49
|
+
<div className="showcase-example-body">{children}</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const BADGE_VARIANTS = [
|
|
55
|
+
'primary', 'secondary', 'success', 'warning', 'danger',
|
|
56
|
+
'info', 'light', 'dark', 'orange', 'salmon', 'purple', 'emerald',
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const BUTTON_VARIANTS = ['primary', 'secondary', 'success', 'danger', 'warning', 'info']
|
|
60
|
+
|
|
61
|
+
const TABLE_DATA = [
|
|
62
|
+
{ id: 1, name: 'Alpha Logistics', trackers: 142, status: 'ACTIVE' },
|
|
63
|
+
{ id: 2, name: 'Beta Transport', trackers: 56, status: 'ACTIVE' },
|
|
64
|
+
{ id: 3, name: 'Gamma Couriers', trackers: 8, status: 'INACTIVE' },
|
|
65
|
+
{ id: 4, name: 'Delta Freight', trackers: 231, status: 'ACTIVE' },
|
|
66
|
+
{ id: 5, name: 'Epsilon Shipping', trackers: 17, status: 'PENDING' },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const STATUS_VARIANT = {
|
|
70
|
+
ACTIVE: 'success',
|
|
71
|
+
INACTIVE: 'secondary',
|
|
72
|
+
PENDING: 'warning',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const HISTOGRAM_BUCKETS = [
|
|
76
|
+
{ start: 0, end: 10, count: 4 },
|
|
77
|
+
{ start: 10, end: 20, count: 12 },
|
|
78
|
+
{ start: 20, end: 30, count: 28 },
|
|
79
|
+
{ start: 30, end: 40, count: 19 },
|
|
80
|
+
{ start: 40, end: 50, count: 9 },
|
|
81
|
+
{ start: 50, end: 60, count: 3 },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const TAB_DEFS = [
|
|
85
|
+
{ key: 'one', label: 'Tab one' },
|
|
86
|
+
{ key: 'two', label: 'Tab two' },
|
|
87
|
+
{ key: 'three', label: 'Tab three' },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
export function WidgetShowcase() {
|
|
91
|
+
const [inputValue, setInputValue] = useState('Hello world')
|
|
92
|
+
const [selectValue, setSelectValue] = useState('eu')
|
|
93
|
+
const [groupValue, setGroupValue] = useState('cards')
|
|
94
|
+
const [switchOn, setSwitchOn] = useState(true)
|
|
95
|
+
const [checkboxSelected, setCheckboxSelected] = useState(['Trackers', 'Geozones'])
|
|
96
|
+
const [dateRange, setDateRange] = useState(() => getDefaultDateRange('last_3_months'))
|
|
97
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
98
|
+
const [showOverlay, setShowOverlay] = useState(false)
|
|
99
|
+
const [sidepanelOpen, setSidepanelOpen] = useState(true)
|
|
100
|
+
const [showcaseTab, setShowcaseTab] = useState('one')
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="page-container widget-showcase">
|
|
104
|
+
<SntCard>
|
|
105
|
+
<p style={{ margin: 0, color: 'var(--snt-grey)' }}>
|
|
106
|
+
Live reference of every Sensolus widget shipped with this app. Each card
|
|
107
|
+
shows the widget, what it's for, and a handful of common configurations.
|
|
108
|
+
Source lives in <code>src/widgets/</code>.
|
|
109
|
+
</p>
|
|
110
|
+
</SntCard>
|
|
111
|
+
|
|
112
|
+
{/* ------------------------------------------------------------------ */}
|
|
113
|
+
<Section
|
|
114
|
+
title="SntButton"
|
|
115
|
+
description="Primary action button. Use variant to express intent (primary, success, danger, ...)."
|
|
116
|
+
>
|
|
117
|
+
<Example label="Variants">
|
|
118
|
+
<div className="showcase-row">
|
|
119
|
+
{BUTTON_VARIANTS.map((v) => (
|
|
120
|
+
<SntButton key={v} variant={v}>{v}</SntButton>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</Example>
|
|
124
|
+
<Example label="Disabled">
|
|
125
|
+
<SntButton variant="primary" disabled>Disabled</SntButton>
|
|
126
|
+
</Example>
|
|
127
|
+
<Example label="With icon">
|
|
128
|
+
<SntButton variant="primary" icon={<span>+</span>}>Add item</SntButton>
|
|
129
|
+
</Example>
|
|
130
|
+
</Section>
|
|
131
|
+
|
|
132
|
+
{/* ------------------------------------------------------------------ */}
|
|
133
|
+
<Section
|
|
134
|
+
title="SntBadge"
|
|
135
|
+
description="Compact status / label pill. onChange receives a value, not an event."
|
|
136
|
+
>
|
|
137
|
+
<Example label="Variants">
|
|
138
|
+
<div className="showcase-row">
|
|
139
|
+
{BADGE_VARIANTS.map((v) => (
|
|
140
|
+
<SntBadge key={v} variant={v} text={v} />
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</Example>
|
|
144
|
+
<Example label="Compact">
|
|
145
|
+
<SntBadge variant="success" text="ACTIVE" compact />
|
|
146
|
+
</Example>
|
|
147
|
+
</Section>
|
|
148
|
+
|
|
149
|
+
{/* ------------------------------------------------------------------ */}
|
|
150
|
+
<Section
|
|
151
|
+
title="SntInput"
|
|
152
|
+
description="Text input. onChange receives the value directly."
|
|
153
|
+
>
|
|
154
|
+
<Example label="Text">
|
|
155
|
+
<SntInput value={inputValue} onChange={setInputValue} placeholder="Type something..." />
|
|
156
|
+
</Example>
|
|
157
|
+
<Example label="Password">
|
|
158
|
+
<SntInput type="password" value="hunter2" onChange={() => {}} />
|
|
159
|
+
</Example>
|
|
160
|
+
<Example label="Disabled / read-only">
|
|
161
|
+
<div className="showcase-row">
|
|
162
|
+
<SntInput value="disabled" onChange={() => {}} disabled />
|
|
163
|
+
<SntInput value="read-only" onChange={() => {}} readOnly />
|
|
164
|
+
</div>
|
|
165
|
+
</Example>
|
|
166
|
+
</Section>
|
|
167
|
+
|
|
168
|
+
{/* ------------------------------------------------------------------ */}
|
|
169
|
+
<Section
|
|
170
|
+
title="SntSelect"
|
|
171
|
+
description="Native dropdown. options = [{ value, label }]."
|
|
172
|
+
>
|
|
173
|
+
<Example label="Single select">
|
|
174
|
+
<SntSelect
|
|
175
|
+
value={selectValue}
|
|
176
|
+
onChange={setSelectValue}
|
|
177
|
+
options={[
|
|
178
|
+
{ value: 'eu', label: 'Europe' },
|
|
179
|
+
{ value: 'us', label: 'United States' },
|
|
180
|
+
{ value: 'apac', label: 'Asia-Pacific' },
|
|
181
|
+
]}
|
|
182
|
+
/>
|
|
183
|
+
</Example>
|
|
184
|
+
<Example label="With placeholder">
|
|
185
|
+
<SntSelect
|
|
186
|
+
value=""
|
|
187
|
+
onChange={() => {}}
|
|
188
|
+
placeholder="Pick a region..."
|
|
189
|
+
options={[
|
|
190
|
+
{ value: 'eu', label: 'Europe' },
|
|
191
|
+
{ value: 'us', label: 'United States' },
|
|
192
|
+
]}
|
|
193
|
+
/>
|
|
194
|
+
</Example>
|
|
195
|
+
</Section>
|
|
196
|
+
|
|
197
|
+
{/* ------------------------------------------------------------------ */}
|
|
198
|
+
<Section
|
|
199
|
+
title="SntButtonGroup"
|
|
200
|
+
description="Segmented control for exclusive choices. Great for view toggles."
|
|
201
|
+
>
|
|
202
|
+
<Example label="Toggle">
|
|
203
|
+
<SntButtonGroup
|
|
204
|
+
value={groupValue}
|
|
205
|
+
onChange={setGroupValue}
|
|
206
|
+
options={[
|
|
207
|
+
{ value: 'cards', label: 'Cards' },
|
|
208
|
+
{ value: 'table', label: 'Table' },
|
|
209
|
+
{ value: 'map', label: 'Map' },
|
|
210
|
+
]}
|
|
211
|
+
/>
|
|
212
|
+
</Example>
|
|
213
|
+
</Section>
|
|
214
|
+
|
|
215
|
+
{/* ------------------------------------------------------------------ */}
|
|
216
|
+
<Section
|
|
217
|
+
title="SntSwitch"
|
|
218
|
+
description="On/off toggle. checked state is controlled."
|
|
219
|
+
>
|
|
220
|
+
<Example label="With label">
|
|
221
|
+
<SntSwitch checked={switchOn} onChange={setSwitchOn} label="Show inactive devices" />
|
|
222
|
+
</Example>
|
|
223
|
+
<Example label="Disabled">
|
|
224
|
+
<SntSwitch checked={true} onChange={() => {}} label="Locked" disabled />
|
|
225
|
+
</Example>
|
|
226
|
+
</Section>
|
|
227
|
+
|
|
228
|
+
{/* ------------------------------------------------------------------ */}
|
|
229
|
+
<Section
|
|
230
|
+
title="SntCheckboxList"
|
|
231
|
+
description="Multi-select filter with select-all. options is a plain array of strings."
|
|
232
|
+
>
|
|
233
|
+
<Example label="Multi-select">
|
|
234
|
+
<div style={{ maxWidth: 320 }}>
|
|
235
|
+
<SntCheckboxList
|
|
236
|
+
label="Resources"
|
|
237
|
+
options={['Organisations', 'Trackers', 'Users', 'Geozones', 'Alerts']}
|
|
238
|
+
selected={checkboxSelected}
|
|
239
|
+
onChange={setCheckboxSelected}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</Example>
|
|
243
|
+
</Section>
|
|
244
|
+
|
|
245
|
+
{/* ------------------------------------------------------------------ */}
|
|
246
|
+
<Section
|
|
247
|
+
title="SntDateRangePicker"
|
|
248
|
+
description="Range selection with locale-aware presets. Receives ISO yyyy-MM-dd."
|
|
249
|
+
>
|
|
250
|
+
<Example label={`Current: ${dateRange.from} → ${dateRange.to}`}>
|
|
251
|
+
<SntDateRangePicker
|
|
252
|
+
from={dateRange.from}
|
|
253
|
+
to={dateRange.to}
|
|
254
|
+
onChange={setDateRange}
|
|
255
|
+
/>
|
|
256
|
+
</Example>
|
|
257
|
+
</Section>
|
|
258
|
+
|
|
259
|
+
{/* ------------------------------------------------------------------ */}
|
|
260
|
+
<Section
|
|
261
|
+
title="SntCard"
|
|
262
|
+
description="Surface for grouping content. Optional image, title, badge, title button."
|
|
263
|
+
>
|
|
264
|
+
<SntGrid minItemWidth={260}>
|
|
265
|
+
<SntGridItem>
|
|
266
|
+
<SntCard title="Simple card">
|
|
267
|
+
<p>Card body content.</p>
|
|
268
|
+
</SntCard>
|
|
269
|
+
</SntGridItem>
|
|
270
|
+
<SntGridItem>
|
|
271
|
+
<SntCard
|
|
272
|
+
title="With badge"
|
|
273
|
+
badge={{ text: 'NEW', variant: 'success' }}
|
|
274
|
+
>
|
|
275
|
+
<p>Cards can carry a status badge.</p>
|
|
276
|
+
</SntCard>
|
|
277
|
+
</SntGridItem>
|
|
278
|
+
<SntGridItem>
|
|
279
|
+
<SntCard
|
|
280
|
+
title="Clickable"
|
|
281
|
+
onClick={() => alert('Card clicked')}
|
|
282
|
+
titleButton={<SntButton variant="primary">Open</SntButton>}
|
|
283
|
+
>
|
|
284
|
+
<p>onClick makes the whole card interactive.</p>
|
|
285
|
+
</SntCard>
|
|
286
|
+
</SntGridItem>
|
|
287
|
+
</SntGrid>
|
|
288
|
+
</Section>
|
|
289
|
+
|
|
290
|
+
{/* ------------------------------------------------------------------ */}
|
|
291
|
+
<Section
|
|
292
|
+
title="SntGrid + SntGridItem"
|
|
293
|
+
description="Responsive equal-height grid. Items wrap based on minItemWidth."
|
|
294
|
+
>
|
|
295
|
+
<SntGrid minItemWidth={140} gap={12}>
|
|
296
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
297
|
+
<SntGridItem key={i}>
|
|
298
|
+
<div className="showcase-grid-cell">Item {i + 1}</div>
|
|
299
|
+
</SntGridItem>
|
|
300
|
+
))}
|
|
301
|
+
</SntGrid>
|
|
302
|
+
</Section>
|
|
303
|
+
|
|
304
|
+
{/* ------------------------------------------------------------------ */}
|
|
305
|
+
<Section
|
|
306
|
+
title="SntToolbar"
|
|
307
|
+
description="Horizontal row for grouping actions, with optional spacer."
|
|
308
|
+
>
|
|
309
|
+
<SntToolbar>
|
|
310
|
+
<SntButton variant="primary">Save</SntButton>
|
|
311
|
+
<SntButton>Cancel</SntButton>
|
|
312
|
+
<SntToolbarSpacer />
|
|
313
|
+
<div style={{ width: 220 }}>
|
|
314
|
+
<SntInput value="" onChange={() => {}} placeholder="Search..." />
|
|
315
|
+
</div>
|
|
316
|
+
<SntButton variant="info">Filter</SntButton>
|
|
317
|
+
</SntToolbar>
|
|
318
|
+
</Section>
|
|
319
|
+
|
|
320
|
+
{/* ------------------------------------------------------------------ */}
|
|
321
|
+
<Section
|
|
322
|
+
title="SntTabs + SntTabPanel"
|
|
323
|
+
description="Standard horizontal tab strip. Controlled via activeTab."
|
|
324
|
+
>
|
|
325
|
+
<SntTabs tabs={TAB_DEFS} activeTab={showcaseTab} onChange={setShowcaseTab}>
|
|
326
|
+
<SntTabPanel tabKey="one" activeTab={showcaseTab}>
|
|
327
|
+
<p style={{ padding: '16px 0' }}>Content for tab one.</p>
|
|
328
|
+
</SntTabPanel>
|
|
329
|
+
<SntTabPanel tabKey="two" activeTab={showcaseTab}>
|
|
330
|
+
<p style={{ padding: '16px 0' }}>Content for tab two.</p>
|
|
331
|
+
</SntTabPanel>
|
|
332
|
+
<SntTabPanel tabKey="three" activeTab={showcaseTab}>
|
|
333
|
+
<p style={{ padding: '16px 0' }}>Content for tab three.</p>
|
|
334
|
+
</SntTabPanel>
|
|
335
|
+
</SntTabs>
|
|
336
|
+
</Section>
|
|
337
|
+
|
|
338
|
+
{/* ------------------------------------------------------------------ */}
|
|
339
|
+
<Section
|
|
340
|
+
title="SntTable"
|
|
341
|
+
description="Sortable, paginated data table. Columns define key, header, optional render."
|
|
342
|
+
>
|
|
343
|
+
<SntTable
|
|
344
|
+
data={TABLE_DATA}
|
|
345
|
+
rowKey="id"
|
|
346
|
+
defaultPageSize={25}
|
|
347
|
+
columns={[
|
|
348
|
+
{ key: 'name', header: 'Name' },
|
|
349
|
+
{ key: 'trackers', header: 'Trackers' },
|
|
350
|
+
{
|
|
351
|
+
key: 'status',
|
|
352
|
+
header: 'Status',
|
|
353
|
+
render: (row, val) => (
|
|
354
|
+
<SntBadge variant={STATUS_VARIANT[val] || 'secondary'} text={val} />
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
]}
|
|
358
|
+
/>
|
|
359
|
+
</Section>
|
|
360
|
+
|
|
361
|
+
{/* ------------------------------------------------------------------ */}
|
|
362
|
+
<Section
|
|
363
|
+
title="SntSummaryStat"
|
|
364
|
+
description="Big-number stat tile. variant adds a coloured value."
|
|
365
|
+
>
|
|
366
|
+
<div className="summary-stats-row">
|
|
367
|
+
<SntSummaryStat value="142" label="Trackers" variant="info" />
|
|
368
|
+
<SntSummaryStat value="36" label="Users" variant="success" />
|
|
369
|
+
<SntSummaryStat value="3" label="Alerts" variant="warning" />
|
|
370
|
+
<SntSummaryStat value="1" label="Outages" variant="danger" />
|
|
371
|
+
</div>
|
|
372
|
+
</Section>
|
|
373
|
+
|
|
374
|
+
{/* ------------------------------------------------------------------ */}
|
|
375
|
+
<Section
|
|
376
|
+
title="SntProgressBar"
|
|
377
|
+
description="Inline percentage bar. variant=auto colours by value."
|
|
378
|
+
>
|
|
379
|
+
<Example label="Variants (25 / 60 / 90)">
|
|
380
|
+
<div className="showcase-stack">
|
|
381
|
+
<SntProgressBar value={25} variant="danger" />
|
|
382
|
+
<SntProgressBar value={60} variant="warning" />
|
|
383
|
+
<SntProgressBar value={90} variant="success" />
|
|
384
|
+
</div>
|
|
385
|
+
</Example>
|
|
386
|
+
<Example label="Auto colour by value">
|
|
387
|
+
<div className="showcase-stack">
|
|
388
|
+
<SntProgressBar value={20} variant="auto" />
|
|
389
|
+
<SntProgressBar value={55} variant="auto" />
|
|
390
|
+
<SntProgressBar value={85} variant="auto" />
|
|
391
|
+
</div>
|
|
392
|
+
</Example>
|
|
393
|
+
</Section>
|
|
394
|
+
|
|
395
|
+
{/* ------------------------------------------------------------------ */}
|
|
396
|
+
<Section
|
|
397
|
+
title="SntHistogram"
|
|
398
|
+
description="Microchart showing a distribution. Pass an array of {start, end, count}."
|
|
399
|
+
>
|
|
400
|
+
<SntHistogram buckets={HISTOGRAM_BUCKETS} height={80} />
|
|
401
|
+
</Section>
|
|
402
|
+
|
|
403
|
+
{/* ------------------------------------------------------------------ */}
|
|
404
|
+
<Section
|
|
405
|
+
title="SntSpinner + SntLoadingOverlay"
|
|
406
|
+
description="Indeterminate loading indicators."
|
|
407
|
+
>
|
|
408
|
+
<Example label="Spinner sizes">
|
|
409
|
+
<div className="showcase-row" style={{ alignItems: 'center' }}>
|
|
410
|
+
<SntSpinner size="small" />
|
|
411
|
+
<SntSpinner size="medium" />
|
|
412
|
+
<SntSpinner size="large" />
|
|
413
|
+
</div>
|
|
414
|
+
</Example>
|
|
415
|
+
<Example label="Loading overlay (toggle)">
|
|
416
|
+
<SntButton variant="primary" onClick={() => {
|
|
417
|
+
setShowOverlay(true)
|
|
418
|
+
setTimeout(() => setShowOverlay(false), 1500)
|
|
419
|
+
}}>
|
|
420
|
+
Show overlay for 1.5s
|
|
421
|
+
</SntButton>
|
|
422
|
+
{showOverlay && (
|
|
423
|
+
<div style={{ position: 'relative', height: 120, marginTop: 12 }}>
|
|
424
|
+
<SntLoadingOverlay message="Loading sample data..." />
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
</Example>
|
|
428
|
+
</Section>
|
|
429
|
+
|
|
430
|
+
{/* ------------------------------------------------------------------ */}
|
|
431
|
+
<Section
|
|
432
|
+
title="SntDialog"
|
|
433
|
+
description="Modal dialog. open + onClose are controlled by the caller."
|
|
434
|
+
>
|
|
435
|
+
<SntButton variant="primary" onClick={() => setDialogOpen(true)}>
|
|
436
|
+
Open dialog
|
|
437
|
+
</SntButton>
|
|
438
|
+
<SntDialog
|
|
439
|
+
open={dialogOpen}
|
|
440
|
+
onClose={() => setDialogOpen(false)}
|
|
441
|
+
title="Example dialog"
|
|
442
|
+
size="small"
|
|
443
|
+
>
|
|
444
|
+
<p>This is a small modal dialog. Click the backdrop or × to close.</p>
|
|
445
|
+
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
446
|
+
<SntButton onClick={() => setDialogOpen(false)}>Cancel</SntButton>
|
|
447
|
+
<SntButton variant="primary" onClick={() => setDialogOpen(false)}>OK</SntButton>
|
|
448
|
+
</div>
|
|
449
|
+
</SntDialog>
|
|
450
|
+
</Section>
|
|
451
|
+
|
|
452
|
+
{/* ------------------------------------------------------------------ */}
|
|
453
|
+
<Section
|
|
454
|
+
title="SntSidepanel + SntFilterSection"
|
|
455
|
+
description="Collapsible filter rail used alongside list views."
|
|
456
|
+
>
|
|
457
|
+
<div className="showcase-sidepanel-demo">
|
|
458
|
+
<SntSidepanel
|
|
459
|
+
title="Filters"
|
|
460
|
+
open={sidepanelOpen}
|
|
461
|
+
onToggle={() => setSidepanelOpen((v) => !v)}
|
|
462
|
+
width={240}
|
|
463
|
+
>
|
|
464
|
+
<SntFilterSection label="Status">
|
|
465
|
+
<SntSwitch checked onChange={() => {}} label="Active only" />
|
|
466
|
+
</SntFilterSection>
|
|
467
|
+
<SntFilterSection label="Region">
|
|
468
|
+
<SntSelect
|
|
469
|
+
value="eu"
|
|
470
|
+
onChange={() => {}}
|
|
471
|
+
options={[
|
|
472
|
+
{ value: 'eu', label: 'Europe' },
|
|
473
|
+
{ value: 'us', label: 'United States' },
|
|
474
|
+
]}
|
|
475
|
+
/>
|
|
476
|
+
</SntFilterSection>
|
|
477
|
+
</SntSidepanel>
|
|
478
|
+
<div className="showcase-sidepanel-content">
|
|
479
|
+
<p>Sidepanel content area. Toggle the chevron to collapse.</p>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</Section>
|
|
483
|
+
|
|
484
|
+
{/* ------------------------------------------------------------------ */}
|
|
485
|
+
<Section
|
|
486
|
+
title="SntMap"
|
|
487
|
+
description="Leaflet map with Street/Satellite layer toggle. Optionally renders geozones (by orgId or array) and device markers."
|
|
488
|
+
>
|
|
489
|
+
<SntMap
|
|
490
|
+
height="320px"
|
|
491
|
+
center={[50.85, 4.35]}
|
|
492
|
+
zoom={6}
|
|
493
|
+
showGeozones={false}
|
|
494
|
+
showGeozoneSelector={false}
|
|
495
|
+
devices={[
|
|
496
|
+
{ id: 'd1', name: 'Demo tracker A', lastLat: 50.8503, lastLng: 4.3517 },
|
|
497
|
+
{ id: 'd2', name: 'Demo tracker B', lastLat: 51.2194, lastLng: 4.4025 },
|
|
498
|
+
{ id: 'd3', name: 'Demo tracker C', lastLat: 51.0543, lastLng: 3.7174 },
|
|
499
|
+
]}
|
|
500
|
+
/>
|
|
501
|
+
</Section>
|
|
502
|
+
|
|
503
|
+
{/* ------------------------------------------------------------------ */}
|
|
504
|
+
<Section
|
|
505
|
+
title="SntColors"
|
|
506
|
+
description="JavaScript colour constants matching the CSS variables."
|
|
507
|
+
>
|
|
508
|
+
<div className="showcase-color-grid">
|
|
509
|
+
{Object.entries(SntColors).map(([name, value]) => (
|
|
510
|
+
<div key={name} className="showcase-color-swatch">
|
|
511
|
+
<div className="showcase-color-chip" style={{ background: value }} />
|
|
512
|
+
<div className="showcase-color-meta">
|
|
513
|
+
<div className="showcase-color-name">{name}</div>
|
|
514
|
+
<div className="showcase-color-value">{value}</div>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
))}
|
|
518
|
+
</div>
|
|
519
|
+
</Section>
|
|
520
|
+
</div>
|
|
521
|
+
)
|
|
522
|
+
}
|