@ofidj/generator-fidj 1.0.1 → 1.0.3

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 (26) hide show
  1. package/README.md +58 -14
  2. package/bin/create-fidj.cjs +9 -1
  3. package/generators/app/index.js +57 -15
  4. package/generators/app/templates/typescript/README.md +11 -2
  5. package/generators/app/templates/typescript/app.config.json +5 -1
  6. package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
  7. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
  8. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
  9. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
  10. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
  11. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
  12. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
  13. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
  14. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
  15. package/generators/app/templates/typescript/scripts/build.mjs +3 -0
  16. package/generators/app/templates/typescript/scripts/render-content.mjs +2 -2
  17. package/generators/app/templates/typescript/server/index.ts +10 -1
  18. package/generators/app/templates/typescript/src/content.ts +112 -37
  19. package/generators/app/templates/typescript/src/fonts.css +92 -0
  20. package/generators/app/templates/typescript/src/main.ts +17 -9
  21. package/generators/app/templates/typescript/src/service-agreement.ts +40 -0
  22. package/generators/app/templates/typescript/src/style.css +640 -363
  23. package/generators/app/templates/typescript/src/tokens.css +56 -0
  24. package/lib/app-module.cjs +6 -1
  25. package/lib/scaffold.cjs +72 -3
  26. package/package.json +2 -6
@@ -0,0 +1,92 @@
1
+ /* The design system's typefaces, self-hosted.
2
+ *
3
+ * A generated site has to stay statically hostable, and an app that sells
4
+ * privacy should not hand a third party a request log on every sign-in. Latin
5
+ * and latin-ext only; other scripts fall back to the stack in tokens.css.
6
+ *
7
+ * The url()s are runtime paths relative to the built main.css, not bundle
8
+ * inputs — the files ride along in public/ and esbuild leaves them external.
9
+ * A consumer that serves the same tokens from somewhere else (fidj-app mounts
10
+ * its console under /module/) writes its own @font-face block instead.
11
+ */
12
+
13
+ @font-face {
14
+ font-family: "IBM Plex Sans";
15
+ font-style: normal;
16
+ font-weight: 400 600;
17
+ font-display: swap;
18
+ src: url("fonts/IBMPlexSans-400_600-latin.woff2") format("woff2");
19
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
20
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
21
+ U+2212, U+2215, U+FEFF, U+FFFD;
22
+ }
23
+ @font-face {
24
+ font-family: "IBM Plex Sans";
25
+ font-style: normal;
26
+ font-weight: 400 600;
27
+ font-display: swap;
28
+ src: url("fonts/IBMPlexSans-400_600-latin-ext.woff2") format("woff2");
29
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
30
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
31
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
32
+ }
33
+ @font-face {
34
+ font-family: "IBM Plex Mono";
35
+ font-style: normal;
36
+ font-weight: 400;
37
+ font-display: swap;
38
+ src: url("fonts/IBMPlexMono-400-latin.woff2") format("woff2");
39
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
40
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
41
+ U+2212, U+2215, U+FEFF, U+FFFD;
42
+ }
43
+ @font-face {
44
+ font-family: "IBM Plex Mono";
45
+ font-style: normal;
46
+ font-weight: 400;
47
+ font-display: swap;
48
+ src: url("fonts/IBMPlexMono-400-latin-ext.woff2") format("woff2");
49
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
50
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
51
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
52
+ }
53
+ @font-face {
54
+ font-family: "IBM Plex Mono";
55
+ font-style: normal;
56
+ font-weight: 500;
57
+ font-display: swap;
58
+ src: url("fonts/IBMPlexMono-500-latin.woff2") format("woff2");
59
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
60
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
61
+ U+2212, U+2215, U+FEFF, U+FFFD;
62
+ }
63
+ @font-face {
64
+ font-family: "IBM Plex Mono";
65
+ font-style: normal;
66
+ font-weight: 500;
67
+ font-display: swap;
68
+ src: url("fonts/IBMPlexMono-500-latin-ext.woff2") format("woff2");
69
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
70
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
71
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
72
+ }
73
+ @font-face {
74
+ font-family: "Instrument Serif";
75
+ font-style: normal;
76
+ font-weight: 400;
77
+ font-display: swap;
78
+ src: url("fonts/InstrumentSerif-400-latin.woff2") format("woff2");
79
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
80
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
81
+ U+2212, U+2215, U+FEFF, U+FFFD;
82
+ }
83
+ @font-face {
84
+ font-family: "Instrument Serif";
85
+ font-style: normal;
86
+ font-weight: 400;
87
+ font-display: swap;
88
+ src: url("fonts/InstrumentSerif-400-latin-ext.woff2") format("woff2");
89
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
90
+ U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
91
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
92
+ }
@@ -1,3 +1,4 @@
1
+ import {agreementMarkup, bindAgreement, acceptedAgreement} from "./service-agreement";
1
2
  import { FidjNodeService } from "@ofidj/node";
