@bardioc/create-bardioc-app 0.4.0 → 0.5.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/README.md +7 -13
- package/bin/create.mjs +5 -5
- package/package.json +1 -1
- package/src/scaffold.js +2 -2
- package/templates/_base/.changeset/README.md +3 -3
- package/templates/_base/.claude/commands/changeset-app.md +5 -5
- package/templates/_base/.claude/commands/refresh-bundle.md +5 -5
- package/templates/_base/README.md +9 -9
- package/templates/angular/package.json +6 -6
- package/templates/nextjs/.env.example +1 -1
- package/templates/nextjs/package.json +1 -1
- package/templates/preact/package.json +1 -1
- package/templates/solid/package.json +1 -1
- package/templates/svelte/package.json +1 -1
- package/templates/vite/.env.example +1 -1
- package/templates/vite/CLAUDE.md +199 -59
- package/templates/vite/package.json +5 -1
- package/templates/vite/public/app-manifest.json +25 -0
- package/templates/vite/src/App.tsx +11 -133
- package/templates/vite/src/auth/onboarding.tsx +67 -0
- package/templates/vite/src/climate/climate-data.ts +92 -0
- package/templates/vite/src/climate/conditions-chart.tsx +38 -0
- package/templates/vite/src/climate/location-card.tsx +50 -0
- package/templates/vite/src/climate/location-select.tsx +26 -0
- package/templates/vite/src/climate/metrics-grid.tsx +22 -0
- package/templates/vite/src/climate/overview-view.tsx +52 -0
- package/templates/vite/src/climate/stations-view.tsx +149 -0
- package/templates/vite/src/climate/types.ts +93 -0
- package/templates/vite/src/climate/use-climate.ts +123 -0
- package/templates/vite/src/dashboard-provider.tsx +27 -0
- package/templates/vite/src/events/live-events-view.tsx +74 -0
- package/templates/vite/src/graph/explorer-view.tsx +118 -0
- package/templates/vite/src/graph/requests.ts +142 -0
- package/templates/vite/src/i18n.tsx +68 -0
- package/templates/vite/src/index.css +12 -64
- package/templates/vite/src/locales/de.json +141 -0
- package/templates/vite/src/locales/en.json +141 -0
- package/templates/vite/src/main.tsx +10 -4
- package/templates/vite/src/profile/profile-view.tsx +75 -0
- package/templates/vite/src/profile/use-profile.ts +52 -0
- package/templates/vite/src/shell/about-dialog.tsx +129 -0
- package/templates/vite/src/shell/app-shell.tsx +160 -0
- package/templates/vite/src/shell/header.tsx +39 -0
- package/templates/vite/src/use-app-notify.ts +23 -0
- package/templates/vite/src/vite-env.d.ts +5 -5
- package/templates/vue/package.json +1 -1
package/templates/vite/CLAUDE.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# {{DISPLAY_NAME}}
|
|
2
2
|
|
|
3
|
-
Bardioc app. Vite + React 19 + TypeScript 6 +
|
|
3
|
+
Bardioc hosted app. Vite + React 19 + TypeScript 6 + `@bardioc/app-sdk`, with
|
|
4
|
+
`@bardioc/ui` (components + theme), `@bardioc/i18n` (translations), and `@bardioc/types`.
|
|
5
|
+
|
|
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.
|
|
4
11
|
|
|
5
12
|
**Global rules apply.**
|
|
6
13
|
|
|
@@ -8,16 +15,53 @@ Bardioc app. Vite + React 19 + TypeScript 6 + @bardioc/app-sdk.
|
|
|
8
15
|
|
|
9
16
|
## Code style
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
- Single-line comments only (`// WHY, not WHAT`); no `/* */` blocks; no task refs.
|
|
19
|
+
- 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`.
|
|
21
|
+
|
|
22
|
+
---
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
- No `/* */` blocks
|
|
15
|
-
- No task refs
|
|
24
|
+
## Structure
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
main.tsx # providers: AppSdkProvider (in host) + AppI18nProvider
|
|
29
|
+
App.tsx # auth gate → DashboardProvider+AppShell, or Onboarding
|
|
30
|
+
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
|
|
33
|
+
dashboard-provider.tsx # context: climate state + profile, via useDashboard()
|
|
34
|
+
use-app-notify.ts # OS host notification (useNotify) mirrored to a local toast
|
|
35
|
+
auth/
|
|
36
|
+
onboarding.tsx # Empty full-page screen with the CLI dev-session steps
|
|
37
|
+
shell/
|
|
38
|
+
app-shell.tsx # Sidebar + view routing + Toaster + OS top-menu integration (useCommandMenu)
|
|
39
|
+
about-dialog.tsx # "About" Dialog — reads public/app-manifest.json at runtime (manifest example)
|
|
40
|
+
header.tsx # location combobox + language switcher + sync
|
|
41
|
+
climate/
|
|
42
|
+
types.ts # Station/ClimateState + METRICS config (icon, unit, color)
|
|
43
|
+
climate-data.ts # demo-data generator (replace with your real source)
|
|
44
|
+
use-climate.ts # hook: IndexedDB load/persist + OS-probe sync + history
|
|
45
|
+
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
|
|
48
|
+
metrics-grid.tsx # GenericMetrics widget (5 readings)
|
|
49
|
+
conditions-chart.tsx # BarChartMultiple widget (7-day outlook)
|
|
50
|
+
location-select.tsx # Combobox station switcher
|
|
51
|
+
events/
|
|
52
|
+
live-events-view.tsx # live graph events via useGraphEvents (SSE, permission "events")
|
|
53
|
+
graph/
|
|
54
|
+
requests.ts # catalog of every SDK transport request (graph + OS)
|
|
55
|
+
explorer-view.tsx # run real requests; mock fallback when unauthorized
|
|
56
|
+
profile/
|
|
57
|
+
use-profile.ts # os.profile fetch (+ operatorLabel helper)
|
|
58
|
+
profile-view.tsx # real profile data + error state
|
|
59
|
+
```
|
|
18
60
|
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
21
65
|
|
|
22
66
|
---
|
|
23
67
|
|
|
@@ -30,85 +74,181 @@ pnpm build # → dist/
|
|
|
30
74
|
pnpm bundle # → {{APP_NAME}}.zip
|
|
31
75
|
```
|
|
32
76
|
|
|
33
|
-
`.env
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
VITE_APP_ID=your-app-id
|
|
38
|
-
VITE_ENABLE_DEV_AUTH=true
|
|
39
|
-
DEV_AUTH_HOST_URL=https://your-host
|
|
40
|
-
DEV_SESSION_KEY=
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Standalone host-backed dev sessions are opt-in. Without `VITE_ENABLE_DEV_AUTH=true`, local `pnpm dev` runs outside the host iframe as a preview shell only and will not call the dev-session flow. Run `bardioc login` once, then `pnpm dev` will mint and refresh `DEV_SESSION_KEY` automatically.
|
|
77
|
+
`.env` (see `.env.example`): set `VITE_APP_ID` (from the BardiocOS AppStore). For standalone
|
|
78
|
+
host-backed dev, set `VITE_ENABLE_DEV_AUTH=true` + `DEV_AUTH_HOST_URL`, run `bardioc login` once,
|
|
79
|
+
then `npm run dev` mints/refreshes `DEV_SESSION_KEY` automatically. Without it, `npm run dev` is a
|
|
80
|
+
local preview shell (no transport) — the dashboard renders only inside the host or dev session.
|
|
44
81
|
|
|
45
82
|
---
|
|
46
83
|
|
|
47
|
-
##
|
|
84
|
+
## UI — `@bardioc/ui`
|
|
48
85
|
|
|
49
|
-
**
|
|
50
|
-
|
|
86
|
+
Tailwind v4 + the Bardioc design system. **Never restyle with raw hex/oklch** — use the theme
|
|
87
|
+
tokens (`bg-background`, `text-foreground`, `text-muted-foreground`, `bg-card`, `border-border`,
|
|
88
|
+
`bg-primary`, and chart tokens `var(--chart-1..5)`). Theme is wired in `src/index.css`:
|
|
51
89
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
```css
|
|
91
|
+
@import 'tailwindcss';
|
|
92
|
+
@import '@bardioc/ui/styles/theme.css';
|
|
93
|
+
@import '@bardioc/ui/styles/components.css';
|
|
94
|
+
@source './**/*.{ts,tsx}';
|
|
56
95
|
```
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
Access OS resources via `bridge.transport.os.*`:
|
|
97
|
+
Import components from the public subpaths (never reach into `@bardioc/ui/src`):
|
|
60
98
|
|
|
61
99
|
```typescript
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
import {
|
|
101
|
+
Badge,
|
|
102
|
+
BadgeStatus,
|
|
103
|
+
Button,
|
|
104
|
+
Card,
|
|
105
|
+
Combobox,
|
|
106
|
+
CheckboxGroup,
|
|
107
|
+
ConnectedLanguageSwitcher,
|
|
108
|
+
DataTable,
|
|
109
|
+
Empty,
|
|
110
|
+
FilterDropdown,
|
|
111
|
+
Kbd,
|
|
112
|
+
SidebarBasic,
|
|
113
|
+
SidebarContainedProvider,
|
|
114
|
+
SortableColumnHeader,
|
|
115
|
+
Toaster,
|
|
116
|
+
TruncatedCellText,
|
|
117
|
+
toast,
|
|
118
|
+
} from '@bardioc/ui/components';
|
|
119
|
+
import { GenericMetrics } from '@bardioc/ui/components/metrics';
|
|
120
|
+
import { BarChartMultiple } from '@bardioc/ui/components/charts';
|
|
121
|
+
import type { ChartConfig } from '@bardioc/ui/shadcn/chart';
|
|
122
|
+
import type { TReactTable } from '@bardioc/ui/types'; // TReactTable.ColumnDef<Row>
|
|
64
123
|
```
|
|
65
124
|
|
|
66
|
-
|
|
125
|
+
Prefer a Bardioc component over a raw HTML element. Icons come from `lucide-react`. The
|
|
126
|
+
sidebar uses `SidebarBasic` inside a `SidebarContainedProvider` (the contained variant for
|
|
127
|
+
OS windows); the language switcher is `ConnectedLanguageSwitcher useI18n={useI18n}`.
|
|
67
128
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## i18n — `@bardioc/i18n`
|
|
72
132
|
|
|
73
|
-
|
|
133
|
+
All user-facing strings go through `t()`. Locales: `en`, `de` (`@bardioc/types`).
|
|
74
134
|
|
|
75
135
|
```typescript
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
136
|
+
import { useTranslation } from './i18n';
|
|
137
|
+
|
|
138
|
+
const { t } = useTranslation();
|
|
139
|
+
t('metrics.oxygen', { fallback: 'Oxygen' });
|
|
140
|
+
t('overview.updated', { fallback: 'Updated {time}', params: { time } });
|
|
80
141
|
```
|
|
81
142
|
|
|
143
|
+
- Add a string → add the key to **both** `src/locales/en.json` and `src/locales/de.json`.
|
|
144
|
+
- `AppI18nProvider syncWithOs` follows the OS language (`useOsConfig().language`); the header's
|
|
145
|
+
`ConnectedLanguageSwitcher` lets the user switch it manually.
|
|
146
|
+
- Always pass a `fallback` so a missing key degrades gracefully.
|
|
147
|
+
- `@bardioc/ui` components translate against this **same** provider — only the app's `en/de.json`
|
|
148
|
+
are loaded (the base `@bardioc/i18n` bundle is not merged). So shared-component keys must live in
|
|
149
|
+
the app locales: `common.button.previous/next` and the `genericTable.*` set (DataTable pagination
|
|
150
|
+
"1 to 5 of 5", Previous/Next). Without them those strings silently fall back to English.
|
|
151
|
+
|
|
82
152
|
---
|
|
83
153
|
|
|
84
|
-
##
|
|
154
|
+
## Persistence — app-scoped IndexedDB
|
|
85
155
|
|
|
86
|
-
|
|
156
|
+
The SDK gives every app a sandboxed IndexedDB via `bridge.idb(storeName)`. Requires the
|
|
157
|
+
`"indexdb"` permission (already in `public/app-manifest.json`).
|
|
87
158
|
|
|
88
159
|
```typescript
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
160
|
+
const store = bridge.idb('climate');
|
|
161
|
+
await store.put('state', value);
|
|
162
|
+
const saved = await store.get<ClimateState>('state');
|
|
163
|
+
await store.query({ prefix: 'log:', limit: 50 }); // history — see use-climate.ts
|
|
93
164
|
```
|
|
94
165
|
|
|
95
|
-
|
|
166
|
+
The Overview shows a live "Persisted to IndexedDB · Saved … · N snapshots" indicator so the
|
|
167
|
+
write path is visible. For small key-value state, `bridge.storage.get/set/delete` (permission
|
|
168
|
+
`"storage"`) is simpler.
|
|
96
169
|
|
|
97
|
-
|
|
98
|
-
function MyComponent() {
|
|
99
|
-
const notify = useNotify();
|
|
100
|
-
notify('Done', 'success');
|
|
101
|
-
}
|
|
102
|
-
```
|
|
170
|
+
---
|
|
103
171
|
|
|
104
|
-
|
|
172
|
+
## SDK API
|
|
105
173
|
|
|
106
174
|
```typescript
|
|
107
|
-
|
|
175
|
+
const bridge = useSdk(); // host bridge (only inside the host/dev session)
|
|
176
|
+
const notify = useAppNotify(); // OS host notification + local toast (src/use-app-notify.ts)
|
|
177
|
+
const osConfig = useOsConfig(); // { theme, language, instanceId, ... }
|
|
108
178
|
```
|
|
109
179
|
|
|
110
|
-
|
|
180
|
+
**Graph** — `bridge.transport.graph.*`: `get(id)`, `getByXid`, `query(lucene, opts)`,
|
|
181
|
+
`gremlin(rootId, query)`, `create`, `update`, `connect`, `timeseries.get/query`. Note: `query`
|
|
182
|
+
resolves to a `{ items: [...] }` envelope at runtime (not a bare array) — normalize before
|
|
183
|
+
indexing, and escape `/` in Lucene as `\/` (`ogit\/_type:"…"`). Hard-coded ids/types won't exist
|
|
184
|
+
in every instance: discover them from a wildcard `query('*')` first (see `graph/requests.ts`).
|
|
185
|
+
|
|
186
|
+
**OS** — `bridge.transport.os.*`: `profile.get()`, `organization.getStructure()`, `applications.list()`.
|
|
187
|
+
|
|
188
|
+
**Live events** — `useGraphEvents({ types }, handler)` (`@bardioc/app-sdk/react`) opens a scoped
|
|
189
|
+
graph-event SSE stream while mounted (permission `"events"`). Each `handler` receives a
|
|
190
|
+
`GraphEvent` (`id`, `type`, `timestamp`, `body?`). See `events/live-events-view.tsx`. No events
|
|
191
|
+
arrive outside a valid session — render an empty state.
|
|
192
|
+
|
|
193
|
+
**OS menu** — `useCommandMenu(commands, menu)` (`@bardioc/app-sdk/react`) pushes an app menu tree
|
|
194
|
+
into the host menu bar. `commands` are `{ id, title, checked?, handler }`; `menu` is `SdkMenuRoot[]`
|
|
195
|
+
where each root has `label`, optional `kind: 'app'`, and `items` of `{ type: 'command', commandId,
|
|
196
|
+
label?, leftIcon? }`. `leftIcon` is a lucide export name (string) the host resolves. See
|
|
197
|
+
`shell/app-shell.tsx` — view switching is mirrored here as checkable commands.
|
|
198
|
+
|
|
199
|
+
The **Graph Explorer** view runs the full catalog in `graph/requests.ts` against the live
|
|
200
|
+
transport. The graph presets issue real calls — a wildcard `query('*')`, a query by the type of a
|
|
201
|
+
real node, a node `get`, and a Gremlin `both()` neighbor traversal. The typed/get/gremlin presets
|
|
202
|
+
each pull a real id and type from a wildcard query first (`sampleNodes` → `toItems`), so they
|
|
203
|
+
return live data instead of empties. Network calls fail (401/timeout) outside a valid session —
|
|
204
|
+
always `try/catch` and fall back (see `use-climate.ts` and `explorer-view.tsx`, which show
|
|
205
|
+
simulated data).
|
|
111
206
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Manifest — `public/app-manifest.json`
|
|
210
|
+
|
|
211
|
+
The starter ships a full manifest as a showcase of every supported field (validated by
|
|
212
|
+
`validateManifest` in `@bardioc/app-sdk`):
|
|
213
|
+
|
|
214
|
+
- `permissions`: `notify`, `transport`, `indexdb`, `events` (the APIs this app actually uses).
|
|
215
|
+
Other valid values: `storage` (host-deferred) and the deprecated `kernel-proxy` — do not add
|
|
216
|
+
`kernel-proxy` for hosted apps.
|
|
217
|
+
- `window`: `size` (`xs|sm|md|lg|xl` or a custom `{ width, height, minWidth?, minHeight? }`),
|
|
218
|
+
`resizable`, `maximizable`, `minimizable`, `customScroll`, `centered`, `isHeaderless`,
|
|
219
|
+
`isInstanceAware`, `canHaveMultipleWindows`.
|
|
220
|
+
- Metadata: `author`, `categories`, `showInDock`, `showInDockAppsMenu`.
|
|
221
|
+
|
|
222
|
+
Trim any field you don't need — only `manifestVersion`, `id`, `name`, `version` are required.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## How to…
|
|
227
|
+
|
|
228
|
+
- **Add a metric tile** → add a `MetricDef` (key, icon, unit, color) to `METRICS` in `types.ts`,
|
|
229
|
+
add the field to `ClimateReading` + the generator, and add `metrics.*` strings to `src/locales/{en,de}.json`.
|
|
230
|
+
- **Add a view** → add a `ViewId` (`climate/types.ts`), a `NAV` entry + `case` in `shell/app-shell.tsx`,
|
|
231
|
+
a `nav.*` string in `src/locales/{en,de}.json`, and the view component. The `NAV` entry (with its
|
|
232
|
+
`iconName`) is auto-wired into both the sidebar and the OS `View` menu.
|
|
233
|
+
- **Add an OS menu command** → add an `AppCommand` (`{ id, title, handler }`) and a matching
|
|
234
|
+
`{ type: 'command', commandId }` item under a `SdkMenuRoot` in `app-shell.tsx`; set `checked` for
|
|
235
|
+
toggles. Add any `menu.*` label to `src/locales/{en,de}.json`.
|
|
236
|
+
- **Add a table column** → push a `TReactTable.ColumnDef<Station>` in `stations-view.tsx`.
|
|
237
|
+
- **Add an SDK request** → add an entry to `SDK_REQUESTS` in `graph/requests.ts` (it shows in the Explorer).
|
|
238
|
+
- **Use real data** → replace `generateState()` with `bridge.transport.graph.*` calls in
|
|
239
|
+
`use-climate.ts`; keep the IndexedDB write so it persists across reloads.
|
|
240
|
+
- **Read the manifest** → `fetch(\`${import.meta.env.BASE_URL}app-manifest.json\`)`(served at the
|
|
241
|
+
Vite base in dev and inside the host). See`shell/about-dialog.tsx` — it shows version, id,
|
|
242
|
+
description, and permissions straight from the manifest.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Anti-patterns
|
|
247
|
+
|
|
248
|
+
| ❌ Don't | ✅ Do |
|
|
249
|
+
| -------------------------------------------- | ----------------------------------------------------- |
|
|
250
|
+
| `bridge.notify(...)` in a component | `const notify = useNotify()` |
|
|
251
|
+
| `<AppSdkProvider>` rendered outside the host | gate on `isInsideIframe()` / dev session (`main.tsx`) |
|
|
252
|
+
| raw `#1a1a1a` / `oklch(...)` colors | theme tokens (`bg-card`, `var(--chart-1)`) |
|
|
253
|
+
| hard-code a label string | `t('key', { fallback })` in `en` **and** `de` |
|
|
254
|
+
| reach into `@bardioc/ui/src/...` | import from `@bardioc/ui/components` etc. |
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"check-types": "tsc --noEmit",
|
|
13
13
|
"changeset": "changeset",
|
|
14
14
|
"release:status": "changeset status --verbose",
|
|
15
|
-
"release:version": "changeset version &&
|
|
15
|
+
"release:version": "changeset version && npm install",
|
|
16
16
|
"release:publish": "changeset publish"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@bardioc/app-sdk": "latest",
|
|
20
|
+
"@bardioc/i18n": "^0.1.6",
|
|
21
|
+
"@bardioc/types": "^0.3.0",
|
|
22
|
+
"@bardioc/ui": "^0.2.11",
|
|
23
|
+
"lucide-react": "^0.544.0",
|
|
20
24
|
"react": "19.2.6",
|
|
21
25
|
"react-dom": "19.2.6"
|
|
22
26
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifestVersion": 2,
|
|
3
|
+
"id": "{{APP_NAME}}",
|
|
4
|
+
"name": "{{DISPLAY_NAME}}",
|
|
5
|
+
"description": "{{DISPLAY_NAME}} — a Bardioc hosted app",
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"entry": "index.html",
|
|
8
|
+
"icon": "icon.svg",
|
|
9
|
+
"author": { "name": "{{DISPLAY_NAME}} Team" },
|
|
10
|
+
"categories": ["productivity"],
|
|
11
|
+
"permissions": ["notify", "transport", "indexdb", "events"],
|
|
12
|
+
"showInDock": true,
|
|
13
|
+
"showInDockAppsMenu": true,
|
|
14
|
+
"window": {
|
|
15
|
+
"size": "lg",
|
|
16
|
+
"resizable": true,
|
|
17
|
+
"maximizable": true,
|
|
18
|
+
"minimizable": true,
|
|
19
|
+
"customScroll": false,
|
|
20
|
+
"centered": false,
|
|
21
|
+
"isHeaderless": false,
|
|
22
|
+
"isInstanceAware": false,
|
|
23
|
+
"canHaveMultipleWindows": false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,141 +1,19 @@
|
|
|
1
1
|
import { isDevStandalone, isInsideIframe } from '@bardioc/app-sdk/dev';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Onboarding } from './auth/onboarding';
|
|
3
|
+
import { DashboardProvider } from './dashboard-provider';
|
|
4
|
+
import { AppShell } from './shell/app-shell';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| { status: 'success'; request: string; data: unknown }
|
|
9
|
-
| { status: 'error'; request: string; message: string };
|
|
10
|
-
|
|
11
|
-
function SdkDemo() {
|
|
12
|
-
const bridge = useSdk();
|
|
13
|
-
const notify = useNotify();
|
|
14
|
-
const [result, setResult] = useState<ResultState>({ status: 'idle' });
|
|
15
|
-
const [running, setRunning] = useState(false);
|
|
16
|
-
|
|
17
|
-
async function runGetProfile() {
|
|
18
|
-
if (running) return;
|
|
19
|
-
setRunning(true);
|
|
20
|
-
|
|
21
|
-
const request = 'transport.os.profile.get()';
|
|
22
|
-
setResult({ status: 'loading', request });
|
|
23
|
-
|
|
24
|
-
const startTime = Date.now();
|
|
25
|
-
notify('Getting user profile', 'info');
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const data = await bridge.transport.os.profile.get();
|
|
29
|
-
|
|
30
|
-
const duration = Date.now() - startTime;
|
|
31
|
-
notify(`Profile loaded (${duration}ms)`, 'success');
|
|
32
|
-
setResult({ status: 'success', request, data });
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
-
notify(`Failed: ${message}`, 'error');
|
|
36
|
-
setResult({ status: 'error', request, message });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
setRunning(false);
|
|
40
|
-
}
|
|
6
|
+
export function App() {
|
|
7
|
+
const authorized =
|
|
8
|
+
isInsideIframe() || (import.meta.env.VITE_ENABLE_DEV_AUTH === 'true' && isDevStandalone());
|
|
41
9
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
setResult({
|
|
45
|
-
status: 'success',
|
|
46
|
-
request: 'SDK_NOTIFY',
|
|
47
|
-
data: { ok: true, message: 'Notification sent to OS' },
|
|
48
|
-
});
|
|
10
|
+
if (!authorized) {
|
|
11
|
+
return <Onboarding />;
|
|
49
12
|
}
|
|
50
13
|
|
|
51
14
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
<p className="text-muted-foreground m-0 text-xs leading-5">
|
|
56
|
-
Minimal hosted-app demo for API transport and SDK APIs.
|
|
57
|
-
</p>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div className="flex flex-wrap gap-2">
|
|
61
|
-
<button
|
|
62
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 rounded px-3 py-1 text-xs font-medium disabled:cursor-not-allowed"
|
|
63
|
-
onClick={() => void runGetProfile()}
|
|
64
|
-
disabled={running}
|
|
65
|
-
>
|
|
66
|
-
Get Profile
|
|
67
|
-
</button>
|
|
68
|
-
<button
|
|
69
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 rounded px-3 py-1 text-xs font-medium disabled:cursor-not-allowed"
|
|
70
|
-
onClick={handleNotify}
|
|
71
|
-
disabled={running}
|
|
72
|
-
>
|
|
73
|
-
Send Notification OS
|
|
74
|
-
</button>
|
|
75
|
-
<button
|
|
76
|
-
className="border-border text-muted-foreground hover:text-foreground rounded border px-3 py-1 text-xs"
|
|
77
|
-
onClick={() => setResult({ status: 'idle' })}
|
|
78
|
-
disabled={running}
|
|
79
|
-
>
|
|
80
|
-
Clear
|
|
81
|
-
</button>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<div className="border-border bg-background flex min-h-0 flex-1 flex-col overflow-hidden rounded border">
|
|
85
|
-
{result.status === 'idle' && (
|
|
86
|
-
<div className="text-muted-foreground flex h-full items-center justify-center p-3 text-center text-xs leading-5">
|
|
87
|
-
Run a query to see results. If the host session is missing, the transport route will return 401.
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
{result.status === 'loading' && (
|
|
91
|
-
<div className="flex h-full flex-col items-center justify-center gap-2 p-3">
|
|
92
|
-
<div className="text-muted-foreground font-mono text-xs">{result.request}</div>
|
|
93
|
-
<div className="text-muted-foreground text-xs">Loading…</div>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
|
-
{result.status === 'error' && (
|
|
97
|
-
<div className="flex h-full flex-col gap-2 p-3 text-xs">
|
|
98
|
-
{result.request && (
|
|
99
|
-
<div className="text-muted-foreground border-border border-b pb-2 font-mono text-xs">
|
|
100
|
-
{result.request}
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
|
-
<div className="text-destructive whitespace-pre-wrap break-all">{result.message}</div>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
{result.status === 'success' && (
|
|
107
|
-
<div className="flex h-full flex-col gap-0 overflow-hidden">
|
|
108
|
-
<div className="border-border text-muted-foreground shrink-0 border-b px-3 py-1 font-mono text-xs">
|
|
109
|
-
{result.request}
|
|
110
|
-
</div>
|
|
111
|
-
<pre className="text-foreground flex-1 overflow-auto p-3 text-[11px] leading-5">
|
|
112
|
-
{JSON.stringify(result.data, null, 2)}
|
|
113
|
-
</pre>
|
|
114
|
-
</div>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
15
|
+
<DashboardProvider>
|
|
16
|
+
<AppShell />
|
|
17
|
+
</DashboardProvider>
|
|
118
18
|
);
|
|
119
19
|
}
|
|
120
|
-
|
|
121
|
-
export function App() {
|
|
122
|
-
const showSdkDemo = isInsideIframe() || (import.meta.env.VITE_ENABLE_DEV_AUTH === 'true' && isDevStandalone());
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div className="bg-background text-foreground min-h-screen">
|
|
126
|
-
{showSdkDemo ? (
|
|
127
|
-
<SdkDemo />
|
|
128
|
-
) : (
|
|
129
|
-
<div className="mx-auto flex min-h-screen w-full max-w-3xl flex-col gap-4 p-4">
|
|
130
|
-
<h1 className="m-0 text-2xl font-semibold">Hello Bardioc App</h1>
|
|
131
|
-
<p className="text-muted-foreground m-0 max-w-2xl text-xs leading-5">
|
|
132
|
-
Open this app inside Bardioc OS to use transport requests and send host notifications.
|
|
133
|
-
</p>
|
|
134
|
-
<div className="border-border bg-muted/20 rounded-xl border p-3 text-xs leading-5">
|
|
135
|
-
Outside the host iframe this page is only a local preview shell.
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isInsideIframe } from '@bardioc/app-sdk/dev';
|
|
2
|
+
import { Button, Empty, Kbd } from '@bardioc/ui/components';
|
|
3
|
+
import { PlugZap } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from '../i18n';
|
|
5
|
+
|
|
6
|
+
function DetectedRow({ label, ok }: Readonly<{ label: string; ok: boolean }>) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center justify-between gap-4">
|
|
10
|
+
<span className="text-muted-foreground">{label}</span>
|
|
11
|
+
<span className={ok ? 'text-success' : 'text-muted-foreground'}>
|
|
12
|
+
{ok ? t('auth.yes', { fallback: 'yes' }) : t('auth.no', { fallback: 'no' })}
|
|
13
|
+
</span>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Onboarding() {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const insideHost = isInsideIframe();
|
|
21
|
+
const devAuth = import.meta.env.VITE_ENABLE_DEV_AUTH === 'true';
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Empty
|
|
25
|
+
fullPage
|
|
26
|
+
icon={PlugZap}
|
|
27
|
+
iconVariant="chip"
|
|
28
|
+
titleSize="lg"
|
|
29
|
+
title={t('auth.title', { fallback: 'Connect to Bardioc OS' })}
|
|
30
|
+
description={t('auth.description', {
|
|
31
|
+
fallback: 'This dashboard reads live data through the Bardioc SDK transport.',
|
|
32
|
+
})}
|
|
33
|
+
actions={
|
|
34
|
+
<Button onClick={() => globalThis.location.reload()}>
|
|
35
|
+
{t('auth.retry', { fallback: 'Reload' })}
|
|
36
|
+
</Button>
|
|
37
|
+
}
|
|
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 a dev session:' })}
|
|
42
|
+
</p>
|
|
43
|
+
<ol className="text-muted-foreground list-decimal space-y-2 pl-4">
|
|
44
|
+
<li>{t('auth.step1', { fallback: 'Set VITE_ENABLE_DEV_AUTH=true in .env' })}</li>
|
|
45
|
+
<li>
|
|
46
|
+
<div>{t('auth.step2', { fallback: 'Authorize once in the app folder:' })}</div>
|
|
47
|
+
<Kbd>bardioc login</Kbd>
|
|
48
|
+
</li>
|
|
49
|
+
<li>
|
|
50
|
+
<div>{t('auth.step3', { fallback: 'Start the dev server:' })}</div>
|
|
51
|
+
<Kbd>pnpm dev</Kbd>
|
|
52
|
+
</li>
|
|
53
|
+
</ol>
|
|
54
|
+
<div className="border-border mt-3 space-y-1 border-t pt-3">
|
|
55
|
+
<p className="text-foreground font-medium">
|
|
56
|
+
{t('auth.detectedTitle', { fallback: 'Detected environment' })}
|
|
57
|
+
</p>
|
|
58
|
+
<DetectedRow
|
|
59
|
+
label={t('auth.insideHost', { fallback: 'Inside host iframe' })}
|
|
60
|
+
ok={insideHost}
|
|
61
|
+
/>
|
|
62
|
+
<DetectedRow label={t('auth.devAuth', { fallback: 'Dev auth enabled' })} ok={devAuth} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</Empty>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ClimateReading, ClimateState, Condition, ForecastDay, Station } from './types';
|
|
2
|
+
|
|
3
|
+
const STATIONS: Array<Pick<Station, 'id' | 'name' | 'region'>> = [
|
|
4
|
+
{ id: 'lagoon', name: 'Lagoon Station', region: 'Equatorial Belt' },
|
|
5
|
+
{ id: 'verdant', name: 'Verdant Ridge', region: 'Highlands' },
|
|
6
|
+
{ id: 'aurora', name: 'Aurora Flats', region: 'Polar Rim' },
|
|
7
|
+
{ id: 'cinder', name: 'Cinder Basin', region: 'Volcanic Plains' },
|
|
8
|
+
{ id: 'mistral', name: 'Mistral Cove', region: 'Coastal Shelf' },
|
|
9
|
+
];
|
|
10
|
+
const CONDITIONS: Condition[] = ['sunny', 'cloudy', 'rainy'];
|
|
11
|
+
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
12
|
+
|
|
13
|
+
function pick<T>(items: readonly T[]): T {
|
|
14
|
+
return items[Math.floor(Math.random() * items.length)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function round(value: number, decimals: number): number {
|
|
18
|
+
const factor = 10 ** decimals;
|
|
19
|
+
return Math.round(value * factor) / factor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function between(min: number, max: number, decimals = 0): number {
|
|
23
|
+
return round(min + Math.random() * (max - min), decimals);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Sync drifts each reading slightly around its reference value (percent of reference).
|
|
27
|
+
const SYNC_DRIFT_PCT = { temperature: 0.1, humidity: 1, windSpeed: 2 } as const;
|
|
28
|
+
|
|
29
|
+
function drift(reference: number, pct: number, decimals: number): number {
|
|
30
|
+
return round(reference + reference * (Math.random() * 2 - 1) * (pct / 100), decimals);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function syncReading(reference: ClimateReading): ClimateReading {
|
|
34
|
+
return {
|
|
35
|
+
oxygen: reference.oxygen,
|
|
36
|
+
vegetation: reference.vegetation,
|
|
37
|
+
temperature: drift(reference.temperature, SYNC_DRIFT_PCT.temperature, 1),
|
|
38
|
+
humidity: drift(reference.humidity, SYNC_DRIFT_PCT.humidity, 0),
|
|
39
|
+
windSpeed: drift(reference.windSpeed, SYNC_DRIFT_PCT.windSpeed, 0),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildForecast(): ForecastDay[] {
|
|
44
|
+
return DAYS.map(day => {
|
|
45
|
+
const sunny = between(2, 11);
|
|
46
|
+
const cloudy = between(1, 14 - sunny);
|
|
47
|
+
return { day, sunny, cloudy, rainy: Math.max(0, 14 - sunny - cloudy) };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildStation(base: Pick<Station, 'id' | 'name' | 'region'>): Station {
|
|
52
|
+
const reading: ClimateReading = {
|
|
53
|
+
oxygen: between(18, 23, 1),
|
|
54
|
+
temperature: between(-4, 32, 1),
|
|
55
|
+
humidity: between(30, 95),
|
|
56
|
+
windSpeed: between(3, 48),
|
|
57
|
+
vegetation: between(12, 88),
|
|
58
|
+
};
|
|
59
|
+
return { ...base, condition: pick(CONDITIONS), reading, reference: { ...reading } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateState(
|
|
63
|
+
activeStationId?: string,
|
|
64
|
+
source: ClimateState['source'] = 'mock'
|
|
65
|
+
): ClimateState {
|
|
66
|
+
const stations = STATIONS.map(buildStation);
|
|
67
|
+
const active = stations.find(station => station.id === activeStationId) ?? stations[0];
|
|
68
|
+
return {
|
|
69
|
+
stations,
|
|
70
|
+
activeStationId: active.id,
|
|
71
|
+
forecast: buildForecast(),
|
|
72
|
+
updatedAt: Date.now(),
|
|
73
|
+
source,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A sync keeps each station's identity, forecast and frozen metrics, drifting only
|
|
78
|
+
// humidity, wind speed and surface temperature around their reference values.
|
|
79
|
+
export function syncState(
|
|
80
|
+
prev: ClimateState,
|
|
81
|
+
source: ClimateState['source'] = 'mock'
|
|
82
|
+
): ClimateState {
|
|
83
|
+
return {
|
|
84
|
+
...prev,
|
|
85
|
+
stations: prev.stations.map(station => {
|
|
86
|
+
const reference = station.reference ?? station.reading;
|
|
87
|
+
return { ...station, reference, reading: syncReading(reference) };
|
|
88
|
+
}),
|
|
89
|
+
updatedAt: Date.now(),
|
|
90
|
+
source,
|
|
91
|
+
};
|
|
92
|
+
}
|