@sensolus/create-snt-agent-app 0.3.9 → 0.3.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sensolus/create-snt-agent-app",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Scaffold a new Sensolus agent app: React frontend wired to @sensolus/snt-agent-kit + Flask API proxy backend.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -89,10 +89,13 @@ import { SntButton, SntInput, SntBadge, SntTable } from '@sensolus/snt-agent-kit
89
89
 
90
90
  | Component | Description |
91
91
  |-----------|-------------|
92
- | `SntButton` | Primary button with variants (primary, secondary, success, danger, warning, info) |
92
+ | `SntButton` | Button. Default (white, `secondary`) is the everyday primary action; use `emphasis` (dark blue) sparingly for the single most important action. Other variants: success, danger, warning, info. (`primary` is a deprecated alias for `emphasis`.) The first letter of the label is auto-capitalised. |
93
+ | `SntIcon` | Named SVG icon (`export`, `download`, `edit`, `settings`, `pdf`, `report`, …). Inherits text color via `currentColor`; `size` to scale. |
94
+ | `SntLink` | Brand-blue text link (underline on hover). `href` → `<a>`; `onClick` only → link-styled `<button>` for tertiary actions (e.g. a dialog's "Learn more"). |
93
95
  | `SntInput` | Text input (onChange receives value directly, not event) |
94
96
  | `SntBadge` | Status badge with color variants |
95
97
  | `SntCard` | Card container with optional image, title, badge |
98
+ | `SntSection` | Titled page section (title + optional description + body) for grouping content. Use for page regions; `SntCard` is for content tiles. |
96
99
  | `SntTable` | Sortable, paginated data table |
97
100
  | `SntSpinner` | Loading spinner (sizes: small, medium, large) |
98
101
  | `SntLoadingOverlay` | Centered spinner with optional message |
@@ -119,14 +122,17 @@ import { SntButton, SntInput, SntBadge, SntTable } from '@sensolus/snt-agent-kit
119
122
  --snt-green: #39CB99; /* Success */
120
123
  --snt-yellow: #FFCC66; /* Warning */
121
124
  --snt-red: #E00000; /* Danger */
122
- --snt-infra: #00A6ED; /* Info */
125
+ --snt-ui-selected: #00A6ED; /* Info */
123
126
  ```
124
127
 
125
128
  ### Widget Examples
126
129
 
127
130
  ```jsx
128
- // Button
129
- <SntButton variant="primary" onClick={handleClick}>Save</SntButton>
131
+ // Button — the white default is the everyday primary action
132
+ <SntButton onClick={handleClick}>Save</SntButton>
133
+
134
+ // Use emphasis (dark blue) only for the single most important action on a screen
135
+ <SntButton variant="emphasis" onClick={handleConfirm}>Confirm</SntButton>
130
136
 
131
137
  // Input
132
138
  <SntInput value={query} onChange={setQuery} placeholder="Search..." />
@@ -163,6 +169,21 @@ import { SntButton, SntInput, SntBadge, SntTable } from '@sensolus/snt-agent-kit
163
169
  2. **Import widgets** from `@sensolus/snt-agent-kit` - they're modular ES modules
164
170
  3. **Follow existing patterns** in `src/App.jsx` for examples
165
171
  4. **Reference the baseline** at `/sensolus/work/baseline/` for additional components not yet ported
172
+ 5. **Put the matching `SntIcon` in the primary button** whenever a mockup has an Export, Download, Edit, Settings, PDF, or Report action — e.g. `<SntButton icon={<SntIcon name="export" />}>Export</SntButton>`.
173
+ 6. **Button order in forms:** put the blue (`variant="emphasis"`) action **first** and the white (default) action **second** (e.g. Save before Cancel). This is a deliberate exception to the white-is-primary default and applies only to forms.
174
+ 7. **Overlay / dialog footer order (always):** blue (`variant="emphasis"`) first → white (default) second → link (`SntLink`) third, in that sequence — e.g. Delete, Cancel, Learn more. The tertiary action is always an `SntLink`, never a button.
175
+
176
+ ### Page layout rules
177
+
178
+ These govern how widgets are composed on a page. Follow them when building or editing pages.
179
+
180
+ 1. **Page wrapper:** render page content inside `.page-root` — it carries the standard padding (24px top, 16px sides/bottom). Don't add your own outer padding.
181
+ 2. **Page header:** start every page with `SntPageHeader` for the title (and back button / actions). Don't hand-roll an `h1` + back chevron.
182
+ 3. **`SntSummaryStat` is never wrapped in a panel.** Place stat tiles directly in a `.summary-stats-row` on the page background — never inside an `SntCard`/panel. The row gaps give them enough white space to live on the page on their own. Non-clickable stats are flat white tiles (no border); clickable ones carry a grey fill + border, which is distinctive enough on its own — they don't need an enclosing card to set them apart. Reserve `SntCard` for content tiles further down.
183
+ 4. **Group sections with `SntSection`, content tiles with `SntCard`.** Use `SntSection` for titled page regions (a heading + body); use `SntCard` for individual content tiles within a region. Don't nest a section inside a card.
184
+ 5. **Avoid pie charts — use bar charts.** For part-to-whole or category comparisons, render a bar chart, not a pie/donut. Bars are easier to read and compare than angles/slices. Reserve pies for nothing by default.
185
+ 6. **Every chart axis has a label.** Both the x- and y-axis must carry a name (with units where relevant, e.g. "Distance (km)"). Never ship a chart with unlabelled axes.
186
+ 7. **Cards are auto-height; siblings in a row match the tallest.** A card sizes to its own content by default — don't set fixed heights. When cards sit side by side (e.g. an `SntGrid` row), they stretch to the height of the tallest card in that row so their tops and bottoms align (use `align-items: stretch`, the default for flex/grid rows). Don't hand-tune per-card heights to fake alignment.
166
187
 
167
188
  ## Internationalization (i18n)
168
189
 
@@ -462,7 +462,7 @@ Use CSS custom properties for consistent styling:
462
462
  --snt-green: #39CB99; /* Success */
463
463
  --snt-yellow: #FFCC66; /* Warning */
464
464
  --snt-red: #E00000; /* Danger */
465
- --snt-infra: #00A6ED; /* Info */
465
+ --snt-ui-selected: #00A6ED; /* Info */
466
466
 
467
467
  /* Semantic Aliases (preferred) */
468
468
  --snt-color-primary: var(--snt-blue-darkest);
@@ -11,6 +11,8 @@ export default {
11
11
  'overview.series.orgs': 'Organisationen',
12
12
  'overview.series.trackers': 'Tracker',
13
13
  'overview.series.users': 'Benutzer',
14
+ 'overview.table.title': 'Verlauf der täglichen Momentaufnahmen',
15
+ 'overview.table.date': 'Datum',
14
16
 
15
17
  // Common
16
18
 
@@ -11,6 +11,8 @@ export default {
11
11
  'overview.series.orgs': 'Organisations',
12
12
  'overview.series.trackers': 'Trackers',
13
13
  'overview.series.users': 'Users',
14
+ 'overview.table.title': 'Daily snapshot history',
15
+ 'overview.table.date': 'Date',
14
16
 
15
17
  // Common
16
18
 
@@ -11,6 +11,8 @@ export default {
11
11
  'overview.series.orgs': 'Organizaciones',
12
12
  'overview.series.trackers': 'Rastreadores',
13
13
  'overview.series.users': 'Usuarios',
14
+ 'overview.table.title': 'Historial de instantáneas diarias',
15
+ 'overview.table.date': 'Fecha',
14
16
 
15
17
  // Common
16
18
 
@@ -11,6 +11,8 @@ export default {
11
11
  'overview.series.orgs': 'Organisations',
12
12
  'overview.series.trackers': 'Traceurs',
13
13
  'overview.series.users': 'Utilisateurs',
14
+ 'overview.table.title': 'Historique des instantanés quotidiens',
15
+ 'overview.table.date': 'Date',
14
16
 
15
17
  // Common
16
18
 
@@ -11,6 +11,8 @@ export default {
11
11
  'overview.series.orgs': 'Organisaties',
12
12
  'overview.series.trackers': 'Trackers',
13
13
  'overview.series.users': 'Gebruikers',
14
+ 'overview.table.title': 'Geschiedenis dagelijkse momentopname',
15
+ 'overview.table.date': 'Datum',
14
16
 
15
17
  // Common
16
18
 
@@ -87,7 +87,6 @@ export function Home() {
87
87
  )}
88
88
  {activeTab === 'explore' && (
89
89
  <SntButton
90
- variant="primary"
91
90
  onClick={() => orgListReloadRef.current?.()}
92
91
  disabled={orgListLoading || !authReady}
93
92
  >
@@ -147,6 +146,15 @@ export function Home() {
147
146
  gap: 8,
148
147
  }}
149
148
  >
149
+ {/* In forms the blue (emphasis) action comes first, white second —
150
+ an intentional exception to the white-is-primary guideline. */}
151
+ <SntButton
152
+ variant="emphasis"
153
+ onClick={submitApiKey}
154
+ disabled={submitting || !apiKeyInput.trim()}
155
+ >
156
+ {submitting ? t('common.loading') : t('common.save')}
157
+ </SntButton>
150
158
  {authReady && (
151
159
  <SntButton
152
160
  variant="secondary"
@@ -156,13 +164,6 @@ export function Home() {
156
164
  {t('common.cancel')}
157
165
  </SntButton>
158
166
  )}
159
- <SntButton
160
- variant="primary"
161
- onClick={submitApiKey}
162
- disabled={submitting || !apiKeyInput.trim()}
163
- >
164
- {submitting ? t('common.loading') : t('common.save')}
165
- </SntButton>
166
167
  </div>
167
168
  </SntDialog>
168
169
  </div>
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { SntCard, SntLoadingOverlay, SntSummaryStat } from '@sensolus/snt-agent-kit'
2
+ import { SntCard, SntLoadingOverlay, SntSummaryStat, SntTable } from '@sensolus/snt-agent-kit'
3
3
  import { useLocale, formatNumber, formatShortDate } from '../i18n'
4
4
 
5
5
  const SERIES = [
@@ -192,6 +192,35 @@ export function Overview({ authReady }) {
192
192
  </div>
193
193
  <LineChart data={data} intlLocale={intlLocale} timezone={timezone} t={t} />
194
194
  </SntCard>
195
+
196
+ <SntCard title={t('overview.table.title')}>
197
+ <SntTable
198
+ data={[...data].reverse()}
199
+ rowKey="date"
200
+ columns={[
201
+ {
202
+ key: 'date',
203
+ header: t('overview.table.date'),
204
+ render: (row) => formatShortDate(row.date, intlLocale, timezone),
205
+ },
206
+ {
207
+ key: 'orgCount',
208
+ header: t('overview.series.orgs'),
209
+ render: (row) => formatNumber(row.orgCount, intlLocale),
210
+ },
211
+ {
212
+ key: 'trackerTotal',
213
+ header: t('overview.series.trackers'),
214
+ render: (row) => formatNumber(row.trackerTotal, intlLocale),
215
+ },
216
+ {
217
+ key: 'userTotal',
218
+ header: t('overview.series.users'),
219
+ render: (row) => formatNumber(row.userTotal, intlLocale),
220
+ },
221
+ ]}
222
+ />
223
+ </SntCard>
195
224
  </>
196
225
  )}
197
226
  </div>
@@ -6,6 +6,8 @@ import {
6
6
  SntCard,
7
7
  SntCheckboxList,
8
8
  SntColors,
9
+ SntIcon,
10
+ SNT_ICON_NAMES,
9
11
  SntDateRangePicker,
10
12
  SntDialog,
11
13
  SntGrid,
@@ -58,20 +60,20 @@ const BADGE_VARIANTS = [
58
60
  'info', 'light', 'dark', 'orange', 'salmon', 'purple', 'emerald',
59
61
  ]
60
62
 
61
- const BUTTON_VARIANTS = ['primary', 'secondary', 'success', 'danger', 'warning', 'info']
63
+ const BUTTON_VARIANTS = ['secondary', 'emphasis', 'success', 'danger', 'warning', 'info']
62
64
 
63
65
  const TABLE_DATA = [
64
- { id: 1, name: 'Alpha Logistics', trackers: 142, status: 'ACTIVE' },
65
- { id: 2, name: 'Beta Transport', trackers: 56, status: 'ACTIVE' },
66
- { id: 3, name: 'Gamma Couriers', trackers: 8, status: 'INACTIVE' },
67
- { id: 4, name: 'Delta Freight', trackers: 231, status: 'ACTIVE' },
68
- { id: 5, name: 'Epsilon Shipping', trackers: 17, status: 'PENDING' },
66
+ { id: 1, name: 'Alpha Logistics', trackers: 142, status: 'Active' },
67
+ { id: 2, name: 'Beta Transport', trackers: 56, status: 'Active' },
68
+ { id: 3, name: 'Gamma Couriers', trackers: 8, status: 'Inactive' },
69
+ { id: 4, name: 'Delta Freight', trackers: 231, status: 'Active' },
70
+ { id: 5, name: 'Epsilon Shipping', trackers: 17, status: 'Pending' },
69
71
  ]
70
72
 
71
73
  const STATUS_VARIANT = {
72
- ACTIVE: 'success',
73
- INACTIVE: 'secondary',
74
- PENDING: 'warning',
74
+ Active: 'success',
75
+ Inactive: 'secondary',
76
+ Pending: 'warning',
75
77
  }
76
78
 
77
79
  const HISTOGRAM_BUCKETS = [
@@ -95,6 +97,7 @@ export function WidgetShowcase() {
95
97
  const [selectValue, setSelectValue] = useState('eu')
96
98
  const [groupValue, setGroupValue] = useState('cards')
97
99
  const [switchOn, setSwitchOn] = useState(true)
100
+ const [activeOnly, setActiveOnly] = useState(true)
98
101
  const [checkboxSelected, setCheckboxSelected] = useState(['Trackers', 'Geozones'])
99
102
  const [dateRange, setDateRange] = useState(() => getDefaultDateRange('week'))
100
103
  const [dialogOpen, setDialogOpen] = useState(false)
@@ -115,20 +118,50 @@ export function WidgetShowcase() {
115
118
  {/* ------------------------------------------------------------------ */}
116
119
  <Section
117
120
  title="SntButton"
118
- description="Primary action button. Use variant to express intent (primary, success, danger, ...)."
121
+ description="The default (white) button is the everyday primary action — reach for it almost always. Use 'emphasis' (dark blue) sparingly for the single most important action on a screen."
119
122
  >
120
123
  <Example label="Variants">
121
124
  <div className="showcase-row">
122
125
  {BUTTON_VARIANTS.map((v) => (
123
- <SntButton key={v} variant={v}>{v}</SntButton>
126
+ <SntButton key={v} variant={v}>{v === 'secondary' ? 'Primary' : v}</SntButton>
124
127
  ))}
125
128
  </div>
126
129
  </Example>
127
130
  <Example label="Disabled">
128
- <SntButton variant="primary" disabled>Disabled</SntButton>
131
+ <SntButton disabled>Disabled</SntButton>
129
132
  </Example>
130
133
  <Example label="With icon">
131
- <SntButton variant="primary" icon={<span>+</span>}>Add item</SntButton>
134
+ <SntButton icon={<SntIcon name="export" />}>Export</SntButton>
135
+ </Example>
136
+ </Section>
137
+
138
+ {/* ------------------------------------------------------------------ */}
139
+ <Section
140
+ title="SntIcon"
141
+ description="Named SVG icons from the Sensolus set. Icons inherit the surrounding text color (currentColor), so they adapt to button variants, links, etc. Set size to scale. For action buttons (export, download, settings, PDF, report) put the matching icon in the button."
142
+ >
143
+ <Example label="All icons">
144
+ <div className="showcase-row" style={{ alignItems: 'center', gap: 20 }}>
145
+ {SNT_ICON_NAMES.map((name) => (
146
+ <div
147
+ key={name}
148
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}
149
+ >
150
+ <SntIcon name={name} size={24} title={name} />
151
+ <code style={{ fontSize: 12, color: 'var(--snt-grey)' }}>{name}</code>
152
+ </div>
153
+ ))}
154
+ </div>
155
+ </Example>
156
+ <Example label="In buttons">
157
+ <div className="showcase-row">
158
+ <SntButton icon={<SntIcon name="export" />}>Export</SntButton>
159
+ <SntButton icon={<SntIcon name="download" />}>Download</SntButton>
160
+ <SntButton icon={<SntIcon name="edit" />}>Edit</SntButton>
161
+ <SntButton icon={<SntIcon name="report" />}>Report</SntButton>
162
+ <SntButton icon={<SntIcon name="pdf" />}>PDF</SntButton>
163
+ <SntButton icon={<SntIcon name="settings" />}>Settings</SntButton>
164
+ </div>
132
165
  </Example>
133
166
  </Section>
134
167
 
@@ -145,7 +178,7 @@ export function WidgetShowcase() {
145
178
  </div>
146
179
  </Example>
147
180
  <Example label="Compact">
148
- <SntBadge variant="success" text="ACTIVE" compact />
181
+ <SntBadge variant="success" text="Active" compact />
149
182
  </Example>
150
183
  </Section>
151
184
 
@@ -207,9 +240,9 @@ export function WidgetShowcase() {
207
240
  value={groupValue}
208
241
  onChange={setGroupValue}
209
242
  options={[
210
- { value: 'cards', label: 'Cards' },
211
- { value: 'table', label: 'Table' },
212
- { value: 'map', label: 'Map' },
243
+ { value: 'cards', label: 'Cards', icon: <SntIcon name="card" /> },
244
+ { value: 'list', label: 'List', icon: <SntIcon name="list" /> },
245
+ { value: 'map', label: 'Map', icon: <SntIcon name="map" /> },
213
246
  ]}
214
247
  />
215
248
  </Example>
@@ -278,7 +311,7 @@ export function WidgetShowcase() {
278
311
  <SntCard
279
312
  title="Clickable"
280
313
  onClick={() => alert('Card clicked')}
281
- titleButton={<SntButton variant="primary">Open</SntButton>}
314
+ titleButton={<SntButton>Open</SntButton>}
282
315
  >
283
316
  <p>onClick makes the whole card interactive.</p>
284
317
  </SntCard>
@@ -306,13 +339,13 @@ export function WidgetShowcase() {
306
339
  description="Horizontal row for grouping actions, with optional spacer."
307
340
  >
308
341
  <SntToolbar>
309
- <SntButton variant="primary">Save</SntButton>
342
+ <SntButton variant="emphasis">Save</SntButton>
310
343
  <SntButton>Cancel</SntButton>
311
344
  <SntToolbarSpacer />
312
345
  <div style={{ width: 220 }}>
313
346
  <SntInput value="" onChange={() => {}} placeholder="Search..." />
314
347
  </div>
315
- <SntButton variant="info">Filter</SntButton>
348
+ <SntButton icon={<SntIcon name="filter" />}>Filter</SntButton>
316
349
  </SntToolbar>
317
350
  </Section>
318
351
 
@@ -412,7 +445,7 @@ export function WidgetShowcase() {
412
445
  </div>
413
446
  </Example>
414
447
  <Example label="Loading overlay (toggle)">
415
- <SntButton variant="primary" onClick={() => {
448
+ <SntButton onClick={() => {
416
449
  setShowOverlay(true)
417
450
  setTimeout(() => setShowOverlay(false), 1500)
418
451
  }}>
@@ -431,7 +464,7 @@ export function WidgetShowcase() {
431
464
  title="SntDialog"
432
465
  description="Modal dialog. open + onClose are controlled by the caller."
433
466
  >
434
- <SntButton variant="primary" onClick={() => setDialogOpen(true)}>
467
+ <SntButton onClick={() => setDialogOpen(true)}>
435
468
  Open dialog
436
469
  </SntButton>
437
470
  <SntDialog
@@ -442,8 +475,8 @@ export function WidgetShowcase() {
442
475
  >
443
476
  <p>This is a small modal dialog. Click the backdrop or × to close.</p>
444
477
  <div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
478
+ <SntButton variant="emphasis" onClick={() => setDialogOpen(false)}>OK</SntButton>
445
479
  <SntButton onClick={() => setDialogOpen(false)}>Cancel</SntButton>
446
- <SntButton variant="primary" onClick={() => setDialogOpen(false)}>OK</SntButton>
447
480
  </div>
448
481
  </SntDialog>
449
482
  </Section>
@@ -461,7 +494,7 @@ export function WidgetShowcase() {
461
494
  width={240}
462
495
  >
463
496
  <SntFilterSection label="Status">
464
- <SntSwitch checked onChange={() => {}} label="Active only" />
497
+ <SntSwitch checked={activeOnly} onChange={setActiveOnly} label="Active only" />
465
498
  </SntFilterSection>
466
499
  <SntFilterSection label="Region">
467
500
  <SntSelect