@inglorious/create-app 1.1.2 → 1.3.0
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 +21 -7
- package/bin/index.js +64 -88
- package/bin/lib/app-jsx.js +280 -0
- package/bin/lib/app-lit.js +275 -0
- package/bin/lib/app-vue.js +239 -0
- package/bin/lib/app.js +402 -0
- package/bin/lib/cli.js +34 -0
- package/bin/lib/constants.js +6 -4
- package/bin/lib/features.js +171 -0
- package/bin/lib/imports.js +64 -0
- package/bin/lib/mobile.js +19 -1
- package/bin/lib/pwa.js +50 -4
- package/bin/lib/questions.js +184 -0
- package/bin/lib/source.js +58 -0
- package/package.json +4 -4
- package/templates/js/package.json +4 -4
- package/templates/js/vite.config.js +2 -1
- package/templates/js-jsx/package.json +4 -4
- package/templates/js-jsx/vite.config.js +2 -1
- package/templates/js-vue/package.json +4 -4
- package/templates/js-vue/vite.config.js +2 -1
- package/templates/ssx-js/package.json +8 -7
- package/templates/ssx-js/src/site.config.js +2 -1
- package/templates/ssx-ts/package.json +9 -7
- package/templates/ssx-ts/src/site.config.ts +1 -0
- package/templates/ts/package.json +8 -6
- package/templates/ts/src/app.ts +4 -2
- package/templates/ts/tsconfig.app.json +2 -0
- package/templates/ts/tsconfig.node.json +2 -0
- package/templates/ts/types/index.d.ts +4 -1
- package/templates/ts/vite.config.ts +2 -1
- package/templates/ts-jsx/package.json +8 -6
- package/templates/ts-jsx/src/app.tsx +2 -3
- package/templates/ts-jsx/tsconfig.app.json +2 -0
- package/templates/ts-jsx/tsconfig.node.json +2 -0
- package/templates/ts-jsx/types/index.d.ts +3 -1
- package/templates/ts-jsx/types/vite-end.d.ts +1 -0
- package/templates/ts-jsx/vite.config.ts +2 -1
- package/templates/ts-vue/eslint.config.ts +1 -1
- package/templates/ts-vue/package.json +8 -6
- package/templates/ts-vue/src/main.ts +1 -1
- package/templates/ts-vue/src/store/middlewares.ts +5 -2
- package/templates/ts-vue/tsconfig.app.json +2 -0
- package/templates/ts-vue/tsconfig.node.json +2 -0
- package/templates/ts-vue/types/index.d.ts +3 -1
- package/templates/ts-vue/vite.config.ts +2 -1
package/README.md
CHANGED
|
@@ -23,17 +23,31 @@ pnpm create @inglorious/app
|
|
|
23
23
|
|
|
24
24
|
The CLI will guide you through the setup process, asking for a project name and which template you'd like to use.
|
|
25
25
|
|
|
26
|
-
For JavaScript and TypeScript Vite
|
|
26
|
+
For the JavaScript and TypeScript Vite templates, the CLI then asks about the app's features:
|
|
27
|
+
|
|
28
|
+
| Question | Flag | Adds |
|
|
29
|
+
| ------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| Use Inglorious UI? | `--ui` / `--no-ui` | `@inglorious/ui`, its base styles and an app bar |
|
|
31
|
+
| Which theme? | `--theme=inglorious\|material\|bootstrap` | The theme's stylesheet and class |
|
|
32
|
+
| Is it a mobile app? | `--mobile` / `--no-mobile` | Capacitor for Android and iOS; with Inglorious UI, a bottom tab bar and safe-area spacing |
|
|
33
|
+
| Add routing? | `--routing` / `--no-routing` | The Router, a home and an about page, and navigation between them |
|
|
34
|
+
| Animate page transitions? | `--transitions` / `--no-transitions` | View transitions between pages (with routing and Inglorious UI) |
|
|
35
|
+
| Which icon packs? | `--icons=material,fontawesome,lucide` / `--no-icons` | Iconify icon sets, drawn by Inglorious UI's `Icon` (with Inglorious UI) |
|
|
36
|
+
| Translate the app? | `--i18n` / `--no-i18n`, `--languages=en,it` | `@inglorious/i18n`, a catalogue per language and a language picker; the first language is the default |
|
|
37
|
+
| Enable PWA support? | `--pwa` / `--no-pwa` | `vite-plugin-pwa`, a manifest, icons and service worker registration |
|
|
38
|
+
|
|
39
|
+
The app is generated in the syntax you chose: lit-html, JSX or Vue. Every question answered on the command line is skipped, so a project can be created without any prompt:
|
|
40
|
+
|
|
41
|
+
| Question | Argument or flag |
|
|
42
|
+
| --------------------------- | -------------------------------------------- |
|
|
43
|
+
| What is your project named? | The first argument, such as `my-app` |
|
|
44
|
+
| Select a template | `--template=minimal\|js\|ts\|ssx-js\|ssx-ts` |
|
|
45
|
+
| Select a rendering syntax | `--syntax=lit-html\|jsx\|vue` |
|
|
27
46
|
|
|
28
47
|
```bash
|
|
29
|
-
npm create @inglorious/app@latest -- --pwa
|
|
30
|
-
npm create @inglorious/app@latest -- --mobile
|
|
31
|
-
npm create @inglorious/app@latest -- --pwa --mobile
|
|
48
|
+
npm create @inglorious/app@latest -- my-app --template=ts --syntax=jsx --ui --theme=material --routing --transitions --icons=material --languages=en,it --no-mobile --no-pwa
|
|
32
49
|
```
|
|
33
50
|
|
|
34
|
-
- `--pwa` adds `vite-plugin-pwa`, a basic web app manifest configuration, PWA icons, and browser-only service worker registration.
|
|
35
|
-
- `--mobile` adds Capacitor dependencies, config, and mobile scripts for iOS and Android.
|
|
36
|
-
|
|
37
51
|
## Templates
|
|
38
52
|
|
|
39
53
|
The following templates are available to get you started:
|
package/bin/index.js
CHANGED
|
@@ -8,22 +8,26 @@ import path from "path"
|
|
|
8
8
|
import prompts from "prompts"
|
|
9
9
|
import { fileURLToPath } from "url"
|
|
10
10
|
|
|
11
|
+
import { addAppFeatures } from "./lib/app.js"
|
|
11
12
|
import { parseCliOptions } from "./lib/cli.js"
|
|
12
13
|
import {
|
|
13
|
-
EMPTY_LENGTH,
|
|
14
14
|
INDENTATION,
|
|
15
15
|
NPM_VIEW_TIMEOUT_MS,
|
|
16
16
|
USER_ARG_START_INDEX,
|
|
17
17
|
VITE_APP_TEMPLATES,
|
|
18
18
|
} from "./lib/constants.js"
|
|
19
|
+
import { SYNTAXES, TEMPLATES } from "./lib/features.js"
|
|
19
20
|
import { copyDir } from "./lib/file.js"
|
|
20
21
|
import { addMobileSupport } from "./lib/mobile.js"
|
|
21
22
|
import { addPwaSupport } from "./lib/pwa.js"
|
|
23
|
+
import { askAppFeatures } from "./lib/questions.js"
|
|
22
24
|
|
|
23
25
|
async function main() {
|
|
24
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
25
27
|
const templatesRoot = path.join(__dirname, "../templates")
|
|
26
28
|
const cliOptions = parseCliOptions(process.argv.slice(USER_ARG_START_INDEX))
|
|
29
|
+
validateChoice("template", cliOptions.template, TEMPLATES)
|
|
30
|
+
validateChoice("syntax", cliOptions.syntax, SYNTAXES)
|
|
27
31
|
|
|
28
32
|
let canceled = false
|
|
29
33
|
|
|
@@ -32,45 +36,19 @@ async function main() {
|
|
|
32
36
|
return false
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
const
|
|
39
|
+
const answers = await prompts(
|
|
36
40
|
[
|
|
37
41
|
{
|
|
38
|
-
type: "text",
|
|
42
|
+
type: cliOptions.projectName ? null : "text",
|
|
39
43
|
name: "projectName",
|
|
40
44
|
message: "What is your project named?",
|
|
41
45
|
initial: "my-app",
|
|
42
46
|
},
|
|
43
47
|
{
|
|
44
|
-
type: "select",
|
|
48
|
+
type: cliOptions.template ? null : "select",
|
|
45
49
|
name: "baseTemplate",
|
|
46
50
|
message: "Select a template",
|
|
47
|
-
choices:
|
|
48
|
-
{
|
|
49
|
-
title: "Minimal",
|
|
50
|
-
description: "A single HTML file for a zero-build setup",
|
|
51
|
-
value: "minimal",
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
title: "JavaScript",
|
|
55
|
-
description: "A vanilla JavaScript setup with Vite",
|
|
56
|
-
value: "js",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
title: "TypeScript",
|
|
60
|
-
description: "A TypeScript setup with Vite",
|
|
61
|
-
value: "ts",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
title: "SSX (JavaScript)",
|
|
65
|
-
description: "Static Site Xecution with JavaScript",
|
|
66
|
-
value: "ssx-js",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
title: "SSX (TypeScript)",
|
|
70
|
-
description: "Static Site Xecution with TypeScript",
|
|
71
|
-
value: "ssx-ts",
|
|
72
|
-
},
|
|
73
|
-
],
|
|
51
|
+
choices: toChoices(TEMPLATES),
|
|
74
52
|
},
|
|
75
53
|
],
|
|
76
54
|
{ onCancel },
|
|
@@ -81,33 +59,18 @@ async function main() {
|
|
|
81
59
|
return
|
|
82
60
|
}
|
|
83
61
|
|
|
62
|
+
const projectName = cliOptions.projectName ?? answers.projectName
|
|
63
|
+
const baseTemplate = cliOptions.template ?? answers.baseTemplate
|
|
64
|
+
|
|
84
65
|
let renderer = "lit-html"
|
|
85
|
-
let pwa = cliOptions.pwa === true
|
|
86
|
-
let mobile = cliOptions.mobile === true
|
|
87
66
|
|
|
88
67
|
if (baseTemplate !== "minimal") {
|
|
89
68
|
const { selectedRenderer } = await prompts(
|
|
90
69
|
{
|
|
91
|
-
type: "select",
|
|
70
|
+
type: cliOptions.syntax ? null : "select",
|
|
92
71
|
name: "selectedRenderer",
|
|
93
72
|
message: "Select a rendering syntax",
|
|
94
|
-
choices:
|
|
95
|
-
{
|
|
96
|
-
title: "lit-html (default)",
|
|
97
|
-
description: "Tagged template literals (html`<div>...</div>`)",
|
|
98
|
-
value: "lit-html",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
title: "JSX",
|
|
102
|
-
description: "JSX/TSX syntax (<div>...</div>)",
|
|
103
|
-
value: "jsx",
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
title: "Vue",
|
|
107
|
-
description: "Vue SFC-style templates",
|
|
108
|
-
value: "vue",
|
|
109
|
-
},
|
|
110
|
-
],
|
|
73
|
+
choices: toChoices(SYNTAXES),
|
|
111
74
|
},
|
|
112
75
|
{ onCancel },
|
|
113
76
|
)
|
|
@@ -117,49 +80,31 @@ async function main() {
|
|
|
117
80
|
return
|
|
118
81
|
}
|
|
119
82
|
|
|
120
|
-
renderer = selectedRenderer
|
|
83
|
+
renderer = cliOptions.syntax ?? selectedRenderer
|
|
84
|
+
} else if (cliOptions.syntax && cliOptions.syntax !== "lit-html") {
|
|
85
|
+
console.warn("The minimal template is only available with lit-html.")
|
|
121
86
|
}
|
|
122
87
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
name: "pwa",
|
|
132
|
-
message: "Enable PWA support?",
|
|
133
|
-
initial: false,
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (cliOptions.mobile === null) {
|
|
138
|
-
featureQuestions.push({
|
|
139
|
-
type: "confirm",
|
|
140
|
-
name: "mobile",
|
|
141
|
-
message: "Add hybrid mobile support with Capacitor?",
|
|
142
|
-
initial: false,
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (featureQuestions.length > EMPTY_LENGTH) {
|
|
147
|
-
const answers = await prompts(featureQuestions, { onCancel })
|
|
88
|
+
let features = {
|
|
89
|
+
ui: false,
|
|
90
|
+
routing: false,
|
|
91
|
+
icons: [],
|
|
92
|
+
languages: [],
|
|
93
|
+
mobile: false,
|
|
94
|
+
pwa: false,
|
|
95
|
+
}
|
|
148
96
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
152
|
-
}
|
|
97
|
+
if (VITE_APP_TEMPLATES.has(baseTemplate)) {
|
|
98
|
+
features = await askAppFeatures(cliOptions)
|
|
153
99
|
|
|
154
|
-
|
|
155
|
-
|
|
100
|
+
if (!features) {
|
|
101
|
+
console.log("Operation canceled.")
|
|
102
|
+
return
|
|
156
103
|
}
|
|
157
|
-
} else if (pwa || mobile) {
|
|
104
|
+
} else if (cliOptions.pwa || cliOptions.mobile) {
|
|
158
105
|
console.warn(
|
|
159
106
|
"PWA and mobile options are only available for JavaScript and TypeScript Vite app templates.",
|
|
160
107
|
)
|
|
161
|
-
pwa = false
|
|
162
|
-
mobile = false
|
|
163
108
|
}
|
|
164
109
|
|
|
165
110
|
const spinner = ora(`Creating project "${projectName}"...`).start()
|
|
@@ -194,12 +139,28 @@ async function main() {
|
|
|
194
139
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
195
140
|
pkg.name = projectName
|
|
196
141
|
|
|
197
|
-
|
|
142
|
+
const hasAppFeatures =
|
|
143
|
+
features.ui ||
|
|
144
|
+
features.routing ||
|
|
145
|
+
Boolean(features.icons.length) ||
|
|
146
|
+
Boolean(features.languages.length)
|
|
147
|
+
|
|
148
|
+
if (hasAppFeatures) {
|
|
149
|
+
spinner.text = "Adding app features..."
|
|
150
|
+
addAppFeatures(targetDir, pkg, {
|
|
151
|
+
...features,
|
|
152
|
+
projectName,
|
|
153
|
+
isTypeScript: baseTemplate === "ts",
|
|
154
|
+
renderer,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (features.pwa) {
|
|
198
159
|
spinner.text = "Adding PWA support..."
|
|
199
160
|
addPwaSupport(targetDir, pkg, projectName)
|
|
200
161
|
}
|
|
201
162
|
|
|
202
|
-
if (mobile) {
|
|
163
|
+
if (features.mobile) {
|
|
203
164
|
spinner.text = "Adding Capacitor support..."
|
|
204
165
|
addMobileSupport(targetDir, pkg, projectName)
|
|
205
166
|
}
|
|
@@ -228,6 +189,21 @@ async function main() {
|
|
|
228
189
|
}
|
|
229
190
|
}
|
|
230
191
|
|
|
192
|
+
function toChoices(options) {
|
|
193
|
+
return Object.entries(options).map(([value, option]) => ({
|
|
194
|
+
value,
|
|
195
|
+
...option,
|
|
196
|
+
}))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function validateChoice(flag, value, options) {
|
|
200
|
+
if (value !== null && !options[value]) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Unknown ${flag} "${value}". Choose one of: ${Object.keys(options).join(", ")}.`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
231
207
|
function getLatestVersion(packageName) {
|
|
232
208
|
try {
|
|
233
209
|
const version = execSync(`npm view ${packageName} version`, {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// The app's components in JSX: .jsx or .tsx files, compiled to lit-html by
|
|
2
|
+
// @inglorious/vite-plugin-jsx. A capitalized tag with props renders a
|
|
3
|
+
// component; `<Message entityId="message1" />` renders an entity. TypeScript
|
|
4
|
+
// expects components to be functions, so in .tsx each capitalized tag is
|
|
5
|
+
// preceded by a `@ts-expect-error`.
|
|
6
|
+
|
|
7
|
+
import { renderLocals, selectChange } from "./app-lit.js"
|
|
8
|
+
import { importBlock } from "./imports.js"
|
|
9
|
+
|
|
10
|
+
export const extension = (app) => (app.isTypeScript ? "tsx" : "jsx")
|
|
11
|
+
|
|
12
|
+
const EXPECT_ERROR =
|
|
13
|
+
"@ts-expect-error - Inglorious components are objects, not functions"
|
|
14
|
+
|
|
15
|
+
// The comment line before a capitalized tag in .tsx: `{/* */}` among JSX
|
|
16
|
+
// children, `//` inside code.
|
|
17
|
+
function expectError(app, indent, isChild = true) {
|
|
18
|
+
if (!app.isTypeScript) return ""
|
|
19
|
+
const comment = isChild ? `{/* ${EXPECT_ERROR} */}` : `// ${EXPECT_ERROR}`
|
|
20
|
+
return `${indent}${comment}\n`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function appSource(app, { pages, navIcons }) {
|
|
24
|
+
const imports = []
|
|
25
|
+
if (navIcons) {
|
|
26
|
+
for (const icon of Object.values(navIcons)) {
|
|
27
|
+
imports.push(`import ${icon.name} from "${icon.from}"`)
|
|
28
|
+
}
|
|
29
|
+
imports.push('import { Icon } from "@inglorious/ui/icon"')
|
|
30
|
+
}
|
|
31
|
+
if (app.ui) {
|
|
32
|
+
imports.push(
|
|
33
|
+
'import { AppBar } from "@inglorious/ui/app-bar"',
|
|
34
|
+
'import { Container } from "@inglorious/ui/container"',
|
|
35
|
+
)
|
|
36
|
+
if (app.tabs) {
|
|
37
|
+
imports.push(
|
|
38
|
+
'import { BottomNavigation } from "@inglorious/ui/bottom-navigation"',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (app.links) imports.push('import { Link } from "@inglorious/ui/link"')
|
|
42
|
+
if (app.i18n) imports.push('import { Select } from "@inglorious/ui/select"')
|
|
43
|
+
}
|
|
44
|
+
if (app.isTypeScript) imports.push('import type { AppApi } from "../types"')
|
|
45
|
+
if (app.i18n) imports.push('import { t } from "./i18n"')
|
|
46
|
+
|
|
47
|
+
const signature = app.isTypeScript
|
|
48
|
+
? "render(props: Record<string, unknown> | null, api: AppApi)"
|
|
49
|
+
: "render(props, api)"
|
|
50
|
+
const content = app.routing
|
|
51
|
+
? "api.render(router.route)"
|
|
52
|
+
: 'api.render("home")'
|
|
53
|
+
|
|
54
|
+
const shell = app.ui
|
|
55
|
+
? uiShell(app, { pages, content })
|
|
56
|
+
: plainShell(app, { pages, content })
|
|
57
|
+
|
|
58
|
+
return `${importBlock(imports)}
|
|
59
|
+
|
|
60
|
+
export const App = {
|
|
61
|
+
${signature} {
|
|
62
|
+
${renderLocals(app)} return (
|
|
63
|
+
<>
|
|
64
|
+
${shell}
|
|
65
|
+
</>
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A callback's parameter. The components' props are not typed in .tsx, so
|
|
73
|
+
// TypeScript cannot infer it.
|
|
74
|
+
function typed(app, name, type) {
|
|
75
|
+
return app.isTypeScript ? `${name}: ${type}` : name
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A page's label: translated, or the English text.
|
|
79
|
+
function labelText(app, page) {
|
|
80
|
+
return app.i18n ? `{t("${page.key}")}` : page.text
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function labelValue(app, page) {
|
|
84
|
+
return app.i18n ? `t("${page.key}")` : `"${page.text}"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function uiShell(app, { pages, content }) {
|
|
88
|
+
const nav = []
|
|
89
|
+
if (app.links) {
|
|
90
|
+
nav.push(
|
|
91
|
+
...pages.map(
|
|
92
|
+
(page) =>
|
|
93
|
+
`${expectError(app, " ")} <Link href="${page.path}">${labelText(app, page)}</Link>`,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
if (app.i18n) {
|
|
98
|
+
nav.push(`${expectError(app, " ")} <Select
|
|
99
|
+
value={i18n.locale}
|
|
100
|
+
options={i18n.languages}
|
|
101
|
+
size="sm"
|
|
102
|
+
ariaLabel={t("language")}
|
|
103
|
+
onChange={(${typed(app, "locale", "string")}) =>
|
|
104
|
+
api.notify("#i18n:localeChange", locale)
|
|
105
|
+
}
|
|
106
|
+
/>`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const appBar = nav.length
|
|
110
|
+
? `${expectError(app, " ")} <AppBar
|
|
111
|
+
title="${app.projectName}"
|
|
112
|
+
trailing={
|
|
113
|
+
<nav className="app-nav">
|
|
114
|
+
${nav.join("\n")}
|
|
115
|
+
</nav>
|
|
116
|
+
}
|
|
117
|
+
/>`
|
|
118
|
+
: `${expectError(app, " ")} <AppBar title="${app.projectName}" />`
|
|
119
|
+
|
|
120
|
+
const header = ` <header${app.mobile ? ' className="pt-safe"' : ""}>
|
|
121
|
+
${appBar}
|
|
122
|
+
</header>
|
|
123
|
+
<main>
|
|
124
|
+
${expectError(app, " ")} <Container maxWidth="md">{${content}}</Container>
|
|
125
|
+
</main>`
|
|
126
|
+
|
|
127
|
+
if (!app.tabs) return header
|
|
128
|
+
|
|
129
|
+
const actions = pages.map((page) => {
|
|
130
|
+
const icon = page.icon
|
|
131
|
+
? `
|
|
132
|
+
${expectError(app, " ", false)} icon: <Icon icon={${page.icon.name}} size="lg" />,`
|
|
133
|
+
: ""
|
|
134
|
+
return ` {
|
|
135
|
+
value: "${page.path}",
|
|
136
|
+
label: ${labelValue(app, page)},${icon}
|
|
137
|
+
},`
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return `${header}
|
|
141
|
+
<footer className="pb-safe">
|
|
142
|
+
${expectError(app, " ")} <BottomNavigation
|
|
143
|
+
value={router.path}
|
|
144
|
+
actions={[
|
|
145
|
+
${actions.join("\n")}
|
|
146
|
+
]}
|
|
147
|
+
onChange={(${typed(app, "path", "string")}) => api.notify("#router:navigate", path)}
|
|
148
|
+
/>
|
|
149
|
+
</footer>`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function plainShell(app, { pages, content }) {
|
|
153
|
+
const nav = app.routing
|
|
154
|
+
? pages.map(
|
|
155
|
+
(page) =>
|
|
156
|
+
` <a href="${page.path}">${labelText(app, page)}</a>`,
|
|
157
|
+
)
|
|
158
|
+
: []
|
|
159
|
+
if (app.i18n) {
|
|
160
|
+
nav.push(` <select
|
|
161
|
+
aria-label={t("language")}
|
|
162
|
+
onChange={${selectChange(app, " ")}
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
{i18n.languages${app.isTypeScript ? "?." : "."}map((language) => (
|
|
166
|
+
<option
|
|
167
|
+
key={language.value}
|
|
168
|
+
value={language.value}
|
|
169
|
+
selected={language.value === i18n.locale}
|
|
170
|
+
>
|
|
171
|
+
{language.label}
|
|
172
|
+
</option>
|
|
173
|
+
))}
|
|
174
|
+
</select>`)
|
|
175
|
+
}
|
|
176
|
+
const navBlock = nav.length
|
|
177
|
+
? `
|
|
178
|
+
<nav className="app-nav">
|
|
179
|
+
${nav.join("\n")}
|
|
180
|
+
</nav>`
|
|
181
|
+
: ""
|
|
182
|
+
|
|
183
|
+
return ` <header>
|
|
184
|
+
<strong>${app.projectName}</strong>${navBlock}
|
|
185
|
+
</header>
|
|
186
|
+
<main>{${content}}</main>`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function homeSource(app, { showcase }) {
|
|
190
|
+
const imports = ['import { Message } from "../types/message"']
|
|
191
|
+
for (const icon of showcase) {
|
|
192
|
+
imports.push(`import ${icon.name} from "${icon.from}"`)
|
|
193
|
+
}
|
|
194
|
+
if (showcase.length) {
|
|
195
|
+
imports.push('import { Icon } from "@inglorious/ui/icon"')
|
|
196
|
+
}
|
|
197
|
+
if (app.i18n) imports.push('import { t } from "../i18n"')
|
|
198
|
+
|
|
199
|
+
const text = (key, fallback) => (app.i18n ? `{t("${key}")}` : fallback)
|
|
200
|
+
const icons = showcase.length
|
|
201
|
+
? `
|
|
202
|
+
<p className="app-icons">
|
|
203
|
+
${showcase
|
|
204
|
+
.map(
|
|
205
|
+
(icon) =>
|
|
206
|
+
`${expectError(app, " ")} <Icon icon={${icon.name}} size="lg" label="${icon.label}" />`,
|
|
207
|
+
)
|
|
208
|
+
.join("\n")}
|
|
209
|
+
</p>`
|
|
210
|
+
: ""
|
|
211
|
+
|
|
212
|
+
return `${importBlock(imports)}
|
|
213
|
+
|
|
214
|
+
export const Home = {
|
|
215
|
+
render() {
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<h1>${text("home.title", "Welcome")}</h1>
|
|
219
|
+
<p>
|
|
220
|
+
${expectError(app, " ")} <Message entityId="message1" />, <Message entityId="message2" />,{" "}
|
|
221
|
+
${expectError(app, " ")} <Message entityId="message3" />!
|
|
222
|
+
</p>
|
|
223
|
+
<p>${text("home.hint", "Click a greeting to shout it.")}</p>${icons}
|
|
224
|
+
</>
|
|
225
|
+
)
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function aboutSource(app) {
|
|
232
|
+
const head = app.i18n ? 'import { t } from "../i18n"\n\n' : ""
|
|
233
|
+
const text = (key, fallback) => (app.i18n ? `{t("${key}")}` : fallback)
|
|
234
|
+
|
|
235
|
+
return `${head}export const About = {
|
|
236
|
+
render() {
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<h1>${text("about.title", "About")}</h1>
|
|
240
|
+
<p>${text("about.body", "A page of its own, at its own address.")}</p>
|
|
241
|
+
</>
|
|
242
|
+
)
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function messageSource(app) {
|
|
249
|
+
const imports = []
|
|
250
|
+
if (app.isTypeScript) {
|
|
251
|
+
imports.push('import type { MessageType } from "../../types"')
|
|
252
|
+
}
|
|
253
|
+
if (app.i18n) imports.push('import { t } from "../i18n"')
|
|
254
|
+
const greeting = app.i18n ? '{t("message.hello", { who })}' : "Hello {who}"
|
|
255
|
+
const declaration = app.isTypeScript
|
|
256
|
+
? "export const Message: MessageType = {"
|
|
257
|
+
: "export const Message = {"
|
|
258
|
+
|
|
259
|
+
const span = app.i18n
|
|
260
|
+
? ` <span onClick={() => api.notify(\`#\${entity.id}:click\`)}>
|
|
261
|
+
${greeting}
|
|
262
|
+
</span>`
|
|
263
|
+
: ` <span onClick={() => api.notify(\`#\${entity.id}:click\`)}>${greeting}</span>`
|
|
264
|
+
|
|
265
|
+
const head = imports.length ? `${importBlock(imports)}\n\n` : ""
|
|
266
|
+
|
|
267
|
+
return `${head}${declaration}
|
|
268
|
+
click(entity) {
|
|
269
|
+
entity.isUpperCase = !entity.isUpperCase
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
render(entity, api) {
|
|
273
|
+
const who = entity.isUpperCase ? entity.who.toUpperCase() : entity.who
|
|
274
|
+
return (
|
|
275
|
+
${span}
|
|
276
|
+
)
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
`
|
|
280
|
+
}
|