@inglorious/create-app 1.7.1 → 1.8.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.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/bin/index.js +19 -4
  3. package/bin/lib/app-jsx.js +0 -11
  4. package/bin/lib/app-lit.js +0 -5
  5. package/bin/lib/app-vue.js +0 -6
  6. package/bin/lib/app.js +121 -28
  7. package/bin/lib/cli.js +2 -2
  8. package/bin/lib/constants.js +3 -2
  9. package/bin/lib/features.js +103 -6
  10. package/bin/lib/firebase.js +177 -0
  11. package/bin/lib/imports.js +0 -9
  12. package/bin/lib/mobile.js +0 -5
  13. package/bin/lib/prompts.js +311 -0
  14. package/bin/lib/pwa.js +21 -16
  15. package/bin/lib/questions.js +29 -7
  16. package/bin/lib/skills.js +11 -2
  17. package/bin/lib/source.js +4 -11
  18. package/package.json +1 -1
  19. package/templates/features/analytics/scripts/analytics-off-native.mjs +31 -0
  20. package/templates/features/push/public/sw-push.js +23 -0
  21. package/templates/features/push/scripts/push-vault-secrets.mjs +60 -0
  22. package/templates/features/push/supabase/functions/deno.json +6 -0
  23. package/templates/features/push/supabase/functions/push-notify/index.ts +187 -0
  24. package/templates/features/push/supabase/migrations/push.sql +115 -0
  25. package/templates/features/push/supabase/seed.sql +20 -0
  26. package/templates/js/package.json +1 -1
  27. package/templates/js/vite.config.js +0 -1
  28. package/templates/js-jsx/package.json +1 -1
  29. package/templates/js-jsx/vite.config.js +0 -1
  30. package/templates/js-vue/package.json +1 -1
  31. package/templates/js-vue/vite.config.js +0 -1
  32. package/templates/ssx-js/package.json +1 -1
  33. package/templates/ssx-js/src/site.config.js +0 -1
  34. package/templates/ssx-js-jsx/package.json +1 -1
  35. package/templates/ssx-js-jsx/src/site.config.js +0 -1
  36. package/templates/ssx-js-vue/package.json +1 -1
  37. package/templates/ssx-js-vue/src/pages/about.js +0 -1
  38. package/templates/ssx-js-vue/src/pages/index.js +0 -1
  39. package/templates/ssx-js-vue/src/site.config.js +0 -1
  40. package/templates/ssx-ts/package.json +1 -1
  41. package/templates/ssx-ts/src/site.config.ts +0 -1
  42. package/templates/ssx-ts/types/index.d.ts +0 -7
  43. package/templates/ssx-ts-jsx/package.json +1 -1
  44. package/templates/ssx-ts-jsx/src/site.config.ts +0 -1
  45. package/templates/ssx-ts-jsx/types/index.d.ts +0 -7
  46. package/templates/ssx-ts-vue/package.json +1 -1
  47. package/templates/ssx-ts-vue/src/pages/about.ts +0 -1
  48. package/templates/ssx-ts-vue/src/pages/index.ts +0 -1
  49. package/templates/ssx-ts-vue/src/site.config.ts +0 -1
  50. package/templates/ssx-ts-vue/types/index.d.ts +0 -6
  51. package/templates/ts/package.json +1 -1
  52. package/templates/ts/types/index.d.ts +0 -7
  53. package/templates/ts/vite.config.ts +0 -2
  54. package/templates/ts-jsx/package.json +1 -1
  55. package/templates/ts-jsx/types/index.d.ts +0 -7
  56. package/templates/ts-jsx/vite.config.ts +0 -2
  57. package/templates/ts-vue/package.json +1 -1
  58. package/templates/ts-vue/types/index.d.ts +0 -6
  59. package/templates/ts-vue/vite.config.ts +0 -2
package/README.md CHANGED
@@ -34,9 +34,11 @@ For the JavaScript and TypeScript Vite templates, the CLI then asks about the ap
34
34
  | Animate page transitions? | `--transitions` / `--no-transitions` | View transitions between pages (with routing and Inglorious UI) |
