@bardioc/create-bardioc-app 0.7.0 → 0.7.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.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/templates/_base/.changeset/README.md +4 -6
  3. package/templates/_base/.claude/commands/changeset-app.md +11 -12
  4. package/templates/_base/.claude/commands/refresh-bundle.md +1 -1
  5. package/templates/_base/AGENTS.md +128 -0
  6. package/templates/_base/CLAUDE.md +1 -0
  7. package/templates/_base/README.md +11 -17
  8. package/templates/_opt-link/scripts/link-source.mjs +1 -1
  9. package/templates/_opt-pipeline/bitbucket-pipelines.yml +12 -56
  10. package/templates/angular/package.json +1 -1
  11. package/templates/nextjs/package.json +1 -1
  12. package/templates/preact/package.json +1 -1
  13. package/templates/solid/package.json +1 -1
  14. package/templates/svelte/package.json +1 -1
  15. package/templates/vite/CLAUDE.md +149 -135
  16. package/templates/vite/package.json +3 -3
  17. package/templates/vite/src/App.tsx +6 -3
  18. package/templates/vite/src/auth/onboarding.tsx +19 -23
  19. package/templates/vite/src/climate/climate-data.ts +19 -0
  20. package/templates/vite/src/climate/conditions-chart.tsx +11 -6
  21. package/templates/vite/src/climate/location-card.tsx +2 -5
  22. package/templates/vite/src/climate/location-select.tsx +7 -2
  23. package/templates/vite/src/climate/metrics-grid.tsx +6 -1
  24. package/templates/vite/src/climate/overview-view.tsx +26 -7
  25. package/templates/vite/src/climate/stations-view.tsx +73 -9
  26. package/templates/vite/src/climate/types.ts +5 -5
  27. package/templates/vite/src/dashboard-provider.tsx +15 -5
  28. package/templates/vite/src/events/live-events-view.tsx +67 -37
  29. package/templates/vite/src/events/mock-events.ts +33 -0
  30. package/templates/vite/src/graph/explorer-view.tsx +81 -63
  31. package/templates/vite/src/i18n.tsx +6 -2
  32. package/templates/vite/src/index.css +0 -6
  33. package/templates/vite/src/locales/de.json +55 -51
  34. package/templates/vite/src/locales/en.json +44 -40
  35. package/templates/vite/src/preview-state.tsx +29 -0
  36. package/templates/vite/src/profile/profile-view.tsx +43 -25
  37. package/templates/vite/src/shell/about-dialog.tsx +6 -5
  38. package/templates/vite/src/shell/app-shell.tsx +46 -45
  39. package/templates/vite/src/shell/debug-menu.tsx +86 -0
  40. package/templates/vite/src/shell/header.tsx +6 -4
  41. package/templates/vite/src/shell/view-content.tsx +69 -0
  42. package/templates/vue/package.json +1 -1
@@ -4,20 +4,32 @@ Bardioc hosted app. Vite + React 19 + TypeScript 6 + `@bardioc/app-sdk`, with
4
4
  `@bardioc/ui` (components + theme) and `@bardioc/i18n-core` (translations).
5
5
 
6
6
  The starter ships a **Climate Dashboard** — a worked example of how a real Bardioc app
7
- is wired: a sidebar shell with multiple views, `@bardioc/ui` components (cards, metrics,
8
- charts, a searchable/filterable `DataTable`, combobox, language switcher), translations,
9
- the SDK transport (graph + OS), notifications, app-scoped IndexedDB persistence, and an
10
- explicit auth-onboarding screen. Build your app by editing these pieces.
7
+ is wired: a searchable sidebar shell with multiple views, `@bardioc/ui` components (cards, metrics,
8
+ charts, a searchable/filterable/resizable `DataTable`, items, tabs, combobox, language switcher),
9
+ translations, the SDK (graph + OS calls, live graph events, OS menu, notifications), app-scoped
10
+ IndexedDB persistence, a debug menu that previews loading/empty/error states, and an explicit
11
+ auth-onboarding screen. Build your app by editing these pieces.
11
12
 
12
13
  **Global rules apply.**
13
14
 
15
+ What every Bardioc app shares (commands, dev auth, SDK, errors, manifest, copy, shipping):
16
+ @AGENTS.md
17
+
18
+ This file adds the Climate Dashboard, `@bardioc/ui`, i18n and the React side of the SDK.
19
+
14
20
  ---
15
21
 
16
22
  ## Code style
17
23
 
18
- - Single-line comments only (`// WHY, not WHAT`); no `/* */` blocks; no task refs.
24
+ - Single-line comments only (`// WHY, not WHAT`); no `/* */` blocks, also not as JSX
25
+ `{/* */}` — put a `//` line above the element or between its attributes. No task refs.
19
26
  - 150–200 LOC per component, 600 LOC hard max per file. Decompose early.
20
- - Constants: `SCREAMING_SNAKE_CASE`. Prefer `switch`/early-return over long `else-if`.
27
+ - Constants: `SCREAMING_SNAKE_CASE`. Keep a named constant even when it has one caller (`IDB_STORE`,
28
+ `MAX_EVENTS`): the name says what the value means. Inline it only when it holds nothing but CSS
29
+ classes.
30
+ - Prefer `switch`/early-return over long `else-if`.
31
+ - Compose class names with `cn` from `@bardioc/ui/lib`, never template strings or `+`. Write whole
32
+ Tailwind class names (`ok ? 'text-success' : 'text-error'`), never build them (`text-${tone}`).
21
33
 