2
3
  import "./style.css";
3
4
 
@@ -23,6 +24,7 @@ let view = "workspace";
23
24
  let notice = "";
24
25
  let error = "";
25
26
  let busy = false;
27
+ let leaving = false;
26
28
  const escape = (value: unknown) =>
27
29
  String(value ?? "").replace(
28
30
  /[&<>"']/g,
@@ -83,7 +85,7 @@ function render() {
83
85
  <main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
84
86
  ${
85
87
  !session
86
- ? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2><p>Sign in with your Fidj account.</p><label for="email">Email</label><input id="email" type="email" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" required><button class="primary" type="submit">Sign in</button>${settings.localDemo ? `<div class="demo"><strong>Try the local example</strong><p>Alex owns the app. Maya and Sam start with the Free role.</p><button type="button" data-demo="alex">Alex · owner</button><button type="button" data-demo="maya">Maya · member</button><button type="button" data-demo="sam">Sam · member</button></div>` : ""}</form></section>`
88
+ ? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2><p>Sign in with your Fidj account.</p><label for="email">Email</label><input id="email" type="email" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" required>${agreementMarkup()}<button class="primary" type="submit" disabled>Continue</button>${settings.localDemo ? `<div class="demo"><strong>Try the local example</strong><p>Alex owns the app. Maya and Sam start with the Free role.</p><button type="button" data-demo="alex">Alex · owner</button><button type="button" data-demo="maya">Maya · member</button><button type="button" data-demo="sam">Sam · member</button></div>` : ""}</form></section>`
87
89
  : `
88
90
  <div class="page-heading"><div><p class="eyebrow">YOUR WORKSPACE</p><h1>A place to think.</h1><p>${escape(session.username)} <span class="roles">${session.roles.map(escape).join(" · ") || "No assigned roles"}</span></p></div><button id="signout">Sign out</button></div>
89
91
  <nav><button id="workspace-tab" class="${view === "workspace" ? "selected" : ""}">My notes</button><button id="privacy-tab" class="${view === "privacy" ? "selected" : ""}">My privacy</button><button id="refresh">Refresh access</button></nav>
@@ -101,16 +103,19 @@ function render() {
101
103
  )
102
104
  .join("")
103
105
  : "<p>No changes yet.</p>"
104
- }<hr><h2>Leave this app</h2><p>This removes your app membership and this starter’s notes. Your Fidj account and other memberships remain.</p>${session.roles.includes("Owner") ? "<p>As the app owner, resolve ownership before leaving.</p>" : '<button id="leave" class="danger">Leave and erase my app data</button>'}<p class="fineprint">Exports here include your Fidj membership and this starter’s notes. The registered app-data handler lets Fidj export and erase these notes too. If cleanup is pending, retry from My privacy on Fidj. Minimal completion receipts are retained; backups and unregistered systems are outside this operation.</p></article></section>`
106
+ }<hr><h2>Leave this app</h2><p>This removes your app membership and this starter’s notes. Your Fidj account and other memberships remain.</p>${session.roles.includes("Owner") ? "<p>As the app owner, resolve ownership before leaving.</p>" : leaving ? '<div role="alertdialog" aria-labelledby="leave-title"><h3 id="leave-title">Confirm departure</h3><p>Your membership, consent and notes in this app will be removed. Your Fidj account and other apps remain available.</p><button id="confirm-leave" class="danger">Confirm leaving and erase</button><button id="cancel-leave">Keep my membership</button></div>' : '<button id="leave" class="danger">Leave and erase my app data</button>'}<p class="fineprint">Exports here include your Fidj membership and this starter’s notes. The registered app-data handler lets Fidj export and erase these notes too. If cleanup is pending, retry from My privacy on Fidj. Minimal completion receipts are retained; backups and unregistered systems are outside this operation.</p></article></section>`
105
107
  }`
106
108
  }
107
109
  <footer>Built with Fidj · One identity. Separate choices for every app.</footer></main>`;
110
+ void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId);
108
111
  el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
109
112
  event.preventDefault();
113
+ const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
114
+ if (!acceptance) return;
110
115
  const email = el<HTMLInputElement>("email").value;
111
116
  const password = el<HTMLInputElement>("password").value;
112
117
  void action(async () => {
113
- await sdk.login(email, password, { autoSignup: false });
118
+ await sdk.login(email, password, { autoSignup: false, ...acceptance });
114
119
  await load();
115
120
  });
116
121
  });
@@ -202,12 +207,14 @@ function render() {
202
207
  }),
203
208
  );
204
209
  el("leave")?.addEventListener("click", () => {
205
- if (
206
- !confirm(
207
- `Leave ${settings.title}? Your membership, consent and notes here will be removed. Other apps remain available.`,
208
- )
209
- )
210
- return;
210
+ leaving = true;
211
+ render();
212
+ });
213
+ el("cancel-leave")?.addEventListener("click", () => {
214
+ leaving = false;
215
+ render();
216
+ });
217
+ el("confirm-leave")?.addEventListener("click", () => {
211
218
  void action(async () => {
212
219
  const result = await api("privacy/leave", "DELETE", {
213
220
  confirm: settings.appId,
@@ -216,6 +223,7 @@ function render() {
216
223
  session = null;
217
224
  notes = [];
218
225
  privacy = null;
226
+ leaving = false;
219
227
  notice =
220
228
  result.status === "pending"
221
229
  ? "Access revoked. Cleanup is pending. Open My privacy on Fidj and choose Retry cleanup."
@@ -0,0 +1,40 @@
1
+ export function agreementMarkup() {
2
+ return `<div class="signin-agreement"><label class="agreement-choice"><input id="service-agreement" type="checkbox" required disabled><span id="agreement-label">I accept the service agreement for this app.</span></label><button type="button" id="read-agreement" disabled>Read service agreement</button><p id="agreement-status" class="fineprint" role="status">Loading service agreement…</p></div><dialog id="agreement-dialog" aria-labelledby="agreement-heading"><h2 id="agreement-heading">Service agreement</h2><p id="agreement-version"></p><p id="agreement-text"></p><button type="button" id="close-agreement">Close agreement</button></dialog>`;
3
+ }
4
+
5
+ export function acceptedAgreement(form: HTMLFormElement) {
6
+ const checkbox = form.querySelector<HTMLInputElement>("#service-agreement");
7
+ if (!checkbox?.checked || checkbox.disabled || !checkbox.dataset.version) return null;
8
+ return { termsAccepted: true, termsVersion: checkbox.dataset.version };
9
+ }
10
+
11
+ export async function bindAgreement(form: HTMLFormElement | null, title: string, endpoint: string, appId: string) {
12
+ if (!form) return;
13
+ const checkbox = form.querySelector<HTMLInputElement>("#service-agreement")!;
14
+ const read = form.querySelector<HTMLButtonElement>("#read-agreement")!;
15
+ const dialog = form.querySelector<HTMLDialogElement>("#agreement-dialog")!;
16
+ const status = form.querySelector<HTMLElement>("#agreement-status")!;
17
+ const submitButtons = form.querySelectorAll<HTMLButtonElement>('button[type="submit"]');
18
+ const update = () => submitButtons.forEach(button => { button.disabled = !acceptedAgreement(form); });
19
+ form.querySelector("#agreement-label")!.textContent = `I accept the service agreement for ${title}.`;
20
+ update();
21
+ checkbox.addEventListener("change", update);
22
+ read.addEventListener("click", () => dialog.showModal());
23
+ form.querySelector("#close-agreement")!.addEventListener("click", () => dialog.close());
24
+ try {
25
+ const response = await fetch(`${endpoint}/apps/${encodeURIComponent(appId)}`, {signal: AbortSignal.timeout(10000)});
26
+ if (!response.ok) throw new Error("Agreement unavailable");
27
+ const agreement = (await response.json()).app?.agreement;
28
+ if (!agreement || typeof agreement.version !== "string" || !agreement.version || typeof agreement.text !== "string" || !agreement.text) throw new Error("Agreement unavailable");
29
+ if (!form.isConnected) return;
30
+ checkbox.dataset.version = agreement.version;
31
+ checkbox.disabled = false;
32
+ read.disabled = false;
33
+ dialog.querySelector("#agreement-version")!.textContent = `Version ${agreement.version}`;
34
+ dialog.querySelector("#agreement-text")!.textContent = agreement.text;
35
+ status.textContent = "Required to sign in. Optional data choices stay separate.";
36
+ update();
37
+ } catch {
38
+ if (form.isConnected) status.textContent = "The service agreement could not be loaded. Reload this page to try again.";
39
+ }
40
+ }