@bardioc/create-bardioc-app 0.4.0 → 0.5.2

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 (46) hide show
  1. package/README.md +7 -13
  2. package/bin/create.mjs +4 -8
  3. package/package.json +1 -1
  4. package/src/scaffold.js +2 -2
  5. package/templates/_base/.changeset/README.md +3 -3
  6. package/templates/_base/.claude/commands/changeset-app.md +5 -5
  7. package/templates/_base/.claude/commands/refresh-bundle.md +5 -5
  8. package/templates/_base/README.md +10 -10
  9. package/templates/angular/package.json +6 -6
  10. package/templates/nextjs/.env.example +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/.env.example +1 -1
  16. package/templates/vite/CLAUDE.md +198 -59
  17. package/templates/vite/package.json +7 -3
  18. package/templates/vite/public/app-manifest.json +25 -0
  19. package/templates/vite/src/App.tsx +11 -133
  20. package/templates/vite/src/auth/onboarding.tsx +67 -0
  21. package/templates/vite/src/climate/climate-data.ts +92 -0
  22. package/templates/vite/src/climate/conditions-chart.tsx +38 -0
  23. package/templates/vite/src/climate/location-card.tsx +50 -0
  24. package/templates/vite/src/climate/location-select.tsx +26 -0
  25. package/templates/vite/src/climate/metrics-grid.tsx +22 -0
  26. package/templates/vite/src/climate/overview-view.tsx +52 -0
  27. package/templates/vite/src/climate/stations-view.tsx +149 -0
  28. package/templates/vite/src/climate/types.ts +93 -0
  29. package/templates/vite/src/climate/use-climate.ts +123 -0
  30. package/templates/vite/src/dashboard-provider.tsx +27 -0
  31. package/templates/vite/src/events/live-events-view.tsx +74 -0
  32. package/templates/vite/src/graph/explorer-view.tsx +118 -0
  33. package/templates/vite/src/graph/requests.ts +142 -0
  34. package/templates/vite/src/i18n.tsx +68 -0
  35. package/templates/vite/src/index.css +12 -64
  36. package/templates/vite/src/locales/de.json +141 -0
  37. package/templates/vite/src/locales/en.json +141 -0
  38. package/templates/vite/src/main.tsx +10 -4
  39. package/templates/vite/src/profile/profile-view.tsx +75 -0
  40. package/templates/vite/src/profile/use-profile.ts +52 -0
  41. package/templates/vite/src/shell/about-dialog.tsx +129 -0
  42. package/templates/vite/src/shell/app-shell.tsx +160 -0
  43. package/templates/vite/src/shell/header.tsx +39 -0
  44. package/templates/vite/src/use-app-notify.ts +23 -0
  45. package/templates/vite/src/vite-env.d.ts +5 -5
  46. package/templates/vue/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ {
2
+ "app": { "title": "Weather Dashboard", "tagline": "Live planetary readings" },
3
+ "nav": {
4
+ "overview": "Overview",
5
+ "stations": "Stations",
6
+ "explorer": "Graph Explorer",
7
+ "events": "Live Events",
8
+ "profile": "Profile"
9
+ },
10
+ "menu": {
11
+ "view": "View",
12
+ "about": "About this app"
13
+ },
14
+ "about": {
15
+ "title": "About this app",
16
+ "version": "Version",
17
+ "id": "App ID",
18
+ "description": "Description",
19
+ "permissions": "Permissions",
20
+ "error": "Could not read app-manifest.json",
21
+ "source": "Read live from public/app-manifest.json"
22
+ },
23
+ "genericTable": {
24
+ "filterPlaceholder": "Filter results...",
25
+ "itemLabel": "rows",
26
+ "showing": "{from} to {to} of {total}",
27
+ "columns": "Columns",
28
+ "noResults": "No results found.",
29
+ "goToPreviousPage": "Go to previous page",
30
+ "goToNextPage": "Go to next page"
31
+ },
32
+ "common": { "button": { "previous": "Previous", "next": "Next" } },
33
+ "header": {
34
+ "search": "Search stations…",
35
+ "sync": "Sync",
36
+ "syncing": "Syncing…",
37
+ "location": "Station",
38
+ "savedAt": "Saved {time}",
39
+ "neverSaved": "Not yet saved"
40
+ },
41
+ "auth": {
42
+ "title": "Connect to Bardioc OS",
43
+ "description": "This dashboard reads live data through the Bardioc SDK transport. It runs when opened inside Bardioc OS, or standalone with a CLI dev session.",
44
+ "stepsTitle": "Run standalone with a dev session:",
45
+ "step1": "Set VITE_ENABLE_DEV_AUTH=true and DEV_AUTH_HOST_URL=<your-host> in .env",
46
+ "step2": "Authorize once in the app folder:",
47
+ "step3": "Start the dev server — the session key is minted and refreshed automatically:",
48
+ "detectedTitle": "Detected environment",
49
+ "insideHost": "Inside host iframe",
50
+ "devAuth": "Dev auth enabled",
51
+ "yes": "yes",
52
+ "no": "no",
53
+ "retry": "Reload"
54
+ },
55
+ "overview": {
56
+ "operator": "Operator",
57
+ "noOperator": "Operator unavailable",
58
+ "updated": "Updated {time}",
59
+ "persistence": "Persisted to IndexedDB",
60
+ "notPersisted": "Preview — not persisted (open inside Bardioc OS to persist)",
61
+ "historyOne": "{count} snapshot in history",
62
+ "historyOther": "{count} snapshots in history",
63
+ "source": "Source"
64
+ },
65
+ "conditions": { "sunny": "Sunny", "cloudy": "Cloudy", "rainy": "Rainy" },
66
+ "source": { "graph": "Bardioc graph", "mock": "Simulated" },
67
+ "metrics": {
68
+ "oxygen": "Oxygen",
69
+ "temperature": "Surface temp",
70
+ "humidity": "Humidity",
71
+ "windSpeed": "Wind speed",
72
+ "vegetation": "Vegetation",
73
+ "oxygenHint": "Atmospheric mix",
74
+ "temperatureHint": "Ground level",
75
+ "humidityHint": "Relative",
76
+ "windSpeedHint": "Sustained",
77
+ "vegetationHint": "Surface cover"
78
+ },
79
+ "chart": { "title": "7-day outlook", "description": "Daylight hours by condition" },
80
+ "stations": {
81
+ "title": "Monitoring stations",
82
+ "description": "All stations reporting to this instance. Search, filter, or select one.",
83
+ "search": "Search stations…",
84
+ "filter": "Condition",
85
+ "colName": "Station",
86
+ "colRegion": "Region",
87
+ "colCondition": "Condition",
88
+ "colTemp": "Temp",
89
+ "colHumidity": "Humidity",
90
+ "colVegetation": "Vegetation",
91
+ "itemLabel": "stations",
92
+ "empty": "No stations match your filters",
93
+ "apply": "Apply",
94
+ "clear": "Clear"
95
+ },
96
+ "explorer": {
97
+ "title": "Graph Explorer",
98
+ "description": "Run real Bardioc SDK transport requests. Without a session, each request falls back to simulated data so you can still explore the shape.",
99
+ "run": "Run request",
100
+ "running": "Running…",
101
+ "empty": "Select a request and run it to see the response",
102
+ "live": "Live",
103
+ "mock": "Simulated",
104
+ "categoryGraph": "Graph",
105
+ "categoryOs": "OS",
106
+ "request": "Request",
107
+ "durationMs": "{ms} ms"
108
+ },
109
+ "profile": {
110
+ "title": "Operator profile",
111
+ "description": "Pulled live from the OS transport (os.profile).",
112
+ "name": "Name",
113
+ "displayName": "Display name",
114
+ "firstName": "First name",
115
+ "lastName": "Last name",
116
+ "email": "Email",
117
+ "id": "ID",
118
+ "organization": "Organization",
119
+ "units": "Units",
120
+ "members": "Members",
121
+ "raw": "Raw response",
122
+ "errorTitle": "Could not load profile",
123
+ "errorHint": "The transport returned an error. Authorize a session, then reload.",
124
+ "unknown": "Unknown"
125
+ },
126
+ "events": {
127
+ "title": "Live Events",
128
+ "description": "Live graph events streamed from the OS over SSE (permission \"events\").",
129
+ "emptyTitle": "Waiting for events",
130
+ "emptyHost": "No graph events yet — activity will appear here as it happens.",
131
+ "emptyStandalone": "Open inside Bardioc OS (or a dev session) to receive live events."
132
+ },
133
+ "notify": {
134
+ "synced": "Synced {count} stations",
135
+ "saved": "Saved to IndexedDB",
136
+ "stationSelected": "Active station: {name}",
137
+ "localeChanged": "Language switched to {locale}",
138
+ "queryOk": "Request ok ({ms} ms)",
139
+ "queryFailed": "Request failed — showing simulated data"
140
+ }
141
+ }
@@ -8,11 +8,13 @@ import { AppSdkProvider } from '@bardioc/app-sdk/react';
8
8
  import { StrictMode } from 'react';
