@pithy-sh/ui-react 0.1.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/LICENSE +21 -0
- package/package.json +47 -0
- package/src/templates.ts +147 -0
- package/src/testing/virtualAuth.ts +12 -0
- package/src/testing/virtualI18n.ts +16 -0
- package/src/testing/virtualPayments.ts +5 -0
- package/src/testing/virtualTurnstile.ts +5 -0
- package/templates/client-env.d.ts +299 -0
- package/templates/index.html +14 -0
- package/templates/src/client.test.tsx +98 -0
- package/templates/src/client.tsx +51 -0
- package/templates/src/payments.tsx +147 -0
- package/templates/src/pithy-config.tsx +93 -0
- package/templates/src/pithy-locale.test.tsx +140 -0
- package/templates/src/pithy-locale.tsx +134 -0
- package/templates/src/pithy-screens.css +379 -0
- package/templates/src/router.test.tsx +63 -0
- package/templates/src/router.tsx +618 -0
- package/templates/src/routes/app/home.bare.tsx +96 -0
- package/templates/src/routes/app/home.tsx +41 -0
- package/templates/src/routes/pithy/callback.tsx +42 -0
- package/templates/src/routes/pithy/otp.tsx +127 -0
- package/templates/src/routes/pithy/paywall.tsx +160 -0
- package/templates/src/routes/pithy/pricing.tsx +312 -0
- package/templates/src/routes/pithy/sign-in.test.tsx +116 -0
- package/templates/src/routes/pithy/sign-in.tsx +470 -0
- package/templates/src/routes/pithy/subscription.tsx +183 -0
- package/templates/src/session.tsx +78 -0
- package/templates/src/styles.css +53 -0
- package/templates/src/turnstile.test.tsx +119 -0
- package/templates/src/turnstile.tsx +105 -0
- package/templates/tsconfig.client.json +28 -0
- package/templates/tsconfig.node.json +22 -0
- package/templates/vite.config.ts +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pithy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pithy-sh/ui-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/pithy-sh/pithy.git",
|
|
8
|
+
"directory": "packages/ui-react"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"!src/**/*.test.*"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./src/*": "./src/*.ts"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json --noEmit false --outDir dist",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.templates.json",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:node": "vitest run --project=node --project=dom",
|
|
27
|
+
"clean": "rm -rf dist .turbo",
|
|
28
|
+
"reset": "bun run clean && rm -rf node_modules"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@pithy-sh/auth": "workspace:*",
|
|
32
|
+
"@pithy-sh/core": "workspace:*",
|
|
33
|
+
"@pithy-sh/i18n": "workspace:*",
|
|
34
|
+
"@pithy-sh/payments": "workspace:*",
|
|
35
|
+
"@pithy-sh/tsconfig": "workspace:*",
|
|
36
|
+
"@types/node": "^22.15.0",
|
|
37
|
+
"@types/react": "^19.2.17",
|
|
38
|
+
"@types/react-dom": "^19.2.3",
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
40
|
+
"happy-dom": "^20.11.1",
|
|
41
|
+
"react": "^19.2.8",
|
|
42
|
+
"react-dom": "^19.2.8",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"vite": "^8.0.16",
|
|
45
|
+
"vitest": "^4.1.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The React 19 front-end templates `pithy ui add react` copies into a Worker.
|
|
9
|
+
*
|
|
10
|
+
* This is a template library, not a runtime library: it ships text, has no dependencies of its own,
|
|
11
|
+
* and nothing imports it at runtime except the CLI's scaffolder. It is a real package rather than a
|
|
12
|
+
* folder inside `@pithy-sh/cli` for one reason that only gets truer with time — **a framework's
|
|
13
|
+
* templates need that framework's toolchain to be checked.** React needs `react`, `@types/react` and
|
|
14
|
+
* a `jsx: react-jsx` program; Svelte would need an entirely different set. Folding them into the CLI
|
|
15
|
+
* makes the CLI accumulate every framework's devDependencies and every framework's tsconfig, to
|
|
16
|
+
* typecheck files it only ever copies.
|
|
17
|
+
*
|
|
18
|
+
* The tree mirrors the scaffolded layout exactly. That is what lets a screen's `../../router` import
|
|
19
|
+
* resolve in the template as well as in the Worker it lands in, so the whole library typechecks in
|
|
20
|
+
* place instead of only after it has been written somewhere.
|
|
21
|
+
*
|
|
22
|
+
* **Grouping is the manifest's job, not the directory's.** The tree is one layout; which files a
|
|
23
|
+
* given invocation writes is chosen by {@link TEMPLATE_GROUPS}. That is why the auth screens sit in
|
|
24
|
+
* their final `src/routes/pithy/` home rather than an `auth/` subtree, and it is how a later screen
|
|
25
|
+
* set — payments, say — joins: add its files to the tree, name them in a new group, and no path,
|
|
26
|
+
* import, or contract moves.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Absolute path to the template tree. Resolved from this module, so it holds in a workspace and once published. */
|
|
30
|
+
export const TEMPLATE_DIR: string = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The named file groups this library offers, each keyed by path within {@link TEMPLATE_DIR}.
|
|
34
|
+
*
|
|
35
|
+
* `base` is every template, always. Every other group is a screen set that rides on a capability
|
|
36
|
+
* being composed — `auth` and `payments` today, and whatever follows is an additive entry here with
|
|
37
|
+
* nothing about the CLI's stub contract having to change to admit it.
|
|
38
|
+
*
|
|
39
|
+
* **`src/pithy-config.tsx` is base, not auth.** It is the one module that imports the
|
|
40
|
+
* `virtual:pithy/*` modules and narrows each on `enabled`, for *every* capability — `docs/UI.md` says
|
|
41
|
+
* so in as many words, and a payments-only scaffold would otherwise never get the file its screens
|
|
42
|
+
* read. It compiles with nothing composed: each projection is then `{ enabled: false }` and the
|
|
43
|
+
* narrowing falls to the defaults it declares.
|
|
44
|
+
*
|
|
45
|
+
* ## Adding a file here is agreeing to one rule
|
|
46
|
+
*
|
|
47
|
+
* > **Every seeded file whose invariant an adopter can break silently ships with the gate that
|
|
48
|
+
* > notices.**
|
|
49
|
+
*
|
|
50
|
+
* `docs/CONVENTIONS.md` § *Seeded files* is the whole statement, with the three properties a seeded
|
|
51
|
+
* gate must meet and the two questions to answer before writing one. `seededGates.test.ts` beside this
|
|
52
|
+
* file is what makes the question due: a path added below is red until the ledger there says which gate
|
|
53
|
+
* holds it, or why none does. The reason is the deliverable either way — a decline with an argument is
|
|
54
|
+
* an answer, and this list growing quietly is not.
|
|
55
|
+
*/
|
|
56
|
+
export const TEMPLATE_GROUPS = {
|
|
57
|
+
base: [
|
|
58
|
+
"index.html",
|
|
59
|
+
"vite.config.ts",
|
|
60
|
+
"tsconfig.client.json",
|
|
61
|
+
"tsconfig.node.json",
|
|
62
|
+
"client-env.d.ts",
|
|
63
|
+
"src/client.tsx",
|
|
64
|
+
// **The gate travels with the file it protects (#383, #391).** `src/client.tsx` used to find its
|
|
65
|
+
// mount node by an id declared in `index.html`, and renaming that div rendered an empty page with
|
|
66
|
+
// no error at all (#394). The node is created in code now, and this is what keeps it that way in a
|
|
67
|
+
// repository Pithy will never see again.
|
|
68
|
+
"src/client.test.tsx",
|
|
69
|
+
"src/pithy-config.tsx",
|
|
70
|
+
// **The language the document declares and the language the page renders are one value, and this
|
|
71
|
+
// file is where it is resolved.** `client.tsx` used to negotiate a locale, put it on `<html lang>`,
|
|
72
|
+
// and mount no translator at all — so a project composing i18n served `lang="es"` over a page of
|
|
73
|
+
// English, which a screen reader believes and mispronounces. Base rather than an i18n group, for
|
|
74
|
+
// `pithy-config.tsx`'s reason: it compiles with nothing composed, where it renders its children
|
|
75
|
+
// untouched and no catalog is ever fetched.
|
|
76
|
+
"src/pithy-locale.tsx",
|
|
77
|
+
// The gate travels with it: the two halves are asserted from one mount, which is the only way to
|
|
78
|
+
// state the invariant at all.
|
|
79
|
+
"src/pithy-locale.test.tsx",
|
|
80
|
+
"src/router.tsx",
|
|
81
|
+
// The router's half of #393: a guard's destination is the path the screen declares, and this is
|
|
82
|
+
// what keeps it so once `router.tsx` is a file in somebody else's repository.
|
|
83
|
+
"src/router.test.tsx",
|
|
84
|
+
"src/styles.css",
|
|
85
|
+
// **`src/pithy-screens.css` is base, and it is base for the same reason `pithy-config.tsx` is.**
|
|
86
|
+
// It carries every class name a Pithy screen renders, and it is written whenever it is absent —
|
|
87
|
+
// which is what makes a *backfill* (`--auth` on a project scaffolded `--no-auth`) produce screens
|
|
88
|
+
// that render styled. Putting it in the `auth` group instead would leave a payments-only scaffold
|
|
89
|
+
// unstyled, and duplicating it across both groups would name one file twice.
|
|
90
|
+
//
|
|
91
|
+
// The adopter's `src/styles.css` is correctly skipped on that backfill — it is theirs. Before this
|
|
92
|
+
// file existed, the classes lived in it, so the run wrote a sign-in screen whose `stack`, `divider`
|
|
93
|
+
// and `secondary` nothing defined and reported it as created.
|
|
94
|
+
// The palette these two files share is #391's item I, and its gate is the one that could NOT be
|
|
95
|
+
// seeded: the invariant is CSS text, and a scaffolded project's runner stubs CSS modules to the
|
|
96
|
+
// empty string, so a seeded gate would sweep nothing and pass. It is kept in `palette.test.ts`,
|
|
97
|
+
// with that reason at the site — see `seededGates.test.ts`, which records it rather than letting
|
|
98
|
+
// the ledger look complete.
|
|
99
|
+
"src/pithy-screens.css",
|
|
100
|
+
],
|
|
101
|
+
auth: [
|
|
102
|
+
"src/session.tsx",
|
|
103
|
+
"src/turnstile.tsx",
|
|
104
|
+
// **The gate travels with the file it protects (#383).** `src/turnstile.tsx` is the adopter's the
|
|
105
|
+
// moment it lands, and its whole risk is that the action label can be retyped into it — a drift
|
|
106
|
+
// no environment before production can see, because the test keys dev and staging run answer with
|
|
107
|
+
// no action to compare (#374). The kit keeping the only gate for that leaves the adopter most
|
|
108
|
+
// likely to hit it with nothing that goes red. So this one is seeded, not kept.
|
|
109
|
+
"src/turnstile.test.tsx",
|
|
110
|
+
"src/routes/pithy/sign-in.tsx",
|
|
111
|
+
// The magic link's `callbackURL` and `callback.tsx`'s `path` are one statement, and this is the
|
|
112
|
+
// gate that keeps them one after the screens are yours (#393). Seeded for the same reason the
|
|
113
|
+
// widget's is: the round trip it protects is the one flow nobody already signed in can test.
|
|
114
|
+
"src/routes/pithy/sign-in.test.tsx",
|
|
115
|
+
"src/routes/pithy/otp.tsx",
|
|
116
|
+
"src/routes/pithy/callback.tsx",
|
|
117
|
+
],
|
|
118
|
+
payments: [
|
|
119
|
+
"src/payments.tsx",
|
|
120
|
+
"src/routes/pithy/paywall.tsx",
|
|
121
|
+
// The pricing screen ships even for a project with no Paddle rail, and renders its own empty state
|
|
122
|
+
// there. The argument against shipping one at all is that a pricing page is the most brand-specific
|
|
123
|
+
// screen there is — which is true of the paywall too, and did not stop that one. What settles it is
|
|
124
|
+
// that the alternative is every adopter writing the same PricePreview plumbing by hand, getting the
|
|
125
|
+
// tax convention wrong in one direction or the other, and freezing it into their own repository.
|
|
126
|
+
"src/routes/pithy/pricing.tsx",
|
|
127
|
+
"src/routes/pithy/subscription.tsx",
|
|
128
|
+
],
|
|
129
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
130
|
+
|
|
131
|
+
/** A group name this library offers. */
|
|
132
|
+
export type TemplateGroup = keyof typeof TEMPLATE_GROUPS;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The adopter's own home screen, which exists in two variants because it is the one file that
|
|
136
|
+
* differs between templates: the auth one carries the signed-in guard, the bare one does one typed
|
|
137
|
+
* `fetch`. Both live in the tree so both compile against the router and the session hook; exactly
|
|
138
|
+
* one is ever written, and always to the same place.
|
|
139
|
+
*/
|
|
140
|
+
export const HOME_SCREEN = {
|
|
141
|
+
target: "src/routes/app/home.tsx",
|
|
142
|
+
auth: "src/routes/app/home.tsx",
|
|
143
|
+
bare: "src/routes/app/home.bare.tsx",
|
|
144
|
+
} as const;
|
|
145
|
+
|
|
146
|
+
/** The token every template uses where the Worker's name belongs. */
|
|
147
|
+
export const WORKER_TOKEN = "__PITHY_WORKER__";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What `virtual:pithy/auth` resolves to under vitest.
|
|
6
|
+
*
|
|
7
|
+
* `{ enabled: false }` and nothing else — the exact shape `@pithy-sh/vite` serves for a capability the
|
|
8
|
+
* Worker does not compose. A screen under test takes its projection as a prop, so this value is never
|
|
9
|
+
* the one asserted against; it exists so importing a template module does not have to resolve a module
|
|
10
|
+
* that only a Vite build can produce.
|
|
11
|
+
*/
|
|
12
|
+
export default { enabled: false as const };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What `virtual:pithy/i18n` resolves to under vitest.
|
|
6
|
+
*
|
|
7
|
+
* `{ enabled: false }` and nothing else — the exact shape `@pithy-sh/vite` serves for a capability the
|
|
8
|
+
* Worker does not compose, and the shape every screen has to render correctly under, because a project
|
|
9
|
+
* that never composes `i18n` is the common case.
|
|
10
|
+
*
|
|
11
|
+
* **Every `dom` test in this package needs it, whether or not it is about language.** They all import a
|
|
12
|
+
* screen, every screen imports `src/pithy-config.tsx`, and that module imports all five virtual modules
|
|
13
|
+
* to narrow them in one place. Without this alias the whole project fails to resolve, on tests that
|
|
14
|
+
* have nothing to do with locale.
|
|
15
|
+
*/
|
|
16
|
+
export default { enabled: false as const };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// The client-safe projection of this worker's composed capabilities, served by @pithy-sh/vite.
|
|
4
|
+
//
|
|
5
|
+
// Each module is a DEFAULT export whose type is a union discriminated on `enabled`. That shape is
|
|
6
|
+
// deliberate. A capability that is not composed projects `{ enabled: false }` and nothing else, so a
|
|
7
|
+
// NAMED import of any other key would be a missing export and the build would fail — on exactly the
|
|
8
|
+
// case this mechanism exists to make survivable. Importing the default and narrowing cannot fail:
|
|
9
|
+
//
|
|
10
|
+
// import turnstile from "virtual:pithy/turnstile";
|
|
11
|
+
// if (!turnstile.enabled) return null; // narrowed: sitekey, mode and token exist below this line
|
|
12
|
+
//
|
|
13
|
+
// The `virtual:pithy/*` modules are never written to disk. These declarations describe modules the
|
|
14
|
+
// Vite plugin serves, built from the Worker's own pithy.config.ts.
|
|
15
|
+
//
|
|
16
|
+
// **Generated, and copied here as it was emitted (#398).** Each declaration below is a capability's
|
|
17
|
+
// declared client projection — `src/client/projection.ts` in @pithy-sh/auth, @pithy-sh/payments,
|
|
18
|
+
// @pithy-sh/support and @pithy-sh/turnstile — written out by @pithy-sh/vite's
|
|
19
|
+
// `src/clientEnvDeclaration.ts` at kit build time. In the kit that is one statement of each shape and
|
|
20
|
+
// nothing to keep in step: a field a projection stops emitting is a compile error where it is
|
|
21
|
+
// projected, and this file moves in the same commit.
|
|
22
|
+
//
|
|
23
|
+
// In your repository it is yours, like every other seeded file. Editing it changes what your compiler
|
|
24
|
+
// believes and nothing about what the plugin serves.
|
|
25
|
+
|
|
26
|
+
declare module "virtual:pithy/auth" {
|
|
27
|
+
/**
|
|
28
|
+
* Whether this capability is composed and serving on this worker. Also the union's discriminant.
|
|
29
|
+
*
|
|
30
|
+
* The only named export, deliberately. Every other key is reached through the default export and a
|
|
31
|
+
* narrowing — see the note at the top of this file.
|
|
32
|
+
*/
|
|
33
|
+
export const enabled: boolean;
|
|
34
|
+
|
|
35
|
+
const config:
|
|
36
|
+
| {
|
|
37
|
+
/** Auth is not composed on this worker. A screen branches rather than rendering a sign-in form. */
|
|
38
|
+
enabled: false;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
/** Auth is composed on this worker. */
|
|
42
|
+
enabled: true;
|
|
43
|
+
/** The path the auth handler mounts under, e.g. `/auth`. */
|
|
44
|
+
basePath: string;
|
|
45
|
+
/**
|
|
46
|
+
* Which social providers are switched on in pithy.config.ts. Credentials never reach the client —
|
|
47
|
+
* they are not in the config this projection can see, they are in the secrets store.
|
|
48
|
+
*
|
|
49
|
+
* Nested, so a screen iterates the set rather than naming four booleans. A fifth provider is one
|
|
50
|
+
* key here — and until it is written here, projecting it is a compile error, not a surprise in
|
|
51
|
+
* somebody's bundle.
|
|
52
|
+
*/
|
|
53
|
+
providers: {
|
|
54
|
+
/** Whether Sign in with Google is offered. */
|
|
55
|
+
google: boolean;
|
|
56
|
+
/** Whether Sign in with Apple is offered. */
|
|
57
|
+
apple: boolean;
|
|
58
|
+
/** Whether Sign in with Facebook is offered. */
|
|
59
|
+
facebook: boolean;
|
|
60
|
+
/** Whether Sign in with GitHub is offered. */
|
|
61
|
+
github: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** How many digits an email OTP carries. */
|
|
64
|
+
otpLength: number;
|
|
65
|
+
/** Whether signing in may provision a new user. Drives the sign-up copy. */
|
|
66
|
+
signUpEnabled: boolean;
|
|
67
|
+
};
|
|
68
|
+
export default config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare module "virtual:pithy/i18n" {
|
|
72
|
+
/**
|
|
73
|
+
* Whether this capability is composed and serving on this worker. Also the union's discriminant.
|
|
74
|
+
*
|
|
75
|
+
* The only named export, deliberately. Every other key is reached through the default export and a
|
|
76
|
+
* narrowing — see the note at the top of this file.
|
|
77
|
+
*/
|
|
78
|
+
export const enabled: boolean;
|
|
79
|
+
|
|
80
|
+
const config:
|
|
81
|
+
| {
|
|
82
|
+
/**
|
|
83
|
+
* The i18n capability is not composed. Every screen renders the English it was scaffolded with,
|
|
84
|
+
* byte for byte as it did before any of this landed — which is what makes the capability optional.
|
|
85
|
+
*/
|
|
86
|
+
enabled: false;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
/** The i18n capability is composed, and these are the languages this project serves. */
|
|
90
|
+
enabled: true;
|
|
91
|
+
/** Every locale this project serves, as BCP-47 tags. The browser negotiates within this set. */
|
|
92
|
+
supportedLocales: string[];
|
|
93
|
+
/** The locale served when nothing in the browser chain answers. Always in `supportedLocales`. */
|
|
94
|
+
defaultLocale: string;
|
|
95
|
+
/** The query parameter an explicit choice arrives on — `?lang=es`. */
|
|
96
|
+
queryParam: string;
|
|
97
|
+
/** The `localStorage` key this device's remembered locale is written under. */
|
|
98
|
+
storageKey: string;
|
|
99
|
+
/**
|
|
100
|
+
* The browser chain, in the order it is asked: `query`, `account`, `storage`, `server`, `default`.
|
|
101
|
+
* Projected so the front end resolves in the order the project configured, not one it assumed.
|
|
102
|
+
*/
|
|
103
|
+
browserResolvers: string[];
|
|
104
|
+
/**
|
|
105
|
+
* Language ranges the matcher cannot derive, as range → supported locale. Usually empty; it
|
|
106
|
+
* carries the historical pairs (`nb` meaning `no`) that no truncation of a tag would reach.
|
|
107
|
+
*/
|
|
108
|
+
exceptions: Record<string, string>;
|
|
109
|
+
};
|
|
110
|
+
export default config;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
declare module "virtual:pithy/payments" {
|
|
114
|
+
/**
|
|
115
|
+
* Whether this capability is composed and serving on this worker. Also the union's discriminant.
|
|
116
|
+
*
|
|
117
|
+
* The only named export, deliberately. Every other key is reached through the default export and a
|
|
118
|
+
* narrowing — see the note at the top of this file.
|
|
119
|
+
*/
|
|
120
|
+
export const enabled: boolean;
|
|
121
|
+
|
|
122
|
+
const config:
|
|
123
|
+
| {
|
|
124
|
+
/**
|
|
125
|
+
* Payments is not composed, or has no catalog this environment can render. Both read the same on
|
|
126
|
+
* purpose: "composed with nothing to sell" is a paywall with nothing on it, exactly like "not
|
|
127
|
+
* composed", and a screen branches on one value rather than guarding.
|
|
128
|
+
*/
|
|
129
|
+
enabled: false;
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
/** Payments is composed AND has a catalog this environment can render. */
|
|
133
|
+
enabled: true;
|
|
134
|
+
/** The environment this bundle was built for. */
|
|
135
|
+
environment: string;
|
|
136
|
+
/**
|
|
137
|
+
* Which rails this project sells through. Apple and Google are display-only on the web — a
|
|
138
|
+
* paywall shows such a product as owned-elsewhere rather than offering a buy button nothing on
|
|
139
|
+
* the web can honor.
|
|
140
|
+
*/
|
|
141
|
+
rails: {
|
|
142
|
+
/** Whether the App Store rail is on. Display-only in a browser. */
|
|
143
|
+
apple: boolean;
|
|
144
|
+
/** Whether the Play Store rail is on. Display-only in a browser. */
|
|
145
|
+
google: boolean;
|
|
146
|
+
/** Whether the Stripe rail is on. */
|
|
147
|
+
stripe: boolean;
|
|
148
|
+
/** Whether the Lemon Squeezy rail is on. */
|
|
149
|
+
lemonSqueezy: boolean;
|
|
150
|
+
/** Whether the Paddle rail is on. */
|
|
151
|
+
paddle: boolean;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* What Paddle.js needs to initialize, or null when the rail is off. The client token is
|
|
155
|
+
* publishable by design — it is what a browser opens a checkout with — and the API key and the
|
|
156
|
+
* webhook signing secret are neither here nor expressible here.
|
|
157
|
+
*/
|
|
158
|
+
paddle: {
|
|
159
|
+
/** The publishable client token Paddle.js initializes with. */
|
|
160
|
+
clientToken: string;
|
|
161
|
+
/** Which Paddle account the token belongs to. */
|
|
162
|
+
environment: "sandbox" | "production";
|
|
163
|
+
/**
|
|
164
|
+
* How checkout is presented: `overlay` opens Paddle.js over your own page, `inline` renders it
|
|
165
|
+
* into a container the screen provides, `hosted` redirects to Paddle's own page.
|
|
166
|
+
*
|
|
167
|
+
* The union is stated, not `string`. A screen switches on this to decide whether to render a
|
|
168
|
+
* container at all, and the exhaustiveness is the point. It was the one field the hand-written
|
|
169
|
+
* `templates/client-env.d.ts` widened, and generating that file from here is what closed it.
|
|
170
|
+
*/
|
|
171
|
+
checkout: "overlay" | "inline" | "hosted";
|
|
172
|
+
} | null;
|
|
173
|
+
/** Where the payments routes mount, e.g. `/payments`. */
|
|
174
|
+
basePath: string;
|
|
175
|
+
/**
|
|
176
|
+
* The catalog, browser-safe, in the order the adopter wrote it. A web rail's price id is
|
|
177
|
+
* publishable by design — a checkout names one. Apple's and Google's SKUs, and anything a
|
|
178
|
+
* purchase fulfills beyond its entitlements, stay server-side.
|
|
179
|
+
*/
|
|
180
|
+
products: {
|
|
181
|
+
/** The logical product id — what `/payments/checkout` is asked for. */
|
|
182
|
+
id: string;
|
|
183
|
+
/** What kind of product it is. */
|
|
184
|
+
type: "consumable" | "non_consumable" | "subscription";
|
|
185
|
+
/** The entitlement keys it grants. Gating code names these, never the product. */
|
|
186
|
+
entitlements: string[];
|
|
187
|
+
/** The display name a paywall renders. */
|
|
188
|
+
name: string;
|
|
189
|
+
/**
|
|
190
|
+
* This product's SKU on each web rail, or null where it is not sold. Keyed by rail rather than one
|
|
191
|
+
* field per rail, so a screen asks `skus[rail]` and a new rail cannot leave a `purchasable()` check
|
|
192
|
+
* silently out of date. Every id here is publishable by design — each is what a checkout names.
|
|
193
|
+
*
|
|
194
|
+
* Apple's and Google's product ids are deliberately absent: a browser cannot open either store.
|
|
195
|
+
*/
|
|
196
|
+
skus: {
|
|
197
|
+
/** The Stripe price id. */
|
|
198
|
+
stripe: string | null;
|
|
199
|
+
/** The Lemon Squeezy variant id. */
|
|
200
|
+
lemonSqueezy: string | null;
|
|
201
|
+
/** The Paddle price id. */
|
|
202
|
+
paddle: string | null;
|
|
203
|
+
};
|
|
204
|
+
}[];
|
|
205
|
+
};
|
|
206
|
+
export default config;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
declare module "virtual:pithy/support" {
|
|
210
|
+
/**
|
|
211
|
+
* Whether this capability is composed and serving on this worker. Also the union's discriminant.
|
|
212
|
+
*
|
|
213
|
+
* The only named export, deliberately. Every other key is reached through the default export and a
|
|
214
|
+
* narrowing — see the note at the top of this file.
|
|
215
|
+
*/
|
|
216
|
+
export const enabled: boolean;
|
|
217
|
+
|
|
218
|
+
const config:
|
|
219
|
+
| {
|
|
220
|
+
/**
|
|
221
|
+
* Support is not composed, or is not serving the in-app submission routes — the only ones a
|
|
222
|
+
* browser calls. A screen branches rather than rendering a compose form nothing will accept.
|
|
223
|
+
*/
|
|
224
|
+
enabled: false;
|
|
225
|
+
}
|
|
226
|
+
| {
|
|
227
|
+
/** Support is composed AND serving the in-app submission routes. */
|
|
228
|
+
enabled: true;
|
|
229
|
+
/** Where the support routes mount, e.g. `/support`. `POST {basePath}/feedback` writes in. */
|
|
230
|
+
basePath: string;
|
|
231
|
+
/**
|
|
232
|
+
* What one submission may carry. Hold a compose form to these so the handler does not have to
|
|
233
|
+
* refuse after somebody pressed Send. The taxonomy is not here: a category's text is the
|
|
234
|
+
* instruction a classifier reads, not copy for a chooser.
|
|
235
|
+
*/
|
|
236
|
+
submission: {
|
|
237
|
+
/** The longest subject accepted. It becomes the thread's name in the inbox. */
|
|
238
|
+
maxSubjectChars: number;
|
|
239
|
+
/** The longest report body accepted. */
|
|
240
|
+
maxBodyChars: number;
|
|
241
|
+
/**
|
|
242
|
+
* What an upload control may offer, or null when attachments are off and it renders none.
|
|
243
|
+
* Null rather than absent: the projection is inlined with `JSON.stringify`, which drops an
|
|
244
|
+
* undefined value and leaves a screen reading a key that is simply gone.
|
|
245
|
+
*/
|
|
246
|
+
attachments: {
|
|
247
|
+
/** How many files one submission may carry. */
|
|
248
|
+
maxCount: number;
|
|
249
|
+
/** The largest single file, measured on the decoded bytes. */
|
|
250
|
+
maxBytes: number;
|
|
251
|
+
/** The exact MIME types accepted — an allowlist, and the `accept` a file input wants. */
|
|
252
|
+
allowedContentTypes: string[];
|
|
253
|
+
} | null;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
export default config;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
declare module "virtual:pithy/turnstile" {
|
|
260
|
+
/**
|
|
261
|
+
* Whether this capability is composed and serving on this worker. Also the union's discriminant.
|
|
262
|
+
*
|
|
263
|
+
* The only named export, deliberately. Every other key is reached through the default export and a
|
|
264
|
+
* narrowing — see the note at the top of this file.
|
|
265
|
+
*/
|
|
266
|
+
export const enabled: boolean;
|
|
267
|
+
|
|
268
|
+
const config:
|
|
269
|
+
| {
|
|
270
|
+
/**
|
|
271
|
+
* Turnstile is not composed, or has no renderable login widget for this environment. A screen
|
|
272
|
+
* branches rather than mounting a widget that cannot solve.
|
|
273
|
+
*/
|
|
274
|
+
enabled: false;
|
|
275
|
+
}
|
|
276
|
+
| {
|
|
277
|
+
/** Turnstile is composed AND has a renderable login widget for this environment. */
|
|
278
|
+
enabled: true;
|
|
279
|
+
/** The public sitekey for the build's environment. The widget secret stays in the secrets store. */
|
|
280
|
+
sitekey: string;
|
|
281
|
+
/** The widget mode `protect.login` names. */
|
|
282
|
+
mode: "visible" | "invisible";
|
|
283
|
+
/**
|
|
284
|
+
* The action label the widget must be solved for. Render it, never retype it: the sign-in route
|
|
285
|
+
* asserts this exact string against the token, and dev and staging cannot notice a copy that has
|
|
286
|
+
* drifted — Cloudflare's test keys answer with no action at all, so the first environment that
|
|
287
|
+
* can tell is the one where a mismatch refuses every sign-in. #377.
|
|
288
|
+
*/
|
|
289
|
+
action: string;
|
|
290
|
+
/** Where the response token goes: a body field, or a header when one is configured. */
|
|
291
|
+
token: {
|
|
292
|
+
/** The body field the middleware reads the token from. */
|
|
293
|
+
field: string;
|
|
294
|
+
/** The header it reads instead, or null when none is configured. */
|
|
295
|
+
header: string | null;
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
export default config;
|
|
299
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>__PITHY_WORKER__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<!-- No mount node here, on purpose. `src/client.tsx` creates the one it renders into, so there is no
|
|
9
|
+
id in this file for a rename to break silently (#394). Anything you add below is left alone; the
|
|
10
|
+
app mounts after it. -->
|
|
11
|
+
<body>
|
|
12
|
+
<script type="module" src="/src/client.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|