saasaloy 0.5.1 → 0.5.2
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 +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/templates/base/apps/web/astro.config.mjs +21 -0
- package/templates/base/apps/web/package.json +11 -10
- package/templates/base/apps/web/wrangler.jsonc +7 -0
- package/templates/base/package.json +9 -9
- package/templates/base/packages/ui/package.json +4 -4
- package/templates/base/packages/ui/src/lib/theme.ts +42 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasaloy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Composable SaaS accelerator kit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"saas",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup",
|
|
42
|
+
"clean": "rimraf dist coverage",
|
|
42
43
|
"prepack": "pnpm -w run readme:cli && pnpm run build",
|
|
43
44
|
"dev": "tsup --watch",
|
|
44
45
|
"start": "node ./dist/index.js",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"@types/node": "26.4.1",
|
|
55
56
|
"@vitest/coverage-v8": "5.0.0",
|
|
56
57
|
"release-it": "21.0.2",
|
|
58
|
+
"rimraf": "6.1.3",
|
|
57
59
|
"tsup": "8.5.1",
|
|
58
60
|
"vitest": "5.0.0"
|
|
59
61
|
},
|
|
@@ -36,6 +36,13 @@ export default defineConfig({
|
|
|
36
36
|
// `imageService: "compile"` optimises images during the build and serves the results
|
|
37
37
|
// as plain assets. The adapter's default would reach for Cloudflare's IMAGES binding
|
|
38
38
|
// at runtime, which needs a Worker this site does not have.
|
|
39
|
+
//
|
|
40
|
+
// package.json pins @cloudflare/vite-plugin, which this file never imports. The
|
|
41
|
+
// adapter takes it through a caret range, and each release of the plugin raises the
|
|
42
|
+
// wrangler version it demands at startup, so an unpinned install pairs a new plugin
|
|
43
|
+
// with our pinned wrangler and `astro dev` exits before it is ready. The direct pin
|
|
44
|
+
// makes pnpm install the same plugin for the adapter. Move it and `wrangler` together:
|
|
45
|
+
// they release as a pair, and apps/api (once added) pins the same two versions.
|
|
39
46
|
adapter: cloudflare({ imageService: "compile" }),
|
|
40
47
|
integrations: [react()],
|
|
41
48
|
// Fixed dev port. Every cross-origin consumer in this repo — the api Worker's CORS
|
|
@@ -50,6 +57,20 @@ export default defineConfig({
|
|
|
50
57
|
// packages/ui/src/styles/globals.css, which Layout.astro imports via @repo/ui.
|
|
51
58
|
plugins: [tailwindcss()],
|
|
52
59
|
server: { strictPort: true },
|
|
60
|
+
// Dev only: pre-bundle every dependency the React components import before the
|
|
61
|
+
// first request. In dev the adapter renders inside workerd, and Vite's scan starts
|
|
62
|
+
// from the pages, so it never walks into @repo/ui (a linked workspace, not an npm
|
|
63
|
+
// package) or the islands a module drops in src/. Without these entries Vite finds
|
|
64
|
+
// lucide-react, @base-ui/react and the rest mid-request, re-bundles, and reloads the
|
|
65
|
+
// worker. The re-run components then import a fresh copy of react while
|
|
66
|
+
// react-dom/server holds the old one, and every hook throws "Invalid hook call".
|
|
67
|
+
// A glob, not an include list, so a component a module adds is covered with no edit
|
|
68
|
+
// here.
|
|
69
|
+
ssr: {
|
|
70
|
+
optimizeDeps: {
|
|
71
|
+
entries: ["src/**/*.tsx", "../../packages/ui/src/**/*.tsx"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
53
74
|
resolve: {
|
|
54
75
|
// `@web` mirrors saasaloy.json's alias of the same name (apps/web/src) — that
|
|
55
76
|
// alias only drives the CLI's file-placement when a module's files[] target
|
|
@@ -4,27 +4,28 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "astro dev",
|
|
7
|
+
"dev": "astro dev --host",
|
|
8
8
|
"build": "astro build",
|
|
9
9
|
"clean": "rimraf dist .astro .wrangler",
|
|
10
10
|
"preview": "astro preview",
|
|
11
11
|
"deploy": "wrangler deploy"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"astro": "7.3.
|
|
15
|
-
"@astrojs/cloudflare": "14.3.
|
|
16
|
-
"@astrojs/react": "
|
|
17
|
-
"react": "19.
|
|
18
|
-
"react-dom": "19.
|
|
14
|
+
"astro": "7.3.5",
|
|
15
|
+
"@astrojs/cloudflare": "14.3.3",
|
|
16
|
+
"@astrojs/react": "7.0.0",
|
|
17
|
+
"react": "19.3.0",
|
|
18
|
+
"react-dom": "19.3.0",
|
|
19
19
|
"@repo/ui": "workspace:*"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"@cloudflare/vite-plugin": "1.62.3",
|
|
22
23
|
"@tailwindcss/vite": "4.3.3",
|
|
23
|
-
"@types/node": "26.
|
|
24
|
-
"@types/react": "19.
|
|
25
|
-
"@types/react-dom": "19.
|
|
24
|
+
"@types/node": "26.6.3",
|
|
25
|
+
"@types/react": "19.3.0",
|
|
26
|
+
"@types/react-dom": "19.3.0",
|
|
26
27
|
"rimraf": "6.1.3",
|
|
27
28
|
"tailwindcss": "4.3.3",
|
|
28
|
-
"wrangler": "4.
|
|
29
|
+
"wrangler": "4.145.0"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
3
|
"name": "{{PROJECT_NAME}}-web",
|
|
4
4
|
"compatibility_date": "2026-07-22",
|
|
5
|
+
// `astro dev` renders inside workerd, and @astrojs/react's server renderer keeps a
|
|
6
|
+
// dynamic `import("node:stream")` for the Node streaming path. That path never runs
|
|
7
|
+
// here (workerd resolves react-dom/server to the edge build), but without this flag
|
|
8
|
+
// @cloudflare/vite-plugin warns "Unexpected Node.js imports" on every dev start. The
|
|
9
|
+
// adapter only adds `nodejs_als` on its own. It costs nothing while the site is
|
|
10
|
+
// assets-only, and the first `prerender = false` page gets Node APIs with no edit.
|
|
11
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
5
12
|
// Static assets, and still no Worker code — but the layout moved. Registering
|
|
6
13
|
// @astrojs/cloudflare in astro.config.mjs (for src/pages/500.astro) makes Astro build
|
|
7
14
|
// to dist/client + dist/server instead of a flat dist/. While every page is
|
|
@@ -23,22 +23,22 @@
|
|
|
23
23
|
"typecheck": "turbo run typecheck"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@commitlint/cli": "21.2.
|
|
27
|
-
"@commitlint/config-conventional": "21.2.
|
|
26
|
+
"@commitlint/cli": "21.2.3",
|
|
27
|
+
"@commitlint/config-conventional": "21.2.3",
|
|
28
28
|
"husky": "9.1.7",
|
|
29
|
-
"lint-staged": "17.
|
|
30
|
-
"oxlint": "1.
|
|
31
|
-
"oxlint-tsgolint": "7.0.
|
|
32
|
-
"prettier": "3.9.
|
|
33
|
-
"prettier-plugin-astro": "
|
|
29
|
+
"lint-staged": "17.6.0",
|
|
30
|
+
"oxlint": "1.86.0",
|
|
31
|
+
"oxlint-tsgolint": "7.0.2003",
|
|
32
|
+
"prettier": "3.9.9",
|
|
33
|
+
"prettier-plugin-astro": "1.1.0",
|
|
34
34
|
"prettier-plugin-tailwindcss": "0.8.1",
|
|
35
35
|
"rimraf": "6.1.3",
|
|
36
36
|
"stylelint": "17.15.0",
|
|
37
37
|
"stylelint-config-idiomatic-order": "10.0.0",
|
|
38
38
|
"stylelint-config-standard": "40.0.0",
|
|
39
39
|
"stylelint-prettier": "5.0.3",
|
|
40
|
-
"turbo": "2.
|
|
40
|
+
"turbo": "2.11.6",
|
|
41
41
|
"typescript": "7.0.2",
|
|
42
|
-
"ultracite": "7.
|
|
42
|
+
"ultracite": "7.12.2"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"@base-ui/react": "1.8.0",
|
|
21
21
|
"class-variance-authority": "0.7.1",
|
|
22
22
|
"clsx": "2.1.1",
|
|
23
|
-
"lucide-react": "1.
|
|
23
|
+
"lucide-react": "1.49.0",
|
|
24
24
|
"shadcn": "4.21.0",
|
|
25
|
-
"tailwind-merge": "3.
|
|
25
|
+
"tailwind-merge": "3.7.0",
|
|
26
26
|
"tailwindcss": "4.3.3",
|
|
27
27
|
"tw-animate-css": "1.4.0"
|
|
28
28
|
},
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@repo/tsconfig": "workspace:*",
|
|
34
|
-
"@types/react": "19.
|
|
35
|
-
"react": "19.
|
|
34
|
+
"@types/react": "19.3.0",
|
|
35
|
+
"react": "19.3.0",
|
|
36
36
|
"rimraf": "6.1.3",
|
|
37
37
|
"typescript": "7.0.2"
|
|
38
38
|
}
|
|
@@ -124,6 +124,48 @@ export function relabelThemeToggles(theme?: Theme): void {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// The toggle's click handler, installed by `installThemeToggle` below. It lives at module
|
|
128
|
+
// scope because it reads nothing from that function, and one identity per page means a
|
|
129
|
+
// second install adds no second listener.
|
|
130
|
+
function onClick(event: MouseEvent): void {
|
|
131
|
+
const { target } = event;
|
|
132
|
+
if (!(target instanceof Element)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!target.closest(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const painted = document.documentElement.getAttribute(THEME_ATTRIBUTE);
|
|
140
|
+
// An unrecognised `data-theme` gives index -1 and starts the cycle at the head of
|
|
141
|
+
// THEME_ORDER.
|
|
142
|
+
const index = isTheme(painted) ? THEME_ORDER.indexOf(painted) : -1;
|
|
143
|
+
const next = THEME_ORDER[(index + 1) % THEME_ORDER.length] ?? "light";
|
|
144
|
+
|
|
145
|
+
setTheme(next);
|
|
146
|
+
|
|
147
|
+
// After `setTheme`, and unconditionally: a `system` press CLEARS THEME_STORAGE_KEY, so
|
|
148
|
+
// this key is the only record that the visitor pressed anything at all.
|
|
149
|
+
try {
|
|
150
|
+
localStorage.setItem(THEME_CHOICE_KEY, "1");
|
|
151
|
+
} catch {
|
|
152
|
+
// Unwritable storage costs persistence, not the current page.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The OS half of the same job, and the reason this is not click-only. THEME_INIT_SCRIPT
|
|
157
|
+
// registers its own matchMedia listener; a host that calls `installThemeToggle` instead
|
|
158
|
+
// does not have that script, so without this listener a visitor on `system` keeps the
|
|
159
|
+
// palette resolved at load until the next reload.
|
|
160
|
+
//
|
|
161
|
+
// It repaints only while the state is `system`. A visitor who picked light or dark said
|
|
162
|
+
// so deliberately, and the OS must not overrule that.
|
|
163
|
+
function onOsChange(): void {
|
|
164
|
+
if (getStoredTheme() === "system") {
|
|
165
|
+
setTheme("system");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
127
169
|
/**
|
|
128
170
|
* Install the toggle's click behaviour on `document`, and answer with a function that
|
|
129
171
|
* removes it again.
|
|
@@ -139,47 +181,8 @@ export function relabelThemeToggles(theme?: Theme): void {
|
|
|
139
181
|
* the same value forever and every press would land on the same theme.
|
|
140
182
|
*/
|
|
141
183
|
export function installThemeToggle(): () => void {
|
|
142
|
-
function onClick(event: MouseEvent): void {
|
|
143
|
-
const { target } = event;
|
|
144
|
-
if (!(target instanceof Element)) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (!target.closest(`[${THEME_TOGGLE_ATTRIBUTE}]`)) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const painted = document.documentElement.getAttribute(THEME_ATTRIBUTE);
|
|
152
|
-
// An unrecognised `data-theme` gives index -1 and starts the cycle at the head of
|
|
153
|
-
// THEME_ORDER.
|
|
154
|
-
const index = isTheme(painted) ? THEME_ORDER.indexOf(painted) : -1;
|
|
155
|
-
const next = THEME_ORDER[(index + 1) % THEME_ORDER.length] ?? "light";
|
|
156
|
-
|
|
157
|
-
setTheme(next);
|
|
158
|
-
|
|
159
|
-
// After `setTheme`, and unconditionally: a `system` press CLEARS THEME_STORAGE_KEY, so
|
|
160
|
-
// this key is the only record that the visitor pressed anything at all.
|
|
161
|
-
try {
|
|
162
|
-
localStorage.setItem(THEME_CHOICE_KEY, "1");
|
|
163
|
-
} catch {
|
|
164
|
-
// Unwritable storage costs persistence, not the current page.
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// The OS half of the same job, and the reason this is not click-only. THEME_INIT_SCRIPT
|
|
169
|
-
// registers its own matchMedia listener; a host that calls this function instead does
|
|
170
|
-
// not have that script, so without this listener a visitor on `system` keeps the palette
|
|
171
|
-
// resolved at load until the next reload.
|
|
172
|
-
//
|
|
173
|
-
// It repaints only while the state is `system`. A visitor who picked light or dark said
|
|
174
|
-
// so deliberately, and the OS must not overrule that.
|
|
175
184
|
const media = window.matchMedia(OS_DARK_QUERY);
|
|
176
185
|
|
|
177
|
-
function onOsChange(): void {
|
|
178
|
-
if (getStoredTheme() === "system") {
|
|
179
|
-
setTheme("system");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
186
|
document.addEventListener("click", onClick);
|
|
184
187
|
media.addEventListener("change", onOsChange);
|
|
185
188
|
|