9
9
  import { createRoot } from 'react-dom/client';
10
10
  import { App } from './App';
11
+ import { AppI18nProvider } from './i18n';
11
12
  import './index.css';
12
13
 
13
14
  const APP_NAME = import.meta.env.VITE_APP_NAME?.trim();
14
15
  const APP_ID = import.meta.env.VITE_APP_ID?.trim();
15
- const standaloneDevAuthEnabled = import.meta.env.DEV && import.meta.env.VITE_ENABLE_DEV_AUTH === 'true';
16
+ const standaloneDevAuthEnabled =
17
+ import.meta.env.DEV && import.meta.env.VITE_ENABLE_DEV_AUTH === 'true';
16
18
 
17
19
  if (!APP_NAME) {
18
20
  throw new Error('Missing VITE_APP_NAME for runtime');
@@ -35,10 +37,14 @@ createRoot(document.getElementById('root')!).render(
35
37
  <StrictMode>
36
38
  {useSdkProvider ? (
37
39
  <AppSdkProvider appId={APP_ID}>
38
- <App />
40
+ <AppI18nProvider syncWithOs>
41
+ <App />
42
+ </AppI18nProvider>
39
43
  </AppSdkProvider>
40
44
  ) : (
41
- <App />
45
+ <AppI18nProvider>
46
+ <App />
47
+ </AppI18nProvider>
42
48
  )}
43
49
  </StrictMode>
44
- );
50
+ );
@@ -0,0 +1,75 @@
1
+ import { Avatar, Card, Empty } from '@bardioc/ui/components';
2
+ import { UserX } from 'lucide-react';
3
+ import { useDashboard } from '../dashboard-provider';
4
+ import { useTranslation } from '../i18n';
5
+ import { operatorLabel } from './use-profile';
6
+
7
+ function Row({ label, value }: Readonly<{ label: string; value: string }>) {
8
+ return (
9
+ <div className="flex items-baseline gap-2 text-sm">
10
+ <span className="text-muted-foreground w-28 shrink-0">{label}</span>
11
+ <span className="truncate font-medium">{value}</span>
12
+ </div>
13
+ );
14
+ }
15
+
16
+ function asString(value: unknown, fallback: string): string {
17
+ return typeof value === 'string' && value.trim() ? value : fallback;
18
+ }
19
+
20
+ export function ProfileView() {
21
+ const { t } = useTranslation();
22
+ const { profile } = useDashboard();
23
+ const unknown = t('profile.unknown', { fallback: 'Unknown' });
24
+
25
+ if (profile.loading) {
26
+ return <p className="text-muted-foreground p-6 text-sm">…</p>;
27
+ }
28
+
29
+ if (profile.error || !profile.profile) {
30
+ return (
31
+ <Empty
32
+ icon={UserX}
33
+ iconVariant="chip"
34
+ title={t('profile.errorTitle', { fallback: 'Could not load profile' })}
35
+ description={profile.error ?? t('profile.errorHint', { fallback: '' })}
36
+ />
37
+ );
38
+ }
39
+
40
+ const data = profile.profile;
41
+ const name = operatorLabel(data) ?? unknown;
42
+
43
+ return (
44
+ <div className="flex flex-col gap-4">
45
+ <div>
46
+ <h2 className="text-lg font-semibold">
47
+ {t('profile.title', { fallback: 'Operator profile' })}
48
+ </h2>
49
+ <p className="text-muted-foreground text-xs">
50
+ {t('profile.description', { fallback: '' })}
51
+ </p>
52
+ </div>
53
+
54
+ <Card>
55
+ <div className="flex items-center gap-4">
56
+ <Avatar firstName={name} size="lg" />
57
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
58
+ <Row
59
+ label={t('profile.displayName', { fallback: 'Display name' })}
60
+ value={asString(data.display_name, unknown)}
61
+ />
62
+ <Row
63
+ label={t('profile.firstName', { fallback: 'First name' })}
64
+ value={asString(data.first_name, unknown)}
65
+ />
66
+ <Row
67
+ label={t('profile.lastName', { fallback: 'Last name' })}
68
+ value={asString(data.last_name, unknown)}
69
+ />
70
+ </div>
71
+ </div>
72
+ </Card>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,52 @@
1
+ import { useSdk } from '@bardioc/app-sdk/react';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ type Record_ = Record<string, unknown>;
5
+
6
+ export interface ProfileState {
7
+ loading: boolean;
8
+ profile: Record_ | null;
9
+ error: string | null;
10
+ }
11
+
12
+ // os.profile.get() returns the BFF profile in snake_case (display_name, first_name, last_name).
13
+ function profileString(profile: Record_, key: string): string {
14
+ const value = profile[key];
15
+ return typeof value === 'string' ? value.trim() : '';
16
+ }
17
+
18
+ export function operatorLabel(profile: Record_ | null): string | null {
19
+ if (!profile) return null;
20
+ const display = profileString(profile, 'display_name') || profileString(profile, 'name');
21
+ if (display) return display;
22
+ const full =
23
+ `${profileString(profile, 'first_name')} ${profileString(profile, 'last_name')}`.trim();
24
+ return full || null;
25
+ }
26
+
27
+ export function useProfile(): ProfileState {
28
+ const bridge = useSdk();
29
+ const [state, setState] = useState<ProfileState>({
30
+ loading: true,
31
+ profile: null,
32
+ error: null,
33
+ });
34
+
35
+ useEffect(() => {
36
+ let active = true;
37
+ void (async () => {
38
+ try {
39
+ const profile = (await bridge.transport.os.profile.get()) as Record_;
40
+ if (active) setState({ loading: false, profile, error: null });
41
+ } catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ if (active) setState({ loading: false, profile: null, error: message });
44
+ }
45
+ })();
46
+ return () => {
47
+ active = false;
48
+ };
49
+ }, [bridge]);
50
+
51
+ return state;
52
+ }
@@ -0,0 +1,129 @@
1
+ import { BardiocLogoIcon, Dialog } from '@bardioc/ui/components';
2
+ import { HashIcon, PackageIcon, ShieldCheckIcon, TagIcon } from 'lucide-react';
3
+ import { useEffect, useState, type ReactNode } from 'react';
4
+ import { useTranslation } from '../i18n';
5
+
6
+ interface AppManifest {
7
+ id: string;
8
+ name: string;
9
+ description: string;
10
+ version: string;
11
+ permissions?: string[];
12
+ }
13
+
14
+ interface AboutDialogProps {
15
+ open: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ }
18
+
19
+ // Reads the app's own manifest (public/app-manifest.json) at runtime. The manifest is served at
20
+ // the Vite base path, so the same code works in dev and inside the host — this is the example of
21
+ // reading metadata (name, version, permissions) from the manifest instead of hard-coding it.
22
+ async function loadManifest(): Promise<AppManifest> {
23
+ const response = await fetch(`${import.meta.env.BASE_URL}app-manifest.json`, {
24
+ cache: 'no-store',
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`Failed to load manifest: ${response.status}`);
28
+ }
29
+ return (await response.json()) as AppManifest;
30
+ }
31
+
32
+ function AboutRow({
33
+ icon,
34
+ label,
35
+ value,
36
+ }: Readonly<{ icon: ReactNode; label: string; value: string }>) {
37
+ return (
38
+ <div className="flex items-center justify-between gap-4 rounded-lg p-2">
39
+ <div className="text-foreground flex items-center gap-2">
40
+ {icon}
41
+ <span className="text-xs font-semibold">{label}</span>
42
+ </div>
43
+ <span className="text-muted-foreground truncate text-xs">{value}</span>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ export function AboutDialog({ open, onOpenChange }: Readonly<AboutDialogProps>) {
49
+ const { t } = useTranslation();
50
+ const [manifest, setManifest] = useState<AppManifest | null>(null);
51
+ const [failed, setFailed] = useState(false);
52
+
53
+ useEffect(() => {
54
+ if (!open) {
55
+ return;
56
+ }
57
+ let cancelled = false;
58
+ setFailed(false);
59
+ loadManifest()
60
+ .then(data => {
61
+ if (!cancelled) {
62
+ setManifest(data);
63
+ }
64
+ })
65
+ .catch(() => {
66
+ if (!cancelled) {
67
+ setFailed(true);
68
+ }
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [open]);
74
+
75
+ const dash = '—';
76
+
77
+ return (
78
+ <Dialog
79
+ open={open}
80
+ onOpenChange={onOpenChange}
81
+ modal
82
+ showCloseOnHeader
83
+ width={440}
84
+ title={t('about.title', { fallback: 'About this app' })}
85
+ >
86
+ <div className="flex flex-col items-center gap-4 px-2 pb-2">
87
+ <div className="flex flex-col items-center gap-2 py-2">
88
+ <BardiocLogoIcon width={48} height={48} aria-hidden="true" />
89
+ <h2 className="text-lg font-semibold">
90
+ {manifest?.name ?? t('app.title', { fallback: 'Weather Dashboard' })}
91
+ </h2>
92
+ </div>
93
+
94
+ {failed ? (
95
+ <p className="text-destructive text-sm">
96
+ {t('about.error', { fallback: 'Could not read app-manifest.json' })}
97
+ </p>
98
+ ) : (
99
+ <div className="w-full">
100
+ <AboutRow
101
+ icon={<TagIcon className="size-4" />}
102
+ label={t('about.version', { fallback: 'Version' })}
103
+ value={manifest?.version ?? dash}
104
+ />
105
+ <AboutRow
106
+ icon={<HashIcon className="size-4" />}
107
+ label={t('about.id', { fallback: 'App ID' })}
108
+ value={manifest?.id ?? dash}
109
+ />
110
+ <AboutRow
111
+ icon={<PackageIcon className="size-4" />}
112
+ label={t('about.description', { fallback: 'Description' })}
113
+ value={manifest?.description ?? dash}
114
+ />
115
+ <AboutRow
116
+ icon={<ShieldCheckIcon className="size-4" />}
117
+ label={t('about.permissions', { fallback: 'Permissions' })}
118
+ value={manifest?.permissions?.join(', ') ?? dash}
119
+ />
120
+ </div>
121
+ )}
122
+
123
+ <p className="text-muted-foreground text-center text-xs">
124
+ {t('about.source', { fallback: 'Read live from public/app-manifest.json' })}
125
+ </p>
126
+ </div>
127
+ </Dialog>
128
+ );
129
+ }
@@ -0,0 +1,160 @@
1
+ import type { SdkMenuRoot } from '@bardioc/app-sdk';
2
+ import { useCommandMenu, type AppCommand } from '@bardioc/app-sdk/react';
3
+ import {
4
+ BardiocLogoIcon,
5
+ SidebarBasic,
6
+ SidebarContainedProvider,
7
+ Toaster,
8
+ } from '@bardioc/ui/components';
9
+ import { ScrollArea } from '@bardioc/ui/shadcn';
10
+ import {
11
+ Activity,
12
+ LayoutDashboard,
13
+ Network,
14
+ Table2,
15
+ UserRound,
16
+ type LucideIcon,
17
+ } from 'lucide-react';
18
+ import { useEffect, useMemo, useRef, useState } from 'react';
19
+ import { OverviewView } from '../climate/overview-view';
20
+ import { StationsView } from '../climate/stations-view';
21
+ import type { ViewId } from '../climate/types';
22
+ import { LiveEventsView } from '../events/live-events-view';
23
+ import { ExplorerView } from '../graph/explorer-view';
24
+ import { useI18n, useTranslation } from '../i18n';
25
+ import { ProfileView } from '../profile/profile-view';
26
+ import { useAppNotify } from '../use-app-notify';
27
+ import { AboutDialog } from './about-dialog';
28
+ import { Header } from './header';
29
+
30
+ // `icon` is the sidebar's React component; `iconName` is the matching lucide export name the
31
+ // OS resolves for the top-menu entry (host calls resolveLucideIcon on the string).
32
+ const NAV: Array<{ id: ViewId; icon: LucideIcon; iconName: string; labelKey: string }> = [
33
+ { id: 'overview', icon: LayoutDashboard, iconName: 'LayoutDashboard', labelKey: 'nav.overview' },
34
+ { id: 'stations', icon: Table2, iconName: 'Table2', labelKey: 'nav.stations' },
35
+ { id: 'explorer', icon: Network, iconName: 'Network', labelKey: 'nav.explorer' },
36
+ { id: 'events', icon: Activity, iconName: 'Activity', labelKey: 'nav.events' },
37
+ { id: 'profile', icon: UserRound, iconName: 'UserRound', labelKey: 'nav.profile' },
38
+ ];
39
+
40
+ function ViewContent({ view }: Readonly<{ view: ViewId }>) {
41
+ switch (view) {
42
+ case 'overview':
43
+ return <OverviewView />;
44
+ case 'stations':
45
+ return <StationsView />;
46
+ case 'explorer':
47
+ return <ExplorerView />;
48
+ case 'events':
49
+ return <LiveEventsView />;
50
+ case 'profile':
51
+ return <ProfileView />;
52
+ default:
53
+ return null;
54
+ }
55
+ }
56
+
57
+ export function AppShell() {
58
+ const { t } = useTranslation();
59
+ const { locale } = useI18n();
60
+ const notify = useAppNotify();
61
+ const [view, setView] = useState<ViewId>('overview');
62
+ const [aboutOpen, setAboutOpen] = useState(false);
63
+
64
+ const firstLocale = useRef(true);
65
+ useEffect(() => {
66
+ if (firstLocale.current) {
67
+ firstLocale.current = false;
68
+ return;
69
+ }
70
+ notify(
71
+ t('notify.localeChanged', { fallback: 'Language switched to {locale}', params: { locale } }),
72
+ 'info'
73
+ );
74
+ }, [locale, notify, t]);
75
+
76
+ const items = NAV.map(entry => ({
77
+ name: entry.id,
78
+ label: t(entry.labelKey, { fallback: entry.id }),
79
+ icon: entry.icon,
80
+ }));
81
+
82
+ // Mirror the section switcher into the OS top menu: each view is a checkable command, so the
83
+ // active section can be changed from the host menu bar (next to About / Version) — not just
84
+ // the in-app sidebar. This is the example of OS menu integration via the SDK.
85
+ const commands = useMemo<AppCommand[]>(
86
+ () => [
87
+ {
88
+ id: 'app.about',
89
+ title: t('menu.about', { fallback: 'About this app' }),
90
+ handler: () => setAboutOpen(true),
91
+ },
92
+ ...NAV.map(entry => ({
93
+ id: `view.${entry.id}`,
94
+ title: t(entry.labelKey, { fallback: entry.id }),
95
+ checked: view === entry.id,
96
+ handler: () => setView(entry.id),
97
+ })),
98
+ ],
99
+ [t, view]
100
+ );
101
+
102
+ const menu = useMemo<SdkMenuRoot[]>(
103
+ () => [
104
+ {
105
+ label: t('app.title', { fallback: 'Weather Dashboard' }),
106
+ kind: 'app',
107
+ items: [{ type: 'command', commandId: 'app.about' }],
108
+ },
109
+ {
110
+ label: t('menu.view', { fallback: 'View' }),
111
+ items: NAV.map(entry => ({
112
+ type: 'command' as const,
113
+ commandId: `view.${entry.id}`,
114
+ label: t(entry.labelKey, { fallback: entry.id }),
115
+ leftIcon: entry.iconName,
116
+ })),
117
+ },
118
+ ],
119
+ [t]
120
+ );
121
+
122
+ useCommandMenu(commands, menu);
123
+
124
+ const header = (
125
+ <div className="flex items-center gap-2 px-2 py-1">
126
+ <BardiocLogoIcon className="size-5" />
127
+ <span className="truncate text-sm font-semibold">
128
+ {t('app.title', { fallback: 'Climate Dashboard' })}
129
+ </span>
130
+ </div>
131
+ );
132
+
133
+ return (
134
+ <SidebarContainedProvider defaultCollapsed={false}>
135
+ <div className="bg-background text-foreground flex h-full min-h-0">
136
+ <SidebarBasic
137
+ header={header}
138
+ rail={false}
139
+ items={items}
140
+ selectedItem={view}
141
+ onItemSelected={item => {
142
+ if (!item) return;
143
+ setView(item as ViewId);
144
+ }}
145
+ className="border-r"
146
+ />
147
+ <main className="flex min-w-0 flex-1 flex-col">
148
+ <Header />
149
+ <ScrollArea className="min-h-0 flex-1">
150
+ <div className="p-4">
151
+ <ViewContent view={view} />
152
+ </div>
153
+ </ScrollArea>
154
+ </main>
155
+ </div>
156
+ <AboutDialog open={aboutOpen} onOpenChange={setAboutOpen} />
157
+ <Toaster position="bottom-right" />
158
+ </SidebarContainedProvider>
159
+ );
160
+ }
@@ -0,0 +1,39 @@
1
+ import { SUPPORTED_LOCALES } from '@bardioc/types';
2
+ import { Button, ConnectedLanguageSwitcher } from '@bardioc/ui/components';
3
+ import { RefreshCw } from 'lucide-react';
4
+ import { LocationSelect } from '../climate/location-select';
5
+ import { useDashboard } from '../dashboard-provider';
6
+ import { useI18n, useTranslation } from '../i18n';
7
+
8
+ export function Header() {
9
+ const { t } = useTranslation();
10
+ const { state, syncing, refresh, setActiveStation } = useDashboard();
11
+
12
+ return (
13
+ <header className="border-border flex shrink-0 items-center justify-end gap-3 border-b px-4 py-2">
14
+ <div className="flex shrink-0 items-center gap-2">
15
+ {state?.stations && (
16
+ <LocationSelect
17
+ stations={state.stations}
18
+ value={state.activeStationId}
19
+ onSelect={setActiveStation}
20
+ />
21
+ )}
22
+ <ConnectedLanguageSwitcher
23
+ useI18n={useI18n}
24
+ supportedLocales={[...SUPPORTED_LOCALES]}
25
+ flags={{ en: 'EN', de: 'DE' }}
26
+ />
27
+ <Button
28
+ className="gap-2"
29
+ size="sm"
30
+ onClick={() => void refresh()}
31
+ disabled={syncing || !state}
32
+ >
33
+ <RefreshCw className={`size-4 ${syncing ? 'animate-spin' : ''}`} />
34
+ {t('header.sync', { fallback: 'Sync' })}
35
+ </Button>
36
+ </div>
37
+ </header>
38
+ );
39
+ }
@@ -0,0 +1,23 @@
1
+ import { isInsideIframe } from '@bardioc/app-sdk/dev';
2
+ import { useNotify } from '@bardioc/app-sdk/react';
3
+ import { toast } from '@bardioc/ui/components';
4
+ import { useCallback } from 'react';
5
+
6
+ export type NotifyLevel = 'success' | 'error' | 'info' | 'warning';
7
+
8
+ // Inside the OS shell we route to the Bardioc-native channel only (the host renders it), which
9
+ // also proves host↔app notification wiring. Standalone we fall back to a local toast.
10
+ export function useAppNotify() {
11
+ const sdkNotify = useNotify();
12
+
13
+ return useCallback(
14
+ (message: string, level: NotifyLevel = 'info') => {
15
+ if (isInsideIframe()) {
16
+ sdkNotify(message, level);
17
+ } else {
18
+ toast[level](message);
19
+ }
20
+ },
21
+ [sdkNotify]
22
+ );
23
+ }
@@ -1,11 +1,11 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  interface ImportMetaEnv {
4
- readonly VITE_APP_NAME: string
5
- readonly VITE_APP_ID: string
6
- readonly VITE_ENABLE_DEV_AUTH?: string
4
+ readonly VITE_APP_NAME: string;
5
+ readonly VITE_APP_ID: string;
6
+ readonly VITE_ENABLE_DEV_AUTH?: string;
7
7
  }
8
8
 
9
9
  interface ImportMeta {
10
- readonly env: ImportMetaEnv
11
- }
10
+ readonly env: ImportMetaEnv;
11
+ }
@@ -12,7 +12,7 @@
12
12
  "check-types": "vue-tsc --noEmit",
13
13
  "changeset": "changeset",
14
14
  "release:status": "changeset status --verbose",
15
- "release:version": "changeset version && pnpm install --lockfile-only",
15
+ "release:version": "changeset version && npm install",
16
16
  "release:publish": "changeset publish"
17
17
  },
18
18
  "dependencies": {