22
34
  ---
23
35
 
@@ -25,63 +37,64 @@ explicit auth-onboarding screen. Build your app by editing these pieces.
25
37
 
26
38
  ```
27
39
  src/
28
- main.tsx # providers: AppSdkProvider (in host) + AppI18nProvider
29
- App.tsx # auth gate → DashboardProvider+AppShell, or Onboarding
40
+ main.tsx # installDevHost (standalone dev) + AppSdkProvider + AppI18nProvider
41
+ App.tsx # auth gate → DashboardProvider + AppShell, or Onboarding
30
42
  index.css # Tailwind v4 + @bardioc/ui theme tokens
31
- i18n.tsx # I18nProvider, OS-locale sync, useTranslation/useI18n
32
- locales/ # en.json / de.json translation strings
43
+ i18n.tsx # I18nProvider, @bardioc/ui catalog merge, OS-locale sync, useTranslation/useI18n
44
+ locales/ # en.json / de.json — the app's own strings
33
45
  dashboard-provider.tsx # context: climate state + profile, via useDashboard()
46
+ preview-state.tsx # debug preview state (live | mock | loading | empty | error), via usePreviewState()
34
47
  use-app-notify.ts # OS host notification (useNotify) mirrored to a local toast
35
48
  auth/
36
49
  onboarding.tsx # Empty full-page screen with the DEV_SECRET dev-auth steps
37
50
  shell/
38
- app-shell.tsx # Sidebar + Page + view routing + Toaster + OS top-menu (useCommandMenu)
39
- about-dialog.tsx # "About" Dialog — reads public/app-manifest.json at runtime (manifest example)
40
- header.tsx # Page actions: location combobox + language switcher + sync
51
+ app-shell.tsx # SidebarBasic (search + nav + footer) + Page + OS top menu (useCommandMenu)
52
+ view-content.tsx # view switch + the preview state (skeleton / error) around it
53
+ debug-menu.tsx # sidebar footer, dev only: debug info toggle + preview state
54
+ header.tsx # Page actions: station combobox + language switcher + sync
55
+ about-dialog.tsx # "About" Dialog — reads public/app-manifest.json at runtime
41
56
  climate/
42
- types.ts # Station/ClimateState + METRICS config (icon, unit, color)
43
- climate-data.ts # demo-data generator (replace with your real source)
57
+ types.ts # ViewId, Station/ClimateState, METRICS config (icon, unit, color), IDB keys
58
+ climate-data.ts # demo-data generator, 24 stations (replace with your real source)
44
59
  use-climate.ts # hook: IndexedDB load/persist + OS-probe sync + history
45
60
  overview-view.tsx # location card + metrics + chart + persistence indicator
46
- stations-view.tsx # DataTable (search + FilterDropdown + row select)
47
- location-card.tsx # Card + BadgeStatus widget
61
+ stations-view.tsx # DataTable: search, FilterDropdown, paging, resizable columns, actions column
62
+ location-card.tsx # MediaTile + BadgeStatus widget
48
63
  metrics-grid.tsx # GenericMetrics widget (5 readings)
49
64
  conditions-chart.tsx # BarChartMultiple widget (7-day outlook)
50
65
  location-select.tsx # Combobox station switcher
51
66
  events/
52
- live-events-view.tsx # live graph events via useGraphEvents (SSE, permission "events")
67
+ live-events-view.tsx # live graph events via useGraphEvents, as Item rows
68
+ mock-events.ts # sample events for the "Mock Data" preview
53
69
  graph/
54
- requests.ts # catalog of every SDK transport request (graph + OS)
55
- explorer-view.tsx # run real requests; mock fallback when unauthorized
70
+ requests.ts # catalog of SDK requests (graph + OS) shown in the Explorer
71
+ explorer-view.tsx # run real requests; simulated data when a call fails
56
72
  profile/
57
73
  use-profile.ts # os.profile fetch (+ operatorLabel helper)
58
74
  profile-view.tsx # real profile data + error state
59
75
  ```
60
76
 
61
- Views are selected from the sidebar (`ViewId` in `climate/types.ts`); each view consumes
62
- shared state from `useDashboard()`. The same sections are also driven from the **OS top menu**
63
- (`useCommandMenu` in `app-shell.tsx`): each view is a checkable command under a `View` menu, plus
64
- an `app`-kind root next to the host's About / Version entries — the example of OS menu integration.
77
+ Views are listed in `NAV` (`shell/app-shell.tsx`) and rendered by `ViewContent`
78
+ (`shell/view-content.tsx`); each view reads shared state from `useDashboard()`. The same `NAV`
79
+ drives the sidebar (filtered by its search, `⌘F` / `Ctrl+F`) and the **OS top menu**
80
+ (`useCommandMenu`): each view is a checkable command under a `View` menu, plus an `app`-kind root
81
+ next to the host's About / Version entries.
65
82
 
66
83
  ---
67
84
 
68
85
  ## Development
69
86
 
