@deckbuilderkit/build 0.1.1 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deckbuilderkit/build",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "deckbuilder": "src/cli.ts"
22
22
  },
23
23
  "dependencies": {
24
- "@deckbuilderkit/platform": "0.1.1",
25
- "@deckbuilderkit/server-core": "0.1.1",
24
+ "@deckbuilderkit/platform": "0.1.2",
25
+ "@deckbuilderkit/server-core": "0.1.2",
26
26
  "sharp": "^0.35.5"
27
27
  }
28
28
  }
package/src/cli.ts CHANGED
@@ -15,18 +15,19 @@ const [command, ...args] = process.argv.slice(2);
15
15
  async function main(): Promise<void> {
16
16
  // Only `new` runs outside an app folder.
17
17
  if (command === 'new') {
18
- const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { name: { type: 'string' }, id: { type: 'string' } } });
18
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { name: { type: 'string' }, id: { type: 'string' }, views: { type: 'string' }, local: { type: 'boolean', default: false }, git: { type: 'boolean', default: false } } });
19
19
  const [dir] = positionals;
20
20
  if (!dir) {
21
- console.error('Usage: deckbuilder new <dir> [--name <Name>] [--id <id>]');
21
+ console.error('Usage: deckbuilder new <dir> [--views a,b] [--local] [--git] [--name <Name>] [--id <id>]');
22
22
  process.exit(1);
23
23
  }
24
- const target = await createApp({ dir, name: values.name, id: values.id }, process.cwd());
24
+ const views = values.views?.split(',').filter(Boolean);
25
+ const target = await createApp({ dir, name: values.name, id: values.id, views, local: values.local, git: values.git }, process.cwd());
25
26
  console.log(`Created ${target}
26
27
 
27
28
  Next:
29
+ cd ${dir}
28
30
  bun install
29
- cd apps/${dir}
30
31
  bun run dev`);
31
32
  return;
32
33
  }
@@ -102,9 +103,11 @@ wrote ${publicKey} (public: commit it; the exe embeds it)`);
102
103
  default:
103
104
  console.log(`Usage: deckbuilder <command>
104
105
 
105
- new <dir> [--name <Name>] [--id <id>]
106
- create apps/<dir> from the template (run from the repo root:
107
- bun run new <dir>); id = URL scheme, data folder ~/.<id>
106
+ new <dir> [--views a,b] [--local] [--git] [--name <Name>] [--id <id>]
107
+ create a standalone app in ./<dir> on the published packages,
108
+ composing the chosen views (e.g. terminal); --local links the
109
+ packages of this checkout instead; --git runs git init;
110
+ id = URL scheme, data folder ~/.<id>
108
111
  add view <id> scaffold packages/view-<id> from the view template and register it
109
112
  in the gallery (run from the repo root)
110
113
  dev [server flags] server (--dev, watch mode) + Vite dev server; e.g. dev --window, dev --app
package/src/new.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { extname, join } from 'node:path';
3
3
  import { isUrlScheme } from '@deckbuilderkit/server-core/protocol';
4
- import { workspaceRoot } from './app.ts';
5
4
 
6
5
  const TEMPLATE = join(import.meta.dir, '..', 'template');
7
6
  const BINARY = new Set(['.png', '.ico', '.woff', '.woff2']);
@@ -10,6 +9,12 @@ export interface NewAppOptions {
10
9
  dir: string;
11
10
  name?: string;
12
11
  id?: string;
12
+ /** View ids to compose, e.g. `['terminal']`. */
13
+ views?: string[];
14
+ /** Link the packages of this checkout instead of the published version. */
15
+ local?: boolean;
16
+ /** Run `git init` in the new folder. */
17
+ git?: boolean;
13
18
  }
14
19
 
15
20
  export interface NewApp {
@@ -46,13 +51,137 @@ function copyFilled(from: string, to: string, app: NewApp): void {
46
51
  }
47
52
  }
48
53
 
