@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.
- package/README.md +58 -14
- package/bin/create-fidj.cjs +9 -1
- package/generators/app/index.js +57 -15
- package/generators/app/templates/typescript/README.md +11 -2
- package/generators/app/templates/typescript/app.config.json +5 -1
- package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/scripts/build.mjs +3 -0
- package/generators/app/templates/typescript/scripts/render-content.mjs +2 -2
- package/generators/app/templates/typescript/server/index.ts +10 -1
- package/generators/app/templates/typescript/src/content.ts +112 -37
- package/generators/app/templates/typescript/src/fonts.css +92 -0
- package/generators/app/templates/typescript/src/main.ts +17 -9
- package/generators/app/templates/typescript/src/service-agreement.ts +40 -0
- package/generators/app/templates/typescript/src/style.css +640 -363
- package/generators/app/templates/typescript/src/tokens.css +56 -0
- package/lib/app-module.cjs +6 -1
- package/lib/scaffold.cjs +72 -3
- 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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|