70
- ```bash
71
- pnpm dev # http://localhost:{{PORT}}
72
- pnpm build # → dist/
73
- pnpm bundle # → {{APP_NAME}}.zip
74
- pnpm check-types # tsc
75
- pnpm lint # eslint (@bardioc/configs preset)
76
- pnpm format # prettier (@bardioc/configs preset)
77
- ```
78
-
79
87
  ESLint, Prettier and the `tsconfig.json` base come from `@bardioc/configs` — edit
80
88
  `eslint.config.mjs` / `prettier.config.mjs` only to add app-specific overrides on top of the preset.
81
89
 
82
- `npm run dev` runs plain Vite — a local preview shell (no transport): the dashboard renders, but
83
- live SDK data comes only when the app is opened inside the host, or when `DEV_SECRET` and
84
- `API_BASE_URL` are set for standalone dev auth (see `.env.example`).
90
+ Standalone, `main.tsx` installs the dev host and `vite.config.ts` the `bardiocDevAuth` proxy; without
91
+ dev auth the views show their error or fallback states. A build opened outside the host shows the
92
+ onboarding screen (`auth/onboarding.tsx`) instead.
93
+
94
+ **Debug menu** (sidebar footer, `npm run dev` only): _Show Debug Info_ toggles the same debug flag
95
+ as the OS dock; _Preview State_ forces every view into `Mock Data`, `Loading`, `Empty` or `Error`,
96
+ so you can check skeletons, empty states and errors without breaking the backend. Give every new
97
+ view all of these states.
85
98
 
86
99
  ---
87
100
 
@@ -89,8 +102,8 @@ live SDK data comes only when the app is opened inside the host, or when `DEV_SE
89
102
 
90
103
  Tailwind v4 + the Bardioc design system. **Before writing any UI, read
91
104
  `node_modules/@bardioc/ui/docs/INDEX.md`** — component choice, design tokens, layout and spacing
92
- patterns, and copy rules ship with the installed package, so they match its version. Theme is wired
93
- in `src/index.css`:
105
+ patterns, and copy rules ship with the installed package, so they match its version. Check a
106
+ component's props in its `.d.ts` before using one. Theme is wired in `src/index.css`:
94
107
 
95
108
  ```css
96
109
  @import 'tailwindcss';
@@ -99,44 +112,51 @@ in `src/index.css`:
99
112
  @source './**/*.{ts,tsx}';
100
113
  ```
101
114
 
102
- Import components from the public subpaths (never reach into `@bardioc/ui/src`):
115
+ Import from the public subpaths (never reach into `@bardioc/ui/src`):
103
116
 
104
117
  ```typescript
105
- import {
106
- Badge,
107
- BadgeStatus,
108
- Button,
109
- Card,
110
- Combobox,
111
- CheckboxGroup,
112
- ConnectedLanguageSwitcher,
113
- DataTable,
114
- Empty,
115
- FilterDropdown,
116
- Kbd,
117
- Page,
118
- SidebarBasic,
119
- SidebarContainedProvider,
120
- SortableColumnHeader,
121
- Toaster,
122
- TruncatedCellText,
123
- toast,
124
- } from '@bardioc/ui/components';
118
+ import { Button, Card, DataTable, Empty, Page, SidebarBasic } from '@bardioc/ui/components';
125
119
  import { GenericMetrics } from '@bardioc/ui/components/metrics';
126
120
  import { BarChartMultiple } from '@bardioc/ui/components/charts';
121
+ import { useElementWidth, useFuzzySearch } from '@bardioc/ui/hooks';
122
+ import { cn, useDebugMode } from '@bardioc/ui/lib';
123
+ import * as Shadcn from '@bardioc/ui/shadcn'; // only parts with no Bardioc version
127
124
  import type { ChartConfig } from '@bardioc/ui/shadcn/chart';
128
125
  import type { TReactTable } from '@bardioc/ui/types'; // TReactTable.ColumnDef<Row>