49
- /** Creates `apps/<dir>` in the workspace root from the template; returns its path. */
54
+ /** A view package a standalone app can compose; the options are the code the generated lists call it with. */
55
+ interface ViewSpec {
56
+ /** Package folder, `packages/<pkg>`. */
57
+ pkg: string;
58
+ web: string;
59
+ webOptions: string;
60
+ webSettings: string;
61
+ server: string;
62
+ serverOptions: string;
63
+ serverSettings: string;
64
+ }
65
+
66
+ /** Views that need nothing from the app (no client, no stores). */
67
+ export const VIEW_SPECS: Record<string, ViewSpec> = {
68
+ terminal: {
69
+ pkg: 'view-terminal',
70
+ web: 'terminalView',
71
+ webOptions: '',
72
+ webSettings: 'TerminalViewSettings',
73
+ server: 'terminalServerView',
74
+ serverOptions: "{ integrationDir: join(appHome(APP.id), 'shell-integration'), resolveCwd: async (root, path) => join(root, path) }",
75
+ serverSettings: 'TerminalServerSettings',
76
+ },
77
+ };
78
+
79
+ /** The other views take an app client (web) and stores (server) that `new` does not generate yet. */
80
+ const NEEDS_GLUE = ['explorer', 'git', 'rest', 'workbench'];
81
+
82
+ export function viewSpecs(ids: string[]): ViewSpec[] {
83
+ return ids.map((id) => {
84
+ const spec = VIEW_SPECS[id];
85
+ if (spec) return spec;
86
+ if (NEEDS_GLUE.includes(id)) throw new Error(`View "${id}" needs an app client and server stores that "new" does not generate yet.`);
87
+ throw new Error(`Unknown view "${id}"; known: ${Object.keys(VIEW_SPECS).join(', ')}.`);
88
+ });
89
+ }
90
+
91
+ const pkgName = (spec: ViewSpec, scope: string) => `${scope}/${spec.pkg}`;
92
+
93
+ /** `web/src/view-settings.ts`: the settings the chosen views add to the web `Settings`. */
94
+ export function viewSettingsSource(ids: string[], scope: string): string {
95
+ const specs = viewSpecs(ids);
96
+ const imports = specs.map((v) => `import type { ${v.webSettings} } from '${pkgName(v, scope)}/web';`);
97
+ const extend = specs.length ? ` extends ${specs.map((v) => v.webSettings).join(', ')}` : '';
98
+ return `${imports.join('\n')}${imports.length ? '\n\n' : ''}/** The settings the chosen views add; \`Settings\` extends it. */\nexport interface ViewSettings${extend} {}\n`;
99
+ }
100
+
101
+ /** `web/src/views.ts`: `createViews` over Home and the chosen views. */
102
+ export function webViewsSource(ids: string[], scope: string): string {
103
+ const specs = viewSpecs(ids);
104
+ const imports = specs.map((v) => `import { ${v.web} } from '${pkgName(v, scope)}/web';`);
105
+ const calls = specs.map((v, i) => `${v.web}({ group: 'nav.section.main', order: ${20 + i * 10}${v.webOptions ? ', ' + v.webOptions : ''} })`);
106
+ return `import { createElement } from 'react';
107
+ import { addMessages, createViews, PanelLoading, type ViewIds } from '@deckbuilderkit/ui';
108
+ ${imports.join('\n')}${imports.length ? '\n' : ''}import { homeView } from './screens/home.view';
109
+ import { settingsRegistry } from './settings';
110
+
111
+ /** The one list the nav, routes, screens, commands, settings and messages come from. */
112
+ export const views = createViews([homeView${calls.map((c) => ', ' + c).join('')}], { fallback: createElement(PanelLoading) });
113
+
114
+ export type ScreenId = ViewIds<typeof views>;
115
+
116
+ views.registerSettings(settingsRegistry);
117
+ addMessages(views.messages);
118
+ `;
119
+ }
120
+
121
+ /** `server/views.ts`: the server half of the list and the settings the views add. */
122
+ export function serverViewsSource(ids: string[], scope: string): string {
123
+ const specs = viewSpecs(ids);
124
+ const imports = specs.map((v) => `import { ${v.server}, type ${v.serverSettings} } from '${pkgName(v, scope)}/server';`);
125
+ const extend = specs.length ? ` extends ${specs.map((v) => v.serverSettings).join(', ')}` : '';
126
+ const list = specs.map((v) => `\n ${v.server}(${v.serverOptions}),`).join('');
127
+ return `import { join } from 'node:path';
128
+ import { appHome } from '@deckbuilderkit/platform';
129
+ import type { ServerViewDef } from '@deckbuilderkit/server-core';
130
+ ${imports.join('\n')}${imports.length ? '\n' : ''}import { APP } from '../app.ts';
131
+
132
+ /** The settings the chosen views add; \`Settings\` extends it. */
133
+ export interface ViewSettings${extend} {}
134
+
135
+ export const serverViews: ServerViewDef[] = [${list}${list ? '\n' : ''}];
136
+ `;
137
+ }
138
+
139
+ interface BuildPackage {
140
+ /** Package name scope, e.g. `@deckbuilderkit`. */
141
+ scope: string;
142
+ version: string;
143
+ /** The checkout's packages folder; null for an installed copy. */
144
+ packagesDir: string | null;
145
+ }
146
+
147
+ /** Scope and version come from this package's own manifest: every package is published at one version. */
148
+ export function buildPackage(): BuildPackage {
149
+ const own = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf8')) as { name: string; version: string };
150
+ const siblings = join(import.meta.dir, '..', '..');
151
+ return { scope: own.name.split('/')[0]!, version: own.version, packagesDir: existsSync(join(siblings, 'server-core', 'package.json')) ? siblings : null };
152
+ }
153
+
154
+ /** The app's `package.json`: the template's, with every `workspace:*` dep and the chosen view packages at the current version, or `link:`ed to the checkout with `local`. */
155
+ export function appPackageJson(app: NewApp, opts: { views: string[]; local: boolean }, pkg: BuildPackage = buildPackage()): Record<string, any> {
156
+ const json = JSON.parse(fillPlaceholders(readFileSync(join(TEMPLATE, 'package.json'), 'utf8'), app));
157
+ const range = (name: string): string => {
158
+ if (!opts.local) return pkg.version;
159
+ if (!pkg.packagesDir) throw new Error('--local needs a checkout of the deckbuilder monorepo.');
160
+ return `link:${join(pkg.packagesDir, name.split('/')[1]!).replaceAll('\\', '/')}`;
161
+ };
162
+ for (const field of ['dependencies', 'devDependencies']) {
163
+ for (const [name, r] of Object.entries<string>(json[field])) if (r.startsWith('workspace:')) json[field][name] = range(name);
164
+ }
165
+ for (const v of viewSpecs(opts.views)) json.dependencies[pkgName(v, pkg.scope)] = range(pkgName(v, pkg.scope));
166
+ json.dependencies = Object.fromEntries(Object.entries(json.dependencies).sort(([a], [b]) => a.localeCompare(b)));
167
+ return json;
168
+ }
169
+
170
+ /** Creates `<cwd>/<dir>` from the template, on the published packages (or linked to this checkout with `local`); returns its path. */
50
171
  export async function createApp(opts: NewAppOptions, cwd: string): Promise<string> {
51
172
  const checked = validateNewApp(opts);
52
173
  if ('error' in checked) throw new Error(checked.error);
53
- const root = await workspaceRoot(cwd);
54
- const target = join(root, 'apps', checked.app.dir);
174
+ const views = opts.views ?? [];
175
+ const pkg = buildPackage();
176
+ const packageJson = appPackageJson(checked.app, { views, local: opts.local ?? false }, pkg);
177
+ const target = join(cwd, checked.app.dir);
55
178
  if (existsSync(target)) throw new Error(`${target} already exists.`);
56
179
  copyFilled(TEMPLATE, target, checked.app);
180
+ writeFileSync(join(target, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
181
+ writeFileSync(join(target, 'web', 'src', 'view-settings.ts'), viewSettingsSource(views, pkg.scope));
182
+ writeFileSync(join(target, 'web', 'src', 'views.ts'), webViewsSource(views, pkg.scope));
183
+ writeFileSync(join(target, 'server', 'views.ts'), serverViewsSource(views, pkg.scope));
184
+ writeFileSync(join(target, '.gitignore'), 'node_modules\ndist\nrelease\n');
185
+ if (opts.git) Bun.spawnSync(['git', 'init'], { cwd: target });
57
186
  return target;
58
187
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@deckbuilderkit/__APP_DIR__",
2
+ "name": "__APP_DIR__",
3
3
  "version": "0.1.0",
4
4
  "description": "__APP_NAME__",
5
5
  "private": true,
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { appHome, createAutostart, pickFolder } from '@deckbuilderkit/platform';
3
- import { updatePublicKey } from '@deckbuilderkit/server-core';
3
+ import { applyViewSettings, mountViews, updatePublicKey } from '@deckbuilderkit/server-core';
4
4
  import {
5
5
  createUpdater,
6
6
  folderSource,
@@ -19,8 +19,9 @@ import { type Keymap, validKeymap } from '@deckbuilderkit/ui/keybindings';
19
19
  import { APP } from '../app.ts';
20
20
  import pkg from '../package.json';
21
21
  import { routes } from './routes.ts';
22
+ import { serverViews, type ViewSettings } from './views.ts';
22
23
 
23
- export interface Settings extends BaseSettings {
24
+ export interface Settings extends BaseSettings, ViewSettings {
24
25
  /** Keybinding overrides by command id, checked by `validKeymap`. */
25
26
  keybindings?: Keymap;
26
27
  /** Start at Windows login (exe only). */
@@ -50,6 +51,7 @@ const settings = settingsStore<Settings>({
50
51
  }
51
52
  const keybindings = validKeymap(patch.keybindings);
52
53
  if (keybindings) next.keybindings = keybindings;
54
+ applyViewSettings(serverViews, patch, next as unknown as Record<string, unknown>);
53
55
  },
54
56
  onChange(next, prev) {
55
57
  if (next.autostart !== prev.autostart) void autostart.set(next.autostart);
@@ -67,6 +69,7 @@ const updater = createUpdater({
67
69
  });
68
70
 
69
71
  const { closeToTray } = await settings.load();
72
+ const mounted = mountViews(serverViews, { dataDir, settings, stores: {} });
70
73
 
71
74
  await runApp({
72
75
  app: APP,
@@ -81,8 +84,12 @@ await runApp({
81
84
  return settings.save({ updateFolder: path });
82
85
  },
83
86
  ...updateRoutes(updater, ctx.quit),
87
+ ...mounted.routes,
84
88
  ...routes(ctx),
85
89
  }),
90
+ sockets: mounted.sockets,
91
+ unauthenticated: mounted.unauthenticated,
92
+ onQuit: () => mounted.onQuit(),
86
93
  webDir: join(import.meta.dir, '../dist/web'),
87
94
  transformIndex: themeHead(settings),
88
95
  urlProtocol: true,
@@ -1,5 +1,30 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "lib": ["ESNext"], "types": ["bun"] },
4
- "include": ["app.ts", "server", "vite.config.ts"]
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Preserve",
5
+ "moduleResolution": "Bundler",
6
+ "moduleDetection": "force",
7
+ "allowImportingTsExtensions": true,
8
+ "resolveJsonModule": true,
9
+ "verbatimModuleSyntax": true,
10
+ "isolatedModules": true,
11
+ "strict": true,
12
+ "noUnusedLocals": true,
13
+ "noUnusedParameters": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noImplicitOverride": true,
16
+ "skipLibCheck": true,
17
+ "noEmit": true,
18
+ "lib": [
19
+ "ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"
20
+ ],
21
+ "types": [
22
+ "bun"
23
+ ]
24
+ },
25
+ "include": [
26
+ "app.ts",
27
+ "server",
28
+ "vite.config.ts"
29
+ ]
5
30
  }
@@ -1,23 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { PALETTES } from '@deckbuilderkit/server-core/palettes';
3
- import { AppFrame, type CommandDef, CommandPalette, detectLanguage, effectiveKeys, ErrorBanner, GLOBAL, Icon, type IconName, isDesktop, KeybindingProvider, LayoutProvider, paletteCommand, type PaletteItem, setLanguage, Sidebar, Spinner, useAsync, useCommand, useCommandPalette, useHashRoute, useHeartbeat, useKeybindings, useTheme, useZoom } from '@deckbuilderkit/ui';
3
+ import { AppFrame, type CommandDef, CommandPalette, detectLanguage, effectiveKeys, ErrorBanner, GLOBAL, Icon, isDesktop, KeybindingProvider, LayoutProvider, paletteCommand, type PaletteItem, setLanguage, Sidebar, Spinner, useAsync, useCommand, useCommandPalette, useHashRoute, useHeartbeat, useKeybindings, useTheme, useZoom } from '@deckbuilderkit/ui';
4
4
  import { APP } from '../../app';
5
5
  import { api, type Settings } from './api';
6
6
  import { type MessageKey, t } from './i18n';
7
- import { Home } from './screens/Home';
8
7
  import { SettingsModal } from './screens/SettingsModal';
9
-
10
- const SCREENS = ['home'] as const;
11
- type ScreenId = (typeof SCREENS)[number];
12
-
13
- const NAV: Array<{ title: MessageKey; items: Array<{ id: ScreenId; label: MessageKey; icon: IconName }> }> = [
14
- {
15
- title: 'nav.section.main',
16
- items: [
17
- { id: 'home', label: 'nav.home', icon: 'home' },
18
- ],
19
- },
20
- ];
8
+ import { views, type ScreenId } from './views';
21
9
 
22
10
  const navCommandId = (screen: ScreenId) => `nav.${screen}`;
23
11
  const SETTINGS_COMMAND = 'app.settings';
@@ -25,15 +13,15 @@ const KEYS_COMMAND = 'app.keybindings';
25
13
 
26
14
  /** Every shortcut of the app; built at render so the labels follow the language. */
27
15
  function commands(): CommandDef[] {
28
- const nav = NAV.flatMap((s) => s.items).map((item, i): CommandDef => ({
29
- id: navCommandId(item.id), scope: GLOBAL, label: t(item.label), keys: [`Ctrl+${i + 1}`], inInput: true,
16
+ const nav = views.nav.flatMap((s) => s.items).map((item, i): CommandDef => ({
17
+ id: navCommandId(item.id), scope: GLOBAL, label: views.label(item.id), keys: [`Ctrl+${i + 1}`], inInput: true,
30
18
  }));
31
19
  // Settings and keybindings are modals over the screen, not screens; both are VS Code's keys.
32
20
  const overlays: CommandDef[] = [
33
21
  { id: SETTINGS_COMMAND, scope: GLOBAL, label: t('nav.settings'), keys: ['Ctrl+,'], inInput: true },
34
22
  { id: KEYS_COMMAND, scope: GLOBAL, label: t('nav.keys'), keys: ['Ctrl+K Ctrl+S'], inInput: true },
35
23
  ];
36
- return [paletteCommand(), ...nav, ...overlays];
24
+ return [paletteCommand(), ...nav, ...overlays, ...views.keybindings(), ...views.commands()];
37
25
  }
38
26
 
39
27
  // Retrying can't revive a used launch code or a stale token; the user has to open the app again.
@@ -65,7 +53,7 @@ export function App() {
65
53
  function Shell({ initialSettings }: { initialSettings: Settings }) {
66
54
  const [settings, setSettings] = useState(initialSettings);
67
55
  const [overlay, setOverlay] = useState(overlayOfHash);
68
- const shown = useRef<ScreenId>('home');
56
+ const shown = useRef<ScreenId>(views.ids[0]!);
69
57
  // Before useHashRoute, so this listener runs first: it puts the shown screen back in the hash for the router.
70
58
  useEffect(() => {
71
59
  const take = () => {
@@ -78,7 +66,7 @@ function Shell({ initialSettings }: { initialSettings: Settings }) {
78
66
  window.addEventListener('hashchange', take);
79
67
  return () => window.removeEventListener('hashchange', take);
80
68
  }, []);
81
- const [screen, go] = useHashRoute(SCREENS, 'home');
69
+ const [screen, go] = useHashRoute(views.ids, views.ids[0]!);
82
70
  shown.current = screen;
83
71
  setLanguage(settings.language ?? detectLanguage());
84
72
  return (
@@ -106,8 +94,7 @@ function Main({ settings, setSettings, screen, go, overlay, setOverlay }: MainPr
106
94
  useZoom(settings.zoom);
107
95
 
108
96
  useEffect(() => {
109
- const item = NAV.flatMap((s) => s.items).find((i) => i.id === screen);
110
- document.title = item ? `${t(item.label)} · ${APP.name}` : APP.name;
97
+ document.title = views.has(screen) ? `${views.label(screen)} · ${APP.name}` : APP.name;
111
98
  }, [screen, settings.language]);
112
99
 
113
100
  const save = useCallback((patch: Partial<Settings>) => {
@@ -115,7 +102,7 @@ function Main({ settings, setSettings, screen, go, overlay, setOverlay }: MainPr
115
102
  api.saveSettings(patch).catch(() => { /* preferences are best-effort */ });
116
103
  }, []);
117
104
 
118
- const navItems = NAV.flatMap((s) => s.items);
105
+ const navItems = views.nav.flatMap((s) => s.items);
119
106
  const defs = commands();
120
107
  // The nav list is a constant, so the hooks run in the same order every render.
121
108
  for (const item of navItems) useCommand(defs.find((d) => d.id === navCommandId(item.id))!, () => go(item.id));
@@ -128,7 +115,7 @@ function Main({ settings, setSettings, screen, go, overlay, setOverlay }: MainPr
128
115
  const nextTheme = kind === 'dark' ? 'light' : 'dark';
129
116
  const items = useMemo<PaletteItem[]>(() => {
130
117
  const goTo = navItems.map((i): PaletteItem => ({
131
- id: `go:${i.id}`, group: t('palette.group.go'), label: t(i.label), hint: chordOf(navCommandId(i.id)), keywords: [i.id], run: () => go(i.id),
118
+ id: `go:${i.id}`, group: t('palette.group.go'), label: views.label(i.id), hint: chordOf(navCommandId(i.id)), keywords: [i.id], run: () => go(i.id),
132
119
  }));
133
120
  const overlays: PaletteItem[] = [
134
121
  { id: 'app:settings', group: t('palette.group.go'), label: t('nav.settings'), hint: chordOf(SETTINGS_COMMAND), keywords: ['settings', 'preferences'], run: () => setOverlay('settings') },
@@ -150,7 +137,7 @@ function Main({ settings, setSettings, screen, go, overlay, setOverlay }: MainPr
150
137
 
151
138
  if (stopped) return <div className="boot"><span>{t('app.stopped', { name: APP.name })}</span></div>;
152
139
 
153
- const sections = NAV.map((s) => ({ title: t(s.title), items: s.items.map((i) => ({ ...i, label: t(i.label), hint: chordOf(navCommandId(i.id)) })) }));
140
+ const sections = views.nav.map((s) => ({ title: t(s.group as MessageKey), items: s.items.map((i) => ({ ...i, label: views.label(i.id), hint: chordOf(navCommandId(i.id)) })) }));
154
141
 
155
142
  const footer = (
156
143
  <>
@@ -168,20 +155,15 @@ function Main({ settings, setSettings, screen, go, overlay, setOverlay }: MainPr
168
155
 
169
156
  return (
170
157
  <LayoutProvider layout={settings.layout} icons={settings.icons} mainMenu={settings.mainMenu} dock={settings.dock} layoutIcon={settings.layoutIcon}>
158
+ <views.Providers settings={settings} go={go as (id: string) => void}>
171
159
  <AppFrame tools={footer} sidebar={<Sidebar name={APP.name} sections={sections} current={screen} onSelect={go} footer={footer} />}>
172
- <main className="main">{renderScreen(screen)}</main>
160
+ <main className="main">{views.render(screen, { settings, onSave: save, go: go as (id: string) => void })}</main>
173
161
  {overlay && (
174
162
  <SettingsModal key={overlay} settings={settings} onChange={save} onQuit={() => setStopped(true)} onClose={() => setOverlay(null)} initialSection={overlay === 'keys' ? 'keyboard.shortcuts' : overlay.startsWith('settings/') ? overlay.slice('settings/'.length) : undefined} />
175
163
  )}
176
164
  {palette.open && <CommandPalette items={items} onClose={() => palette.setOpen(false)} />}
177
165
  </AppFrame>
166
+ </views.Providers>
178
167
  </LayoutProvider>
179
168
  );
180
169
  }
181
-
182
- function renderScreen(screen: ScreenId) {
183
- switch (screen) {
184
- default:
185
- return <Home />;
186
- }
187
- }
@@ -1,8 +1,9 @@
1
1
  import type { ThemeSettings } from '@deckbuilderkit/server-core/palettes';
2
2
  import type { UpdateState } from '@deckbuilderkit/server-core/protocol';
3
+ import type { ViewSettings } from './view-settings';
3
4
  import { type Keymap, type Language, request } from '@deckbuilderkit/ui';
4
5
 
5
- export interface Settings extends ThemeSettings {
6
+ export interface Settings extends ThemeSettings, ViewSettings {
6
7
  language?: Language;
7
8
  /** The main menu shows only its icons. */
8
9
  /** Keybinding overrides by command id; the server validates it. */
@@ -0,0 +1,10 @@
1
+ import { defineView } from '@deckbuilderkit/ui';
2
+ import type { Settings } from '../api';
3
+ import { Home } from './Home';
4
+
5
+ export const homeView = defineView<'home', Settings>({
6
+ id: 'home',
7
+ nav: { group: 'nav.section.main', icon: 'home', label: 'nav.home', order: 10 },
8
+ render: () => <Home />,
9
+ messages: { en: {}, 'pt-BR': {} },
10
+ });
@@ -1,9 +1,30 @@
1
1
  {
2
- "extends": "../../../tsconfig.base.json",
3
2
  "compilerOptions": {
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
3
+ "target": "ES2022",
4
+ "module": "Preserve",
5
+ "moduleResolution": "Bundler",
6
+ "moduleDetection": "force",
7
+ "allowImportingTsExtensions": true,
8
+ "resolveJsonModule": true,
9
+ "verbatimModuleSyntax": true,
10
+ "isolatedModules": true,
11
+ "strict": true,
12
+ "noUnusedLocals": true,
13
+ "noUnusedParameters": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noImplicitOverride": true,
16
+ "skipLibCheck": true,
17
+ "noEmit": true,
18
+ "lib": [
19
+ "ES2022",
20
+ "DOM",
21
+ "DOM.Iterable"
22
+ ],
5
23
  "jsx": "react-jsx",
6
24
  "types": []
7
25
  },
8
- "include": ["src", "../app.ts"]
26
+ "include": [
27
+ "src",
28
+ "../app.ts"
29
+ ]
9
30
  }