@inglorious/create-app 1.1.2 → 1.2.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 +15 -7
- package/bin/index.js +34 -39
- 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 +31 -0
- package/bin/lib/constants.js +3 -3
- package/bin/lib/features.js +142 -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 +3 -3
- 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 +7 -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 +7 -6
- package/templates/ts-jsx/src/app.tsx +2 -2
- 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 +7 -6
- 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,25 @@ 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 by a flag is skipped:
|
|
27
40
|
|
|
28
41
|
```bash
|
|
29
|
-
npm create @inglorious/app@latest -- --
|
|
30
|
-
npm create @inglorious/app@latest -- --mobile
|
|
31
|
-
npm create @inglorious/app@latest -- --pwa --mobile
|
|
42
|
+
npm create @inglorious/app@latest -- --ui --theme=material --routing --transitions --icons=material --languages=en,it --mobile
|
|
32
43
|
```
|
|
33
44
|
|
|
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
45
|
## Templates
|
|
38
46
|
|
|
39
47
|
The following templates are available to get you started:
|
package/bin/index.js
CHANGED
|
@@ -8,9 +8,9 @@ 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,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { copyDir } from "./lib/file.js"
|
|
20
20
|
import { addMobileSupport } from "./lib/mobile.js"
|
|
21
21
|
import { addPwaSupport } from "./lib/pwa.js"
|
|
22
|
+
import { askAppFeatures } from "./lib/questions.js"
|
|
22
23
|
|
|
23
24
|
async function main() {
|
|
24
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -82,8 +83,6 @@ async function main() {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
let renderer = "lit-html"
|
|
85
|
-
let pwa = cliOptions.pwa === true
|
|
86
|
-
let mobile = cliOptions.mobile === true
|
|
87
86
|
|
|
88
87
|
if (baseTemplate !== "minimal") {
|
|
89
88
|
const { selectedRenderer } = await prompts(
|
|
@@ -120,46 +119,26 @@ async function main() {
|
|
|
120
119
|
renderer = selectedRenderer
|
|
121
120
|
}
|
|
122
121
|
|
|
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 })
|
|
122
|
+
let features = {
|
|
123
|
+
ui: false,
|
|
124
|
+
routing: false,
|
|
125
|
+
icons: [],
|
|
126
|
+
languages: [],
|
|
127
|
+
mobile: false,
|
|
128
|
+
pwa: false,
|
|
129
|
+
}
|
|
148
130
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
152
|
-
}
|
|
131
|
+
if (VITE_APP_TEMPLATES.has(baseTemplate)) {
|
|
132
|
+
features = await askAppFeatures(cliOptions)
|
|
153
133
|
|
|
154
|
-
|
|
155
|
-
|
|
134
|
+
if (!features) {
|
|
135
|
+
console.log("Operation canceled.")
|
|
136
|
+
return
|
|
156
137
|
}
|
|
157
|
-
} else if (pwa || mobile) {
|
|
138
|
+
} else if (cliOptions.pwa || cliOptions.mobile) {
|
|
158
139
|
console.warn(
|
|
159
140
|
"PWA and mobile options are only available for JavaScript and TypeScript Vite app templates.",
|
|
160
141
|
)
|
|
161
|
-
pwa = false
|
|
162
|
-
mobile = false
|
|
163
142
|
}
|
|
164
143
|
|
|
165
144
|
const spinner = ora(`Creating project "${projectName}"...`).start()
|
|
@@ -194,12 +173,28 @@ async function main() {
|
|
|
194
173
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
195
174
|
pkg.name = projectName
|
|
196
175
|
|
|
197
|
-
|
|
176
|
+
const hasAppFeatures =
|
|
177
|
+
features.ui ||
|
|
178
|
+
features.routing ||
|
|
179
|
+
Boolean(features.icons.length) ||
|
|
180
|
+
Boolean(features.languages.length)
|
|
181
|
+
|
|
182
|
+
if (hasAppFeatures) {
|
|
183
|
+
spinner.text = "Adding app features..."
|
|
184
|
+
addAppFeatures(targetDir, pkg, {
|
|
185
|
+
...features,
|
|
186
|
+
projectName,
|
|
187
|
+
isTypeScript: baseTemplate === "ts",
|
|
188
|
+
renderer,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (features.pwa) {
|
|
198
193
|
spinner.text = "Adding PWA support..."
|
|
199
194
|
addPwaSupport(targetDir, pkg, projectName)
|
|
200
195
|
}
|
|
201
196
|
|
|
202
|
-
if (mobile) {
|
|
197
|
+
if (features.mobile) {
|
|
203
198
|
spinner.text = "Adding Capacitor support..."
|
|
204
199
|
addMobileSupport(targetDir, pkg, projectName)
|
|
205
200
|
}
|
|
@@ -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
|
+
}
|