129
126
  ```
130
127
 
131
- The sidebar uses `SidebarBasic` inside a `SidebarContainedProvider` (the contained variant for
132
- OS windows); the language switcher is `ConnectedLanguageSwitcher useI18n={useI18n}`.
128
+ **Component choice.** Use the Bardioc component from `@bardioc/ui/components` wherever one exists
129
+ (`Button`, `Badge`, `Tabs`, `Item*`, `DropdownMenuItem`, `SidebarMenuButton`, …). Import from
130
+ `@bardioc/ui/shadcn` only the parts Bardioc doesn't override (`DropdownMenu`, `DropdownMenuTrigger`,
131
+ `DropdownMenuContent`, `SidebarMenu`, `SidebarMenuItem`) — that's correct, not a gap. Don't rebuild
132
+ what a component already does: a list of rows is `ItemGroup` + `Item`, a "nothing here" message is
133
+ `Empty`, a table is `DataTable`.
134
+
135
+ **Use the props, not classes.** Size, variant, truncation and layout come from component props
136
+ (`size="sm"`, `variant="ghost"`, `ItemTitle truncate`, `Card contentClassName`). If a component
137
+ can't do what you need, that's a `@bardioc/ui` gap to report — not a reason for an arbitrary
138
+ `h-8`, `[&>span]:…` selector or a raw color.
139
+
140
+ **Rules from the design system:**
141
+
142
+ - No false affordances: hover shadows, lifts or highlights only on things that are clickable.
143
+ - `Empty` goes on the page, not inside a `Card` (its background shows as a block in dark mode).
144
+ - Page-header actions use the small sizes: `size="sm"` buttons and inputs, `icon-sm` icon buttons.
145
+ - An icon-only button needs a `title` (it becomes the tooltip and accessible name) or `aria-label`.
146
+ - Selected/forced state is shown with the component's own state (`isActive`, `aria-pressed`, a
147
+ filled `variant`), e.g. the debug entry and the Stations actions column.
148
+ - Filter triggers read "Filter" / "2 Filters" (`common.filter.*` from the `@bardioc/ui` catalog),
149
+ wrapped in `StableWidthText` so the button keeps its width.
150
+ - Theme tokens only (`bg-card`, `text-muted-foreground`, `var(--chart-1)`), never raw colors.
133
151
 
134
152
  ### Layout — the container contract
135
153
 
136
154
  The app root opens `@container/app`; `Page` opens its own `@container/page` inside it. Keep both:
137
155
  sizing that should follow the whole app reads `@container/app`, and anything inside the page column
138
156
  — the page header included — reads `@container/page`, so it responds to the column rather than the
139
- window.
157
+ window. Use container variants (`@xl/page:flex`, `@3xl/page:grid-cols-2`), not viewport
158
+ breakpoints (`md:`), and `useElementWidth` when JS has to switch a layout (the Explorer flips its
159
+ tabs to vertical when wide).
140
160
 
141
161
  `Page` is the scrolling column, a **sibling of the sidebar** — it owns the header, the scroller and
142
162
  the padding that lines them up. Don't compose a header and a `ScrollArea` by hand, and don't wrap
@@ -151,9 +171,9 @@ the sidebar in it:
151
171
  </div>
152
172
  ```
153
173
 
154
- Collapse your own `actions` against the same container (e.g. `@3xl/page:hidden`) so both sides agree
155
- on what "wide enough" means. `sticky` keeps the header at the top while the body scrolls under it,
156
- compacting once it does; drop it if you want the header to scroll away instead.
174
+ Collapse your own `actions` against the same container (the station combobox is
175
+ `hidden @xl/page:flex`) so both sides agree on what "wide enough" means. `sticky` keeps the header
176
+ at the top while the body scrolls under it, compacting once it does.
157
177
 
158
178
  ---
159
179
 
@@ -170,114 +190,108 @@ t('overview.updated', { fallback: 'Updated {time}', params: { time } });
170
190
  ```
171
191
 
172
192
  - Add a string → add the key to **both** `src/locales/en.json` and `src/locales/de.json`.
193
+ - Always pass a `fallback`. It is interpolated like a translation, so `{param}`s and `{count}` are
194
+ filled in (`fallback: 'Debug: {state}', params: { state }`).
195
+ - `src/i18n.tsx` lays the app's catalog over `@bardioc/ui/i18n` (`mergeTranslations`), so
196
+ `@bardioc/ui` component strings and its `common.*` keys work out of the box and the app only
197
+ carries its own keys.
173
198
  - `AppI18nProvider syncWithOs` follows the OS language (`useOsConfig().language`); the header's
174
199
  `ConnectedLanguageSwitcher` lets the user switch it manually.
175
- - Always pass a `fallback` so a missing key degrades gracefully.
176
- - `@bardioc/ui` component strings ship with the `@bardioc/ui` package itself — the app only
177
- carries its own `en/de.json`.
178
- - Copy style (Title Case labels, sentence-case messages, American English):
179
- `node_modules/@bardioc/ui/docs/copy.md`.
200
+ - Copy rules are in `AGENTS.md`; details and exceptions in `node_modules/@bardioc/ui/docs/copy.md`.
180
201
 
181
202
  ---
182
203
 
183
204
  ## Persistence — app-scoped IndexedDB
184
205
 
185
- The SDK gives every app a sandboxed IndexedDB via `bridge.idb(storeName)`. Requires the
186
- `"indexdb"` permission (already in `public/app-manifest.json`).
206
+ The SDK gives every app a sandboxed IndexedDB via `bardioc.idb(storeName)`. Standalone, it lives
207
+ in this browser's IndexedDB.
187
208
 
188
209
  ```typescript
189
- const store = bridge.idb('climate');
190
- await store.put('state', value);
191
- const saved = await store.get<ClimateState>('state');
192
- await store.query({ prefix: 'log:', limit: 50 }); // history — see use-climate.ts
210
+ const bardioc = useSdk();
211
+ const store = bardioc.idb(IDB_STORE);
212
+ await store.put(IDB_STATE_KEY, value);
213
+ const saved = await store.get(IDB_STATE_KEY); // { key, value, updatedAt } | null
214
+ await store.query({ prefix: IDB_LOG_PREFIX, limit: 100 }); // history — see use-climate.ts
193
215
  ```
194
216
 
195
217
  The Overview shows a live "Persisted to IndexedDB · Saved … · N snapshots" indicator so the
196
- write path is visible. For small key-value state, `bridge.storage.get/set/delete` (permission
197
- `"storage"`) is simpler.
218
+ write path is visible. Persisted state outlives code changes: after changing the demo data, clear
219
+ the site data (DevTools → Application) to see it.
198
220
 
199
221
  ---
200
222
 
201
- ## SDK API
223
+ ## SDK in React — `@bardioc/app-sdk/react`
224
+
225
+ The `Bardioc` object and its API are in `AGENTS.md`; this app reaches it through hooks.
202
226
 
203
227
  ```typescript
