@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 +3 -3
- package/src/cli.ts +10 -7
- package/src/new.ts +133 -4
- package/template/package.json +1 -1
- package/template/server/main.ts +9 -2
- package/template/tsconfig.json +28 -3
- package/template/web/src/App.tsx +14 -32
- package/template/web/src/api.ts +2 -1
- package/template/web/src/screens/home.view.tsx +10 -0
- package/template/web/tsconfig.json +24 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deckbuilderkit/build",
|
|
3
|
-
"version": "0.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.
|
|
25
|
-
"@deckbuilderkit/server-core": "0.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
|
|
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
|
|
107
|
-
|
|
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
|
-
/**
|
|
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
|
|
54
|
-
const
|
|
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
|
}
|
package/template/package.json
CHANGED
package/template/server/main.ts
CHANGED
|
@@ -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,
|
package/template/tsconfig.json
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
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
|
}
|
package/template/web/src/App.tsx
CHANGED
|
@@ -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,
|
|
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 =
|
|
29
|
-
id: navCommandId(item.id), scope: GLOBAL, label:
|
|
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>(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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">{
|
|
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
|
-
}
|
package/template/web/src/api.ts
CHANGED
|
@@ -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
|
-
"
|
|
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": [
|
|
26
|
+
"include": [
|
|
27
|
+
"src",
|
|
28
|
+
"../app.ts"
|
|
29
|
+
]
|
|
9
30
|
}
|