35
35
  | Which icon packs? | `--icons=material,fontawesome,lucide` / `--no-icons` | Iconify icon sets, drawn by Inglorious UI's `Icon` (with Inglorious UI) |
36
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
+ | Add analytics? | `--analytics` / `--no-analytics` | Google Analytics for Firebase behind a consent prompt, on the web and in the native apps |
38
+ | Add push notifications? | `--push` / `--no-push` | Firebase Cloud Messaging, a permission prompt and the Supabase side of sending; turns on PWA support |
37
39
  | Enable PWA support? | `--pwa` / `--no-pwa` | `vite-plugin-pwa`, a manifest, icons, and a service worker that keeps installed apps on the latest version |
38
40
 
39
- Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, and `i18n` when the project uses them. If the install fails, the CLI prints the command to retry it.
41
+ Every template except `minimal` then asks whether to add agent skills (`--skills` / `--no-skills`). Saying yes runs the [skills CLI](https://skills.sh) in the new project. It asks which coding agents to install for, and installs the [Inglorious Forge skills](https://github.com/IngloriousCoderz/forge-skills) that match your choices: `forge`, `forge-best-practices`, `store`, `web` and `web-best-practices` always, plus `ssx`, `vite-plugin-jsx` or `vite-plugin-vue`, `ui` and `ui-best-practices`, `i18n`, `firebase` and `supabase` when the project uses them. If the install fails, the CLI prints the command to retry it.
40
42
 
41
43
  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:
42
44
 
@@ -47,9 +49,11 @@ The app is generated in the syntax you chose: lit-html, JSX or Vue. Every questi
47
49
  | Select a rendering syntax | `--syntax=lit-html\|jsx\|vue` |
48
50
 
49
51
  ```bash
50
- npm create @inglorious/app@latest -- my-app --template=ts --syntax=jsx --ui --theme=material-3 --routing --transitions --icons=material --languages=en,it --no-mobile --no-pwa --skills
52
+ npm create @inglorious/app@latest -- my-app --template=ts --syntax=jsx --ui --theme=material-3 --routing --transitions --icons=material --languages=en,it --no-mobile --no-analytics --no-push --no-pwa --skills
51
53
  ```
52
54
 
55
+ Analytics and push generate their setup steps into the new project's README, and a `.env.example`. Push also generates a Supabase migration, the `push-notify` edge function and a service worker: a push is sent by inserting a row in `notifications`. It needs a Firebase project, a Supabase project and a sign-in screen, since the permission prompt appears once someone is signed in with Supabase Auth.
56
+
53
57
  ## Templates
54
58
 
55
59
  The following templates are available to get you started:
package/bin/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  } from "./lib/constants.js"
19
19
  import { SYNTAXES, TEMPLATES } from "./lib/features.js"
20
20
  import { copyDir } from "./lib/file.js"
21
+ import { addFirebaseSupport } from "./lib/firebase.js"
21
22
  import { addMobileSupport } from "./lib/mobile.js"
22
23
  import { addPwaSupport } from "./lib/pwa.js"
23
24
  import { askAppFeatures } from "./lib/questions.js"
@@ -93,6 +94,8 @@ async function main() {
93
94
  languages: [],
94
95
  mobile: false,
95
96
  pwa: false,
97
+ analytics: false,
98
+ push: false,
96
99
  }
97
100
 
98
101
  if (VITE_APP_TEMPLATES.has(baseTemplate)) {
@@ -102,9 +105,14 @@ async function main() {
102
105
  console.log("Operation canceled.")
103
106
  return
104
107
  }
105
- } else if (cliOptions.pwa || cliOptions.mobile) {
108
+ } else if (
109
+ cliOptions.pwa ||
110
+ cliOptions.mobile ||
111
+ cliOptions.analytics ||
112
+ cliOptions.push
113
+ ) {
106
114
  console.warn(
107
- "PWA and mobile options are only available for JavaScript and TypeScript Vite app templates.",
115
+ "PWA, mobile, analytics and push options are only available for JavaScript and TypeScript Vite app templates.",
108
116
  )
109
117
  }
110
118
 