204
- const bridge = useSdk(); // host bridge (only inside the host/dev session)
228
+ const bardioc = useSdk(); // the connected SDK (only inside AppSdkProvider)
205
229
  const notify = useAppNotify(); // OS host notification + local toast (src/use-app-notify.ts)
206
- const osConfig = useOsConfig(); // { theme, language, instanceId, ... }
230
+ const osConfig = useOsConfig(); // { theme, language, ... }
207
231
  ```
208
232
 
209
- **Graph** — `bridge.transport.graph.*`: `get(id)`, `getByXid`, `query(lucene, opts)`,
210
- `gremlin(rootId, query)`, `create`, `update`, `connect`, `timeseries.get/query`. Note: `query`
211
- resolves to a `{ items: [...] }` envelope at runtime (not a bare array) — normalize before
212
- indexing, and escape `/` in Lucene as `\/` (`ogit\/_type:"…"`). Hard-coded ids/types won't exist
213
- in every instance: discover them from a wildcard `query('*')` first (see `graph/requests.ts`).
214
-
215
- **OS** — `bridge.transport.os.*`: `profile.get()`, `organization.getStructure()`, `applications.list()`.
233
+ The `{ items }` envelope is normalized by `toItems` in `graph/requests.ts`. Error states:
234
+ `use-climate.ts`, `profile-view.tsx`, and `explorer-view.tsx`, which shows labeled simulated data.
216
235
 
217
- **Live events** — `useGraphEvents({ types }, handler)` (`@bardioc/app-sdk/react`) opens a scoped
218
- graph-event SSE stream while mounted (permission `"events"`). Each `handler` receives a
219
- `GraphEvent` (`id`, `type`, `timestamp`, `body?`). See `events/live-events-view.tsx`. No events
220
- arrive outside a valid session — render an empty state.
236
+ **Live events** — `useGraphEvents({ types }, handler)` (`@bardioc/app-sdk/react`) opens a graph
237
+ event stream while mounted. Each `handler` receives a `GraphEvent` (`id`, `type`, `timestamp`,
238
+ `body?`). See `events/live-events-view.tsx`. No events arrive outside a valid session — render an
239
+ empty state.
221
240
 
222
241
  **OS menu** — `useCommandMenu(commands, menu)` (`@bardioc/app-sdk/react`) pushes an app menu tree
223
- into the host menu bar. `commands` are `{ id, title, checked?, handler }`; `menu` is `SdkMenuRoot[]`
224
- where each root has `label`, optional `kind: 'app'`, and `items` of `{ type: 'command', commandId,
225
- label?, leftIcon? }`. `leftIcon` is a lucide export name (string) the host resolves. See
226
- `shell/app-shell.tsx` — view switching is mirrored here as checkable commands.
227
-
228
- The **Graph Explorer** view runs the full catalog in `graph/requests.ts` against the live
229
- transport. The graph presets issue real calls — a wildcard `query('*')`, a query by the type of a
230
- real node, a node `get`, and a Gremlin `both()` neighbor traversal. The typed/get/gremlin presets
231
- each pull a real id and type from a wildcard query first (`sampleNodes` → `toItems`), so they
232
- return live data instead of empties. Network calls fail (401/timeout) outside a valid session —
233
- always `try/catch` and fall back (see `use-climate.ts` and `explorer-view.tsx`, which show
234
- simulated data).
242
+ into the host menu bar. `commands` are `AppCommand`s (`{ id, title, checked?, handler }`); `menu`
243
+ is `MenuRoot[]`, each root with a `label`, optional `kind: 'app'`, and `items` of
244
+ `{ type: 'command', commandId, label?, leftIcon? }`. `leftIcon` is a lucide export name the host
245
+ resolves. See `shell/app-shell.tsx` — view switching is mirrored there as checkable commands.
246
+
247
+ The **Graph Explorer** view runs the catalog in `graph/requests.ts` against the live SDK: a
248
+ wildcard `query('*')`, a query by the type of a real node, a node `get`, a Gremlin `both()`
249
+ traversal, and the OS calls. The typed/get/gremlin presets pull a real id and type from a wildcard
250
+ query first (`sampleNodes`), so they return live data instead of empties.
235
251
 
236
252
  ---
237
253
 
238
254
  ## Manifest — `public/app-manifest.json`
239
255
 
240
- The starter ships a full manifest as a showcase of every supported field (validated by
241
- `validateManifest` in `@bardioc/app-sdk`):
242
-
243
- - `permissions`: `notify`, `transport`, `indexdb`, `events` (the APIs this app actually uses).
244
- Other valid values: `storage` (host-deferred) and the deprecated `kernel-proxy` — do not add
245
- `kernel-proxy` for hosted apps.
246
- - `window`: `size` (`xs|sm|md|lg|xl` or a custom `{ width, height, minWidth?, minHeight? }`),
247
- `resizable`, `maximizable`, `minimizable`, `customScroll`, `centered`, `isHeaderless`,
248
- `isInstanceAware`, `canHaveMultipleWindows`.
249
- - Metadata: `author`, `categories`, `showInDock`, `showInDockAppsMenu`.
250
-
251
- Trim any field you don't need — only `manifestVersion`, `id`, `name`, `version` are required.
256
+ The starter ships a full manifest as a showcase of the supported fields (listed in `AGENTS.md`).
257
+ Trim any field you don't need.
252
258
 
253
259
  ---
254
260
 
255
261
  ## How to…
256
262
 
257
263
  - **Add a metric tile** → add a `MetricDef` (key, icon, unit, color) to `METRICS` in `types.ts`,
258
- add the field to `ClimateReading` + the generator, and add `metrics.*` strings to `src/locales/{en,de}.json`.
259
- - **Add a view** → add a `ViewId` (`climate/types.ts`), a `NAV` entry + `case` in `shell/app-shell.tsx`,
260
- a `nav.*` string in `src/locales/{en,de}.json`, and the view component. The `NAV` entry (with its
261
- `iconName`) is auto-wired into both the sidebar and the OS `View` menu.
264
+ add the field to `ClimateReading` + the generator, and add `metrics.*` strings to
265
+ `src/locales/{en,de}.json`.
266
+ - **Add a view** → add a `ViewId` (`climate/types.ts`), a `NAV` entry in `shell/app-shell.tsx`, a
267
+ `case` in `shell/view-content.tsx`, a `nav.*` string in `src/locales/{en,de}.json`, and the view
268
+ component. The `NAV` entry (with its `iconName`) is auto-wired into the sidebar, its search and
269
+ the OS `View` menu. Check the view in every debug preview state.
262
270
  - **Add an OS menu command** → add an `AppCommand` (`{ id, title, handler }`) and a matching
263
- `{ type: 'command', commandId }` item under a `SdkMenuRoot` in `app-shell.tsx`; set `checked` for
271
+ `{ type: 'command', commandId }` item under a `MenuRoot` in `app-shell.tsx`; set `checked` for
264
272
  toggles. Add any `menu.*` label to `src/locales/{en,de}.json`.
265
- - **Add a table column** → push a `TReactTable.ColumnDef<Station>` in `stations-view.tsx`.
266
- - **Add an SDK request** → add an entry to `SDK_REQUESTS` in `graph/requests.ts` (it shows in the Explorer).
267
- - **Use real data** → replace `generateState()` with `bridge.transport.graph.*` calls in
268
- `use-climate.ts`; keep the IndexedDB write so it persists across reloads.
269
- - **Read the manifest** → `fetch(\`${import.meta.env.BASE_URL}app-manifest.json\`)`(served at the
270
- Vite base in dev and inside the host). See`shell/about-dialog.tsx` — it shows version, id,
271
- description, and permissions straight from the manifest.
273
+ - **Add a table column** → push a `TReactTable.ColumnDef<Station>` in `stations-view.tsx`, with a
274
+ `size` (resizing resets to it). Row actions go in the trailing `actions` column: fixed 40px, no
275
+ header, not sortable or resizable, `icon-xs` buttons.
276
+ - **Add an SDK request** → add an entry to `SDK_REQUESTS` in `graph/requests.ts` (it shows in the
277
+ Explorer).
278
+ - **Use real data** → replace `generateState()` with `bardioc.graph.*` calls in `use-climate.ts`;
279
+ keep the IndexedDB write so it persists across reloads.
280
+ - **Read the manifest** → ``fetch(`${import.meta.env.BASE_URL}app-manifest.json`)`` (served at
281
+ the Vite base in dev and inside the host). See `shell/about-dialog.tsx`.
272
282
 
273
283
  ---
274
284
 
275
285
  ## Anti-patterns
276
286
 
277
- | ❌ Don't | ✅ Do |
278
- | -------------------------------------------- | ----------------------------------------------------- |
279
- | `bridge.notify(...)` in a component | `const notify = useNotify()` |
280
- | `<AppSdkProvider>` rendered outside the host | gate on `isInsideIframe()` / dev session (`main.tsx`) |
281
- | raw `#1a1a1a` / `oklch(...)` colors | theme tokens (`bg-card`, `var(--chart-1)`) |
282
- | hard-code a label string | `t('key', { fallback })` in `en` **and** `de` |
283
- | reach into `@bardioc/ui/src/...` | import from `@bardioc/ui/components` etc. |
287
+ | ❌ Don't | ✅ Do |
288
+ | ------------------------------------------------- | ------------------------------------------------------------- |
289
+ | `bardioc.ui.notify(...)` in a component | `const notify = useAppNotify()` |
290
+ | `<AppSdkProvider>` without a host or dev session | gate on `isInsideIframe()` / `isDevStandalone()` (`main.tsx`) |
291
+ | raw `#1a1a1a` / `oklch(...)` colors | theme tokens (`bg-card`, `var(--chart-1)`) |
292
+ | `h-8`, `[&>span]:truncate` to restyle a component | its props (`size`, `variant`, `truncate`) |
293
+ | viewport breakpoints (`md:`) inside the page | container variants (`@xl/page:`) |
294
+ | hover shadow on something not clickable | no hover, or make it a real action |
295
+ | hand-built rows / empty messages | `ItemGroup` + `Item`, `Empty` |
296
+ | hard-code a label string | `t('key', { fallback })` in `en` **and** `de` |
297
+ | reach into `@bardioc/ui/src/...` | import from `@bardioc/ui/components` etc. |
@@ -22,9 +22,9 @@
22
22
  "release:publish": "changeset publish"
23
23
  },
24
24
  "dependencies": {
25
- "@bardioc/app-sdk": "^1.0.1",
26
- "@bardioc/i18n-core": "^0.1.1",
27
- "@bardioc/ui": "^0.7.29",
25
+ "@bardioc/app-sdk": "^1.0.3",
26
+ "@bardioc/i18n-core": "^0.1.2",
27
+ "@bardioc/ui": "^0.7.31",
28
28
  "lucide-react": "^0.544.0",
29
29
  "react": "19.2.6",
30
30
  "react-dom": "19.2.6"
@@ -1,6 +1,7 @@
1
1
  import { isDevStandalone, isInsideIframe } from '@bardioc/app-sdk/dev';
2
2
  import { Onboarding } from './auth/onboarding';
3
3
  import { DashboardProvider } from './dashboard-provider';
4
+ import { PreviewStateProvider } from './preview-state';
4
5
  import { AppShell } from './shell/app-shell';
5
6
 
6
7
  export function App() {
@@ -11,8 +12,10 @@ export function App() {
11
12
  }
12
13
 
13
14
  return (
14
- <DashboardProvider>
15
- <AppShell />
16
- </DashboardProvider>
15
+ <PreviewStateProvider>
16
+ <DashboardProvider>
17
+ <AppShell />
18
+ </DashboardProvider>
19
+ </PreviewStateProvider>
17
20
  );
18
21
  }
@@ -1,5 +1,5 @@
1
1
  import { isInsideIframe } from '@bardioc/app-sdk/dev';
2
- import { Button, Empty, Kbd } from '@bardioc/ui/components';
2
+ import { Button, Card, Empty, Kbd } from '@bardioc/ui/components';
3
3
  import { PlugZap } from 'lucide-react';
4
4
  import { useTranslation } from '../i18n';
5
5
 
@@ -9,7 +9,7 @@ function DetectedRow({ label, ok }: Readonly<{ label: string; ok: boolean }>) {
9
9
  <div className="flex items-center justify-between gap-4">
10
10
  <span className="text-muted-foreground">{label}</span>
11
11
  <span className={ok ? 'text-success' : 'text-muted-foreground'}>
12
- {ok ? t('auth.yes', { fallback: 'yes' }) : t('auth.no', { fallback: 'no' })}
12
+ {ok ? t('auth.yes', { fallback: 'Yes' }) : t('auth.no', { fallback: 'No' })}
13
13
  </span>
14
14
  </div>
15
15
  );
@@ -36,31 +36,27 @@ export function Onboarding() {
36
36
  </Button>
37
37
  }
38
38
  >
39
- <div className="border-border bg-card mt-4 w-full max-w-md rounded-lg border p-4 text-left text-xs leading-6">
40
- <p className="text-foreground mb-2 font-medium">
41
- {t('auth.stepsTitle', { fallback: 'Run standalone with dev auth:' })}
42
- </p>
43
- <ol className="text-muted-foreground list-decimal space-y-2 pl-4">
44
- <li>
45
- {t('auth.step1', {
46
- fallback:
47
- 'Set DEV_SECRET=<your-secret> and API_BASE_URL=<your-host> in the .env file.',
48
- })}
49
- </li>
50
- <li>
51
- <div>{t('auth.step2', { fallback: 'Start the dev server:' })}</div>
52
- <Kbd>pnpm dev</Kbd>
53
- </li>
54
- </ol>
55
- <div className="border-border mt-3 space-y-1 border-t pt-3">
56
- <p className="text-foreground font-medium">
57
- {t('auth.detectedTitle', { fallback: 'Detected Environment' })}
58
- </p>
39
+ <div className="flex w-full max-w-md flex-col gap-4 text-left">
40
+ <Card title={t('auth.stepsTitle', { fallback: 'Run Standalone with Dev Auth' })}>
41
+ <ol className="text-muted-foreground flex list-decimal flex-col gap-2 pl-4">
42
+ <li>
43
+ {t('auth.step1', {
44
+ fallback:
45
+ 'Set DEV_SECRET=<your-secret> and API_BASE_URL=<your-host> in the .env file.',
46
+ })}
47
+ </li>
48
+ <li>
49
+ <div>{t('auth.step2', { fallback: 'Start the dev server.' })}</div>
50
+ <Kbd>npm run dev</Kbd>
51
+ </li>
52
+ </ol>
53
+ </Card>
54
+ <Card title={t('auth.detectedTitle', { fallback: 'Detected Environment' })}>
59
55
  <DetectedRow
60
56
  label={t('auth.insideHost', { fallback: 'Inside Host Iframe' })}
61
57
  ok={insideHost}
62
58
  />
63
- </div>
59
+ </Card>
64
60
  </div>
65
61
  </Empty>
66
62
  );