@@ -167,7 +175,9 @@ async function main() {
167
175
  features.ui ||
168
176
  features.routing ||
169
177
  Boolean(features.icons.length) ||
170
- Boolean(features.languages.length)
178
+ Boolean(features.languages.length) ||
179
+ features.analytics ||
180
+ features.push
171
181
 
172
182
  if (hasAppFeatures) {
173
183
  spinner.text = "Adding app features..."
@@ -181,7 +191,7 @@ async function main() {
181
191
 
182
192
  if (features.pwa) {
183
193
  spinner.text = "Adding PWA support..."
184
- addPwaSupport(targetDir, pkg, projectName)
194
+ addPwaSupport(targetDir, pkg, projectName, { push: features.push })
185
195
  }
186
196
 
187
197
  if (features.mobile) {
@@ -189,6 +199,11 @@ async function main() {
189
199
  addMobileSupport(targetDir, pkg, projectName)
190
200
  }
191
201
 
202
+ if (features.analytics || features.push) {
203
+ spinner.text = "Adding Firebase support..."
204
+ addFirebaseSupport(targetDir, pkg, { ...features, templatesRoot })
205
+ }
206
+
192
207
  for (const depType of ["dependencies", "devDependencies"]) {
193
208
  if (pkg[depType]) {
194
209
  for (const [name, version] of Object.entries(pkg[depType])) {
@@ -1,9 +1,3 @@
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
1
  import { renderLocals, selectChange } from "./app-lit.js"
8
2
  import { importBlock } from "./imports.js"
9
3
 
@@ -152,21 +146,16 @@ ${span}
152
146
  `
153
147
  }
154
148
 
155
- // The comment line before a capitalized tag in .tsx: `{/* */}` among JSX
156
- // children, `//` inside code.
157
149
  function expectError(app, indent, isChild = true) {
158
150
  if (!app.isTypeScript) return ""
159
151
  const comment = isChild ? `{/* ${EXPECT_ERROR} */}` : `// ${EXPECT_ERROR}`
160
152
  return `${indent}${comment}\n`
161
153
  }
162
154
 
163
- // A callback's parameter. The components' props are not typed in .tsx, so
164
- // TypeScript cannot infer it.
165
155
  function typed(app, name, type) {
166
156
  return app.isTypeScript ? `${name}: ${type}` : name
167
157
  }
168
158
 
169
- // A page's label: translated, or the English text.
170
159
  function labelText(app, page) {
171
160
  return app.i18n ? `{t("${page.key}")}` : page.text
172
161
  }
@@ -1,5 +1,3 @@
1
- // The app's components in lit-html: tagged templates in .js or .ts files.
2
-
3
1
  import { importBlock } from "./imports.js"
4
2
 
5
3
  export const extension = (app) => (app.isTypeScript ? "ts" : "js")
@@ -53,7 +51,6 @@ ${template}
53
51
  `
54
52
  }
55
53
 
56
- // The app's own entities are always there; TypeScript cannot know that.
57
54
  export function renderLocals(app) {
58
55
  const always = app.isTypeScript ? "!" : ""
59
56
  const locals = []
@@ -64,8 +61,6 @@ export function renderLocals(app) {
64
61
  return locals.length ? `${locals.join("\n")}\n\n` : ""
65
62
  }
66
63
 
67
- // The native language picker's change handler, laid out as Prettier would at
68
- // `indent`. TypeScript has to be told what the event's target is.
69
64
  export function selectChange(app, indent) {
70
65
  if (!app.isTypeScript) {
71
66
  return `(event) =>
@@ -1,8 +1,3 @@
1
- // The app's components as Vue single-file components, compiled to lit-html by
2
- // @inglorious/vite-plugin-vue. Imported Inglorious UI components are tags with
3
- // props; imported .vue components with `v-bind="id"` render store entities;
4
- // script values that read `api` are computed on every render.
5
-
6
1
  import { importBlock } from "./imports.js"
7
2
  import { LINE_WIDTH } from "./source.js"
8
3
 
@@ -173,7 +168,6 @@ ${nav.join("\n")}
173
168
 
174
169
  if (!app.tabs) return header
175
170
 
176
- // Each action on one line when it fits, one property per line otherwise.
177
171
  const actions = pages.map((page) => {
178
172
  const fields = [
179
173
  `value: '${page.path}'`,
package/bin/lib/app.js CHANGED
@@ -5,9 +5,16 @@ import path from "path"
5
5
  import * as jsxComponents from "./app-jsx.js"
6
6
  import * as litComponents from "./app-lit.js"
7
7
  import * as vueComponents from "./app-vue.js"
8
- import { INDENTATION } from "./constants.js"
9
- import { ICON_PACKS, LANGUAGES } from "./features.js"
8
+ import {
9
+ CAPACITOR_FIREBASE_VERSION,
10
+ CAPACITOR_VERSION,
11
+ FIREBASE_VERSION,
12
+ INDENTATION,
13
+ SUPABASE_VERSION,
14
+ } from "./constants.js"
15
+ import { ICON_PACKS, LANGUAGES, PROMPT_TEXTS } from "./features.js"
10
16
  import { importBlock } from "./imports.js"
17
+ import { envExample, promptFiles } from "./prompts.js"
11
18
  import { appModel, call, LINE_WIDTH, union } from "./source.js"
12
19
 
13
20
  const COMPONENTS = {
@@ -18,8 +25,8 @@ const COMPONENTS = {
18
25
 
19
26
  /**
20
27
  * Writes the source of an app with the features chosen: Inglorious UI,
21
- * routing, page transitions, icon packs, internationalization and a mobile
22
- * layout, in lit-html, JSX or Vue. It replaces the template's hello world,
28
+ * routing, page transitions, icon packs, internationalization, a mobile
29
+ * layout, analytics and push prompts, in lit-html, JSX or Vue. It replaces the template's hello world,
23
30
  * keeping its message type as the example on the home page. The files come
24
31
  * out as Prettier and the template's ESLint config would leave them.
25
32
  *
@@ -36,14 +43,16 @@ const COMPONENTS = {
36
43
  * @param {boolean} options.transitions
37
44
  * @param {string[]} options.icons - Keys of ICON_PACKS.
38
45
  * @param {string[]} options.languages - Keys of LANGUAGES, the default first.
46
+ * @param {boolean} options.analytics
47
+ * @param {boolean} options.push
39
48
  */
40
49
  export function addAppFeatures(targetDir, pkg, options) {
41
50
  const app = { ...options, ext: options.isTypeScript ? "ts" : "js" }
42
51
  app.i18n = Boolean(app.languages.length)
43
- // On a phone the pages are tabs at the bottom; elsewhere, links on top.
44
52
  app.tabs = app.ui && app.routing && app.mobile
45
53
  app.links = app.routing && !app.tabs
46
54
  app.isVue = app.renderer === "vue"
55
+ app.hasPrompts = app.analytics || app.push
47
56
 
48
57
  const components = COMPONENTS[app.renderer]
49
58
  const componentExt = components.extension(app)
@@ -68,9 +77,13 @@ export function addAppFeatures(targetDir, pkg, options) {
68
77
  files[`src/i18n/index.${app.ext}`] = i18nSource(app)
69
78
  for (const language of app.languages) {
70
79
  files[`src/i18n/locales/${language}.json`] =
71
- JSON.stringify(LANGUAGES[language].texts, null, INDENTATION) + EOL
80
+ JSON.stringify(catalogue(app, language), null, INDENTATION) + EOL
72
81
  }
73
82
  }
83
+ if (app.hasPrompts) {
84
+ Object.assign(files, promptFiles(app))
85
+ files[".env.example"] = envExample(app)
86
+ }
74
87
  if (app.isTypeScript) {
75
88
  files["types/index.d.ts"] = typesSource(app)
76
89
  }
@@ -91,9 +104,31 @@ function addDependencies(pkg, app) {
91
104
  for (const key of app.icons) {
92
105
  Object.assign(pkg.dependencies, ICON_PACKS[key].packages)
93
106
  }
107
+ if (app.hasPrompts) {
108
+ pkg.dependencies["@inglorious/firebase"] = "workspace:*"
109
+ pkg.dependencies["@capacitor/core"] = CAPACITOR_VERSION
110
+ pkg.dependencies.firebase = FIREBASE_VERSION
111
+ }
112
+ if (app.analytics) {
113
+ pkg.dependencies["@capacitor-firebase/analytics"] =
114
+ CAPACITOR_FIREBASE_VERSION
115
+ }
116
+ if (app.push) {
117
+ pkg.dependencies["@capacitor-firebase/messaging"] =
118
+ CAPACITOR_FIREBASE_VERSION
119
+ pkg.dependencies["@supabase/supabase-js"] = SUPABASE_VERSION
120
+ }
121
+ }
122
+
123
+ function catalogue(app, language) {
124
+ const { analytics, push } = PROMPT_TEXTS[language]
125
+ return {
126
+ ...LANGUAGES[language].texts,
127
+ ...(app.analytics && { analytics }),
128
+ ...(app.push && { push }),
129
+ }
94
130
  }
95
131
 
96
- // The theme is a class on <body>, which the theme's stylesheet is scoped to.
97
132
  function applyTheme(targetDir, app) {
98
133
  const htmlPath = path.join(targetDir, "index.html")
99
134
  if (!fs.existsSync(htmlPath)) return
@@ -114,8 +149,6 @@ function componentImport(app, names, from) {
114
149
  : line
115
150
  }
116
151
 
117
- // ---------------------------------------------------------------- main
118
-
119
152
  function mainSource(app) {
120
153
  const imports = []
121
154
  if (app.ui) {
@@ -130,15 +163,21 @@ function mainSource(app) {
130
163
  }
131
164
  imports.push('import "./app.css"')
132
165
  imports.push(
133
- 'import { mount } from "@inglorious/web"',
166
+ `import { ${app.hasPrompts ? "html, mount" : "mount"} } from "@inglorious/web"`,
134
167
  componentImport(app, "App", "./app"),
135
168
  'import { store } from "./store"',
136
169
  )
137
170
  if (app.i18n) imports.push('import { ready } from "./i18n"')
171
+ if (app.push) imports.push('import { supabase } from "./api/supabase"')
172
+ if (app.hasPrompts) {
173
+ imports.push('import { renderPrompts } from "./common/prompt"')
174
+ }
138
175
 
139
176
  const args = [
140
177
  "store",
141
- "(api) => App.render(null, api)",
178
+ app.hasPrompts
179
+ ? "(api) => html`${App.render(null, api)} ${renderPrompts(api)}`"
180
+ : "(api) => App.render(null, api)",
142
181
  app.isTypeScript
143
182
  ? 'document.getElementById("root")!'
144
183
  : 'document.getElementById("root")',
@@ -152,22 +191,56 @@ ${call("mount", args, " ", ",")}
152
191
  )`
153
192
  : call("mount", args, "", "")
154
193
 
194
+ const session = app.push
195
+ ? `
196
+
197
+ supabase.auth.onAuthStateChange((_event, session) => {
198
+ store.notify(session ? "#pushPrompt:pushStart" : "#pushPrompt:pushStop")
199
+ })`
200
+ : ""
201
+
155
202
  return `${importBlock(imports)}
156
203
 
157
- ${body}
204
+ ${body}${session}
158
205
  `
159
206
  }
160
207
 
161
- // The Inglorious UI components the app uses, for their stylesheets.
162
208
  function uiComponents(app) {
163
209
  const names = ["app-bar", "container"]
164
210
  if (app.links) names.push("link")
165
211
  if (app.tabs) names.push("bottom-navigation")
166
212
  if (app.icons.length) names.push("icon")
167
213
  if (app.i18n) names.push("select")
214
+ if (app.hasPrompts) names.push("snackbar", "button", "flex")
168
215
  return names
169
216
  }
170
217
 
218
+ const PROMPT_ABOVE_TABS = `
219
+ body {
220
+ --iw-snackbar-offset-bottom: 5rem;
221
+ }
222
+ `
223
+
224
+ const PLAIN_PROMPT = `
225
+ .app-prompt {
226
+ position: fixed;
227
+ inset-block-end: 1rem;
228
+ inset-inline: 1rem;
229
+ display: flex;
230
+ flex-wrap: wrap;
231
+ align-items: center;
232
+ gap: 0.5rem;
233
+ padding: 0.75rem 1rem;
234
+ border: 1px solid #ddd;
235
+ background: #fff;
236
+ }
237
+
238
+ .app-prompt p {
239
+ flex: 1 1 100%;
240
+ margin: 0;
241
+ }
242
+ `
243
+
171
244
  function appStyles(app) {
172
245
  const layout = `body {
173
246
  min-height: 100dvh;
@@ -191,10 +264,8 @@ main {
191
264
  align-items: center;
192
265
  gap: 1rem;
193
266
  }
194
- `
267
+ ${app.tabs && app.hasPrompts ? PROMPT_ABOVE_TABS : ""}`
195
268
  if (app.ui) {
196
- // In Vue the navigation is the app bar's content rather than its trailing
197
- // slot, so it pushes itself to the end.
198
269
  return app.isVue
199
270
  ? `${layout}
200
271
  .iw-app-bar .app-nav {
@@ -203,7 +274,7 @@ main {
203
274
  `
204
275
  : layout
205
276
  }
206
- return `${layout}
277
+ return `${layout}${app.hasPrompts ? PLAIN_PROMPT : ""}
207
278
  header {
208
279
  display: flex;
209
280
  align-items: center;
@@ -218,8 +289,6 @@ main {
218
289
  `
219
290
  }
220
291
 
221
- // ---------------------------------------------------------------- store
222
-
223
292
  function storeSource(app) {
224
293
  const imports = [
225
294
  'import { createStore } from "@inglorious/store"',
@@ -244,10 +313,18 @@ function storeSource(app) {
244
313
  imports.push('import type { AppEntity, AppState } from "../../types"')
245
314
  }
246
315
  if (app.i18n) imports.push('import { I18n } from "../i18n"')
316
+ if (app.analytics) {
317
+ imports.push('import { Analytics } from "../types/analytics"')
318
+ }
319
+ if (app.push) {
320
+ imports.push('import { PushPrompt } from "../types/push-prompt"')
321
+ }
247
322
 
248
323
  const types = ["Message", "Home"]
249
324
  if (app.routing) types.push("About", "Router")
250
325
  if (app.i18n) types.push("I18n")
326
+ if (app.analytics) types.push("Analytics")
327
+ if (app.push) types.push("PushPrompt")
251
328
  const oneLine = ` types: { ${types.join(", ")} },`
252
329
  const typesValue =
253
330
  oneLine.length <= LINE_WIDTH
@@ -269,17 +346,15 @@ ${typesValue}
269
346
  `
270
347
  }
271
348
 
272
- // The entities the store creates on its own, as the comment in the store
273
- // lists them.
274
349
  function singletons(app) {
275
350
  const ids = ["home"]
276
351
  if (app.routing) ids.push("about", "router")
277
352
  if (app.i18n) ids.push("i18n")
353
+ if (app.analytics) ids.push("analytics")
354
+ if (app.push) ids.push("pushPrompt")
278
355
  return ids
279
356
  }
280
357
 
281
- // Only the entities that need data, or come more than one to a type; the
282
- // store creates the rest.
283
358
  function entitiesSource(app) {
284
359
  const head = app.isTypeScript
285
360
  ? `import type { AppState } from "../../types"
@@ -309,8 +384,6 @@ export const routes = {`
309
384
  `
310
385
  }
311
386
 
312
- // ---------------------------------------------------------------- i18n
313
-
314
387
  function i18nSource(app) {
315
388
  const [defaultLocale] = app.languages
316
389
  return `import { createI18n } from "@inglorious/i18n"
@@ -325,12 +398,10 @@ export const { I18n, t, ready } = createI18n({
325
398
  `
326
399
  }
327
400
 
328
- // ---------------------------------------------------------------- types
329
-
330
401
  function typesSource(app) {
331
402
  const imports = [
332
403
  'import "./vite-end"',
333
- 'import type {\n BaseEntity,\n EntitiesState,\n EntityType,\n} from "@inglorious/store"',
404
+ 'import type { BaseEntity, EntitiesState, EntityType } from "@inglorious/store"',
334
405
  'import type { Api, TemplateResult } from "@inglorious/web"',
335
406
  ]
336
407
  const entities = ["MessageEntity", "HomeEntity"]
@@ -375,6 +446,28 @@ function typesSource(app) {
375
446
  )
376
447
  state.unshift(" i18n: AppI18nEntity")
377
448
  }
449
+ if (app.analytics) {
450
+ imports.push(
451
+ 'import type { AnalyticsEntity } from "@inglorious/web/analytics"',
452
+ )
453
+ entities.push("AppAnalyticsEntity")
454
+ declarations.push(
455
+ `export interface AppAnalyticsEntity extends AnalyticsEntity {
456
+ type: "Analytics"
457
+ }`,
458
+ )
459
+ state.push(" analytics: AppAnalyticsEntity")
460
+ }
461
+ if (app.push) {
462
+ imports.push('import type { PushPromptEntity } from "@inglorious/web/push"')
463
+ entities.push("AppPushPromptEntity")
464
+ declarations.push(
465
+ `export interface AppPushPromptEntity extends PushPromptEntity {
466
+ type: "PushPrompt"
467
+ }`,
468
+ )
469
+ state.push(" pushPrompt: AppPushPromptEntity")
470
+ }
378
471
 
379
472
  return `${importBlock(imports)}
380
473
 
package/bin/lib/cli.js CHANGED
@@ -5,6 +5,8 @@ export function parseCliOptions(args) {
5
5
  syntax: readValueFlag(args, "syntax"),
6
6
  mobile: readBooleanFlag(args, "mobile"),
7
7
  pwa: readBooleanFlag(args, "pwa"),
8
+ analytics: readBooleanFlag(args, "analytics"),
9
+ push: readBooleanFlag(args, "push"),
8
10
  ui: readBooleanFlag(args, "ui"),
9
11
  theme: readValueFlag(args, "theme"),
10
12
  routing: readBooleanFlag(args, "routing"),
@@ -28,14 +30,12 @@ export function readBooleanFlag(args, name) {
28
30
  return null
29
31
  }
30
32
 
31
- // `--name=value`, or null when the flag is not given.
32
33
  export function readValueFlag(args, name) {
33
34
  const prefix = `--${name}=`
34
35
  const arg = args.find((value) => value.startsWith(prefix))
35
36
  return arg ? arg.slice(prefix.length) : null
36
37
  }
37
38
 
38
- // `--name=a,b`, `--no-name` for an empty list, or null when neither is given.
39
39
  export function readListFlag(args, name) {
40
40
  if (args.includes(`--no-${name}`)) {
41
41
  return []
@@ -1,8 +1,9 @@
1
1
  export const CAPACITOR_VERSION = "^8.5.3"
2
+ export const CAPACITOR_FIREBASE_VERSION = "^8.5.2"
3
+ export const FIREBASE_VERSION = "^12.19.0"
4
+ export const SUPABASE_VERSION = "^2.118.0"
2
5
  export const VITE_PLUGIN_PWA_VERSION = "^2.0.0"
3
6
  export const VITE_PWA_ASSETS_GENERATOR_VERSION = "^2.0.0"
4
- // TypeScript 7 has no JavaScript API, which typescript-eslint and the
5
- // Capacitor CLI import, so `typescript` stays on 6.
6
7
  export const TYPESCRIPT_VERSION = "npm:@typescript/typescript6@^6.0.2"
7
8
  export const WORKBOX_WINDOW_VERSION = "^7.4.1"
8
9
 
@@ -1,5 +1,3 @@
1
- // The choices create-app offers for lit-html apps, and what each one brings.
2
-
3
1
  export const TEMPLATES = {
4
2
  minimal: {
5
3
  title: "Minimal",
@@ -41,9 +39,6 @@ export const THEMES = {
41
39
  "ios-26": { title: "iOS 26", description: "Liquid Glass inspired" },
42
40
  }
43
41
 
44
- // Icon sets from Iconify: one module per icon, so only the icons an app
45
- // imports are bundled. `nav` names the icons for the two example pages,
46
- // `showcase` the one shown on the home page.
47
42
  export const ICON_PACKS = {
48
43
  material: {
49
44
  title: "Material Symbols",
@@ -93,7 +88,6 @@ export const ICON_PACKS = {
93
88
  },
94
89
  }
95
90
 
96
- // The languages offered for the example app, with its few texts in each.
97
91
  export const LANGUAGES = {
98
92
  en: language("English", {
99
93
  nav: { home: "Home", about: "About" },
@@ -168,6 +162,109 @@ export const LANGUAGES = {
168
162
  }),
169
163
  }
170
164
 
165
+ export const PROMPT_TEXTS = {
166
+ en: {
167
+ analytics: {
168
+ prompt:
169
+ "May we measure how you use this app? It helps us make it better.",
170
+ accept: "Allow",
171
+ decline: "No thanks",
172
+ },
173
+ push: {
174
+ prompt: "Turn on push notifications?",
175
+ ask: "Notify me",
176
+ decline: "Not now",
177
+ },
178
+ },
179
+ it: {
180
+ analytics: {
181
+ prompt: "Possiamo misurare come usi questa app? Ci aiuta a migliorarla.",
182
+ accept: "Consenti",
183
+ decline: "No, grazie",
184
+ },
185
+ push: {
186
+ prompt: "Attivare le notifiche push?",
187
+ ask: "Avvisami",
188
+ decline: "Non ora",
189
+ },
190
+ },
191
+ fr: {
192
+ analytics: {
193
+ prompt:
194
+ "Pouvons-nous mesurer comment vous utilisez cette app\u00a0? Cela nous aide à l'améliorer.",
195
+ accept: "Autoriser",
196
+ decline: "Non merci",
197
+ },
198
+ push: {
199
+ prompt: "Activer les notifications push\u00a0?",
200
+ ask: "Me prévenir",
201
+ decline: "Plus tard",
202
+ },
203
+ },
204
+ de: {
205
+ analytics: {
206
+ prompt:
207
+ "Dürfen wir messen, wie du diese App nutzt? Das hilft uns, sie zu verbessern.",
208
+ accept: "Erlauben",
209
+ decline: "Nein, danke",
210
+ },
211
+ push: {
212
+ prompt: "Push-Benachrichtigungen aktivieren?",
213
+ ask: "Benachrichtigen",
214
+ decline: "Nicht jetzt",
215
+ },
216
+ },
217
+ es: {
218
+ analytics: {
219
+ prompt: "¿Podemos medir cómo usas esta app? Nos ayuda a mejorarla.",
220
+ accept: "Permitir",
221
+ decline: "No, gracias",
222
+ },
223
+ push: {
224
+ prompt: "¿Activar las notificaciones push?",
225
+ ask: "Avisarme",
226
+ decline: "Ahora no",
227
+ },
228
+ },
229
+ pt: {
230
+ analytics: {
231
+ prompt: "Podemos medir como usas esta app? Ajuda-nos a melhorá-la.",
232
+ accept: "Permitir",
233
+ decline: "Não, obrigado",
234
+ },
235
+ push: {
236
+ prompt: "Ativar as notificações push?",
237
+ ask: "Avisar-me",
238
+ decline: "Agora não",
239
+ },
240
+ },
241
+ ja: {
242
+ analytics: {
243
+ prompt: "このアプリの使われ方を計測してもよいですか?改善に役立てます。",
244
+ accept: "許可する",
245
+ decline: "許可しない",
246
+ },
247
+ push: {
248
+ prompt: "プッシュ通知をオンにしますか?",
249
+ ask: "通知する",
250
+ decline: "今はしない",
251
+ },
252
+ },
253
+ ar: {
254
+ analytics: {
255
+ prompt:
256
+ "هل تسمح لنا بقياس طريقة استخدامك لهذا التطبيق؟ يساعدنا ذلك على تحسينه.",
257
+ accept: "سماح",
258
+ decline: "لا، شكرًا",
259
+ },
260
+ push: {
261
+ prompt: "هل تريد تفعيل الإشعارات الفورية؟",
262
+ ask: "أعلمني",
263
+ decline: "ليس الآن",
264
+ },
265
+ },
266
+ }
267
+
171
268
  function from(name, source) {
172
269
  return { name, from: source }
173
270
  }