@@ -9,6 +9,25 @@ const STATIONS: [StationBase, ...StationBase[]] = [
9
9
  { id: 'aurora', name: 'Aurora Flats', region: 'Polar Rim' },
10
10
  { id: 'cinder', name: 'Cinder Basin', region: 'Volcanic Plains' },
11
11
  { id: 'mistral', name: 'Mistral Cove', region: 'Coastal Shelf' },
12
+ { id: 'ember', name: 'Ember Spire', region: 'Volcanic Plains' },
13
+ { id: 'glacier', name: 'Glacier Gate', region: 'Polar Rim' },
14
+ { id: 'saffron', name: 'Saffron Dunes', region: 'Arid Expanse' },
15
+ { id: 'tidewater', name: 'Tidewater Point', region: 'Coastal Shelf' },
16
+ { id: 'canopy', name: 'Canopy Hollow', region: 'Equatorial Belt' },
17
+ { id: 'granite', name: 'Granite Pass', region: 'Highlands' },
18
+ { id: 'halcyon', name: 'Halcyon Bay', region: 'Coastal Shelf' },
19
+ { id: 'obsidian', name: 'Obsidian Field', region: 'Volcanic Plains' },
20
+ { id: 'frostline', name: 'Frostline Camp', region: 'Polar Rim' },
21
+ { id: 'mirage', name: 'Mirage Basin', region: 'Arid Expanse' },
22
+ { id: 'fernwood', name: 'Fernwood Glade', region: 'Equatorial Belt' },
23
+ { id: 'summit', name: 'Summit Watch', region: 'Highlands' },
24
+ { id: 'driftwood', name: 'Driftwood Reach', region: 'Coastal Shelf' },
25
+ { id: 'cobalt', name: 'Cobalt Crater', region: 'Volcanic Plains' },
26
+ { id: 'tundra', name: 'Tundra Outpost', region: 'Polar Rim' },
27
+ { id: 'sandstone', name: 'Sandstone Mesa', region: 'Arid Expanse' },
28
+ { id: 'monsoon', name: 'Monsoon Delta', region: 'Equatorial Belt' },
29
+ { id: 'windward', name: 'Windward Crest', region: 'Highlands' },
30
+ { id: 'coral', name: 'Coral Lagoon', region: 'Coastal Shelf' },
12
31
  ];
13
32
  const CONDITIONS: [Condition, ...Condition[]] = ['sunny', 'cloudy', 'rainy'];
14
33
  const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const;
@@ -1,3 +1,4 @@
1
+ import { Card } from '@bardioc/ui/components';
1
2
  import { BarChartMultiple } from '@bardioc/ui/components/charts';
2
3
  import type { ChartConfig } from '@bardioc/ui/shadcn/chart';
3
4
  import { useTranslation } from '../i18n';
@@ -24,13 +25,17 @@ export function ConditionsChart({ forecast }: Readonly<ConditionsChartProps>) {
24
25
  }));
25
26
 
26
27
  return (
27
- <BarChartMultiple
28
- seriesData={seriesData}
29
- xKey="day"
30
- chartConfig={chartConfig}
28
+ // BarChartMultiple is a bare chart; the Card around it carries the heading.
29
+ <Card
31
30
  title={t('chart.title', { fallback: '7-Day Outlook' })}
32
31
  description={t('chart.description', { fallback: 'Daylight hours by condition.' })}
33
- heightClass="h-[220px]"
34
- />
32
+ >
33
+ <BarChartMultiple
34
+ seriesData={seriesData}
35
+ xKey="day"
36
+ chartConfig={chartConfig}
37
+ heightClass="h-55"
38
+ />
39
+ </Card>
35
40
  );
36
41
  }
@@ -1,4 +1,4 @@
1
- import { Badge, BadgeStatus, Card } from '@bardioc/ui/components';
1
+ import { Badge, BadgeStatus, Card, MediaTile } from '@bardioc/ui/components';
2
2
  import { Cloud, CloudRain, Sun, type LucideIcon } from 'lucide-react';
3
3
  import { useTranslation } from '../i18n';
4
4
  import type { Condition, DataSource, Station } from './types';
@@ -20,15 +20,12 @@ interface LocationCardProps {
20
20
  export function LocationCard({ station, updatedAt, source }: Readonly<LocationCardProps>) {
21
21
  const { t } = useTranslation();
22
22
  const condition = CONDITION[station.condition];
23
- const Icon = condition.icon;
24
23
  const time = new Date(updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
25
24
 
26
25
  return (
27
26
  <Card>
28
27
  <div className="flex items-center gap-4">
29
- <div className="bg-muted text-foreground flex size-12 shrink-0 items-center justify-center rounded-full">
30
- <Icon className="size-6" />
31
- </div>
28
+ <MediaTile size="md" icon={condition.icon} />
32
29
  <div className="min-w-0 flex-1">
33
30
  <div className="truncate text-lg font-semibold">{station.name}</div>
34
31
  <div className="text-muted-foreground truncate text-xs">{station.region}</div>
@@ -16,11 +16,16 @@ export function LocationSelect({ stations, value, onSelect }: Readonly<LocationS
16
16
  <Combobox
17
17
  items={stations.map(station => station.id)}
18
18
  value={value}
19
- onValueChange={next => onSelect(next as string)}
19
+ // Combobox reports null when cleared or when its value leaves the list; keep the station.
20
+ onValueChange={next => {
21
+ if (next) onSelect(next as string);
22
+ }}
20
23
  itemToStringValue={item => item as string}
21
24
  itemToStringLabel={item => nameById.get(item as string) ?? (item as string)}
22
25
  placeholder={t('header.location', { fallback: 'Station' })}
23
- className="h-8 w-52"
26
+ size="sm"
27
+ // Too wide for a narrow page header; below `@xl` stations stay selectable from the Stations view.
28
+ className="hidden w-52 @xl/page:flex"
24
29
  />
25
30
  );
26
31
  }