create-zerotal 1.0.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/package.json +43 -0
- package/src/index.ts +136 -0
- package/src/prompts.ts +81 -0
- package/src/scaffold.ts +104 -0
- package/templates/admin/_env.example +2 -0
- package/templates/admin/_gitignore +4 -0
- package/templates/admin/app/admin/ProductResource.ts +192 -0
- package/templates/admin/app/admin/SettingsResource.ts +37 -0
- package/templates/admin/app/admin/UserResource.ts +103 -0
- package/templates/admin/app/admin/index.ts +46 -0
- package/templates/admin/app/admin/shared.ts +36 -0
- package/templates/admin/app/admin/widgets.ts +51 -0
- package/templates/admin/app/models/Product.ts +21 -0
- package/templates/admin/app/models/Setting.ts +14 -0
- package/templates/admin/app/models/User.ts +20 -0
- package/templates/admin/bootstrap/app.ts +16 -0
- package/templates/admin/bootstrap/providers.ts +23 -0
- package/templates/admin/config/app.ts +22 -0
- package/templates/admin/config/auth.ts +5 -0
- package/templates/admin/config/cache.ts +7 -0
- package/templates/admin/config/database.ts +14 -0
- package/templates/admin/config/session.ts +10 -0
- package/templates/admin/database/seeders/DatabaseSeeder.ts +50 -0
- package/templates/admin/package.json +18 -0
- package/templates/admin/tests/admin_test.ts +58 -0
- package/templates/admin/tsconfig.json +20 -0
- package/templates/admin/zt.ts +15 -0
- package/templates/api/_env.example +4 -0
- package/templates/api/_gitignore +5 -0
- package/templates/api/app/controllers/AuthController.ts +69 -0
- package/templates/api/app/controllers/WelcomeController.ts +10 -0
- package/templates/api/app/middleware/RequireAuth.ts +10 -0
- package/templates/api/app/models/User.ts +16 -0
- package/templates/api/bootstrap/app.ts +19 -0
- package/templates/api/config/app.ts +8 -0
- package/templates/api/config/database.ts +6 -0
- package/templates/api/config/notifications.ts +13 -0
- package/templates/api/config/queue.ts +6 -0
- package/templates/api/config/session.ts +10 -0
- package/templates/api/database/migrations/0001_create_users_table.ts +18 -0
- package/templates/api/package.json +16 -0
- package/templates/api/routes/index.ts +13 -0
- package/templates/api/tests/README.md +77 -0
- package/templates/api/tests/auth.test.ts +134 -0
- package/templates/api/tests/helpers.ts +36 -0
- package/templates/api/tsconfig.json +14 -0
- package/templates/api/zt.ts +16 -0
- package/templates/flow/_env.example +2 -0
- package/templates/flow/_gitignore +6 -0
- package/templates/flow/app/flow/layouts/app.tsx +52 -0
- package/templates/flow/app/flow/pages/about.tsx +35 -0
- package/templates/flow/app/flow/pages/contact.tsx +47 -0
- package/templates/flow/app/flow/pages/index.tsx +48 -0
- package/templates/flow/bootstrap/app.ts +8 -0
- package/templates/flow/bootstrap/providers.ts +6 -0
- package/templates/flow/config/app.ts +28 -0
- package/templates/flow/package.json +20 -0
- package/templates/flow/public/zt.svg +17 -0
- package/templates/flow/resources/css/app.css +1 -0
- package/templates/flow/resources/js/app.ts +10 -0
- package/templates/flow/resources/js/env.d.ts +23 -0
- package/templates/flow/tests/smoke.test.ts +56 -0
- package/templates/flow/tsconfig.json +18 -0
- package/templates/flow/zt.ts +15 -0
- package/templates/minimal/_env.example +2 -0
- package/templates/minimal/_gitignore +6 -0
- package/templates/minimal/bootstrap/app.ts +9 -0
- package/templates/minimal/config/app.ts +28 -0
- package/templates/minimal/package.json +20 -0
- package/templates/minimal/resources/css/app.css +1 -0
- package/templates/minimal/resources/js/app.ts +2 -0
- package/templates/minimal/resources/js/components/Logo.tsx +48 -0
- package/templates/minimal/resources/js/env.d.ts +13 -0
- package/templates/minimal/resources/js/layouts/AppLayout.tsx +56 -0
- package/templates/minimal/resources/js/pages/about.tsx +30 -0
- package/templates/minimal/resources/js/pages/contact.tsx +49 -0
- package/templates/minimal/resources/js/pages/welcome.tsx +42 -0
- package/templates/minimal/routes/index.ts +16 -0
- package/templates/minimal/tests/smoke.test.ts +51 -0
- package/templates/minimal/tsconfig.json +19 -0
- package/templates/minimal/zt.ts +15 -0
- package/templates/react/_env.example +4 -0
- package/templates/react/_gitignore +5 -0
- package/templates/react/app/exceptions/Handler.ts +55 -0
- package/templates/react/app/routes/about.ts +8 -0
- package/templates/react/app/routes/contact.ts +31 -0
- package/templates/react/app/routes/index.ts +11 -0
- package/templates/react/bootstrap/app.ts +12 -0
- package/templates/react/bootstrap/providers.ts +10 -0
- package/templates/react/config/app.ts +25 -0
- package/templates/react/config/inertia.ts +7 -0
- package/templates/react/config/session.ts +11 -0
- package/templates/react/package.json +26 -0
- package/templates/react/public/zt.svg +17 -0
- package/templates/react/resources/app.html +39 -0
- package/templates/react/resources/css/app.css +206 -0
- package/templates/react/resources/js/Components/Button.tsx +67 -0
- package/templates/react/resources/js/Components/Card.tsx +40 -0
- package/templates/react/resources/js/Components/Code.tsx +33 -0
- package/templates/react/resources/js/Components/Field.tsx +125 -0
- package/templates/react/resources/js/Components/FlashToasts.tsx +79 -0
- package/templates/react/resources/js/Components/Icons.tsx +149 -0
- package/templates/react/resources/js/Components/ThemeToggle.tsx +47 -0
- package/templates/react/resources/js/Layouts/AppLayout.tsx +163 -0
- package/templates/react/resources/js/app.tsx +29 -0
- package/templates/react/resources/js/env.d.ts +13 -0
- package/templates/react/resources/js/lib/cn.ts +13 -0
- package/templates/react/resources/js/lib/site.ts +11 -0
- package/templates/react/resources/js/pages/about.tsx +146 -0
- package/templates/react/resources/js/pages/contact.tsx +152 -0
- package/templates/react/resources/js/pages/error.tsx +91 -0
- package/templates/react/resources/js/pages/home.tsx +199 -0
- package/templates/react/resources/js/pages.generated.ts +13 -0
- package/templates/react/resources/js/types.ts +17 -0
- package/templates/react/tests/smoke.test.ts +69 -0
- package/templates/react/tsconfig.json +21 -0
- package/templates/react/zt.ts +15 -0
- package/templates/vue/_env.example +4 -0
- package/templates/vue/_gitignore +5 -0
- package/templates/vue/app/routes/about.ts +7 -0
- package/templates/vue/app/routes/contact.ts +7 -0
- package/templates/vue/app/routes/index.ts +8 -0
- package/templates/vue/bootstrap/app.ts +8 -0
- package/templates/vue/bootstrap/providers.ts +6 -0
- package/templates/vue/config/app.ts +25 -0
- package/templates/vue/config/inertia.ts +7 -0
- package/templates/vue/package.json +24 -0
- package/templates/vue/resources/app.html +13 -0
- package/templates/vue/resources/css/app.css +1 -0
- package/templates/vue/resources/js/Layouts/AppLayout.vue +63 -0
- package/templates/vue/resources/js/app.tsx +19 -0
- package/templates/vue/resources/js/env.d.ts +20 -0
- package/templates/vue/resources/js/pages/about.vue +41 -0
- package/templates/vue/resources/js/pages/contact.vue +54 -0
- package/templates/vue/resources/js/pages/home.vue +54 -0
- package/templates/vue/resources/js/pages.generated.ts +12 -0
- package/templates/vue/tests/smoke.test.ts +46 -0
- package/templates/vue/tsconfig.json +21 -0
- package/templates/vue/zt.ts +15 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, Link } from "@zerotal/flow";
|
|
2
|
+
import type { HtmlNode } from "@zerotal/flow";
|
|
3
|
+
import { AppLayout } from "../layouts/app";
|
|
4
|
+
|
|
5
|
+
export class WelcomePage extends Component {
|
|
6
|
+
static layout = AppLayout;
|
|
7
|
+
static title = "Home";
|
|
8
|
+
|
|
9
|
+
async render(): Promise<HtmlNode> {
|
|
10
|
+
return (
|
|
11
|
+
<section class="py-10 text-center">
|
|
12
|
+
<span class="inline-flex items-center gap-2 rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700 ring-1 ring-green-600/20">
|
|
13
|
+
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
|
14
|
+
The install worked successfully!
|
|
15
|
+
</span>
|
|
16
|
+
|
|
17
|
+
<h1 class="mx-auto mt-6 max-w-2xl text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
18
|
+
Build server-driven apps with <span class="text-indigo-600">Zerotal</span>
|
|
19
|
+
</h1>
|
|
20
|
+
|
|
21
|
+
<p class="mx-auto mt-4 max-w-xl text-lg leading-relaxed text-gray-600">
|
|
22
|
+
Flow pages, file-based routing, and Tailwind — out of the box. Edit{" "}
|
|
23
|
+
<code class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600">
|
|
24
|
+
app/flow/pages/index.tsx
|
|
25
|
+
</code>{" "}
|
|
26
|
+
to get started.
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<div class="mt-8 flex items-center justify-center gap-3">
|
|
30
|
+
<Link
|
|
31
|
+
href="/about"
|
|
32
|
+
hover
|
|
33
|
+
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700"
|
|
34
|
+
>
|
|
35
|
+
Learn more
|
|
36
|
+
</Link>
|
|
37
|
+
<Link
|
|
38
|
+
href="/contact"
|
|
39
|
+
hover
|
|
40
|
+
class="rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
|
|
41
|
+
>
|
|
42
|
+
Get in touch
|
|
43
|
+
</Link>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { env, requireEnv } from "zerotal";
|
|
2
|
+
import { AppConfig } from "zerotal/config";
|
|
3
|
+
|
|
4
|
+
export default AppConfig({
|
|
5
|
+
name: "{{name}}",
|
|
6
|
+
url: env("APP_URL", "http://localhost:3000"),
|
|
7
|
+
/**
|
|
8
|
+
* The APP_KEY is used for encryption and should be set to a random 32
|
|
9
|
+
* character string. Use `bun zt key:generate` to generate a secure key.
|
|
10
|
+
*/
|
|
11
|
+
key: requireEnv("APP_KEY"),
|
|
12
|
+
|
|
13
|
+
cors: {
|
|
14
|
+
origin: env("CORS_ORIGIN", "*"),
|
|
15
|
+
credentials: false,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
throttle: {
|
|
19
|
+
maxAttempts: 120,
|
|
20
|
+
windowSeconds: 60,
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
assets: {
|
|
24
|
+
entrypoint: "resources/js/app.ts",
|
|
25
|
+
outDir: "public",
|
|
26
|
+
prefix: env("ASSETS_PREFIX", "/"),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"zt": "bun zt.ts",
|
|
8
|
+
"dev": "bun zt.ts serve --dev",
|
|
9
|
+
"start": "bun zt.ts serve",
|
|
10
|
+
"test": "bun zt.ts test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@zerotal/flow": "{{zerotal_version}}",
|
|
14
|
+
"zerotal": "{{zerotal_version}}"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"bun-plugin-tailwind": "latest",
|
|
18
|
+
"tailwindcss": "^4.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Zerotal">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="zt-grad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#818cf8" />
|
|
5
|
+
<stop offset="0.5" stop-color="#6366f1" />
|
|
6
|
+
<stop offset="1" stop-color="#7c3aed" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="zt-shine" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
|
|
9
|
+
<stop stop-color="#ffffff" stop-opacity="0.35" />
|
|
10
|
+
<stop offset="0.55" stop-color="#ffffff" stop-opacity="0" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<rect width="64" height="64" rx="18" fill="url(#zt-grad)" />
|
|
14
|
+
<rect width="64" height="64" rx="18" fill="url(#zt-shine)" />
|
|
15
|
+
<rect x="0.75" y="0.75" width="62.5" height="62.5" rx="17.25" stroke="#ffffff" stroke-opacity="0.18" stroke-width="1.5" />
|
|
16
|
+
<text x="32" y="41" text-anchor="middle" font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="20" font-weight="800" letter-spacing="0.5" fill="#ffffff">ZT</text>
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "../css/app.css";
|
|
2
|
+
console.log("Zerotal app, welcome from Browser!");
|
|
3
|
+
|
|
4
|
+
// Global client store — app-wide, client-only reactive UI state (theme, sidebar, …)
|
|
5
|
+
// shared across components with no server round-trip. Declare its initial shape here,
|
|
6
|
+
// then read/write it in JSX client expressions as `$flow.store.ui.dark`. Type it by
|
|
7
|
+
// augmenting `FlowStore` (see resources/js/env.d.ts).
|
|
8
|
+
//
|
|
9
|
+
// import { defineStore } from "@zerotal/flow/store";
|
|
10
|
+
// defineStore({ ui: { dark: false, sidebar: true } });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ambient declarations for non-code assets imported from TypeScript and bundled
|
|
2
|
+
// by `bun zt serve` (see the `assets` block in config/app.ts). These let
|
|
3
|
+
// side-effect imports like `import "../css/app.css"` type-check.
|
|
4
|
+
declare module "*.css";
|
|
5
|
+
declare module "*.scss";
|
|
6
|
+
declare module "*.svg";
|
|
7
|
+
declare module "*.png";
|
|
8
|
+
declare module "*.jpg";
|
|
9
|
+
declare module "*.jpeg";
|
|
10
|
+
declare module "*.gif";
|
|
11
|
+
declare module "*.webp";
|
|
12
|
+
declare module "*.woff";
|
|
13
|
+
declare module "*.woff2";
|
|
14
|
+
|
|
15
|
+
// Type the global client store by declaring your namespaces here. Every `$flow.store.*`
|
|
16
|
+
// access in your components is then fully typed. Keep in sync with the `defineStore(…)`
|
|
17
|
+
// call in resources/js/app.ts.
|
|
18
|
+
//
|
|
19
|
+
// declare module "@zerotal/flow/store" {
|
|
20
|
+
// interface FlowStore {
|
|
21
|
+
// ui: { dark: boolean; sidebar: boolean };
|
|
22
|
+
// }
|
|
23
|
+
// }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold boots and serves its pages.
|
|
3
|
+
*
|
|
4
|
+
* Two levels are shown here, and they answer different questions. The HTTP tests
|
|
5
|
+
* confirm the app wires up: providers register, file-based routes resolve, a
|
|
6
|
+
* request completes. The FlowTest block drives a page's own lifecycle in
|
|
7
|
+
* process — no server and no browser — which is where you test what a component
|
|
8
|
+
* does rather than that it exists.
|
|
9
|
+
*/
|
|
10
|
+
import { beforeAll, afterAll, describe, test, expect } from 'bun:test';
|
|
11
|
+
import { createTestApp, type TestApp } from '@zerotal/testing';
|
|
12
|
+
import { FlowTest } from '@zerotal/flow/testing';
|
|
13
|
+
import { ContactPage } from '../app/flow/pages/contact.tsx';
|
|
14
|
+
|
|
15
|
+
let app: TestApp;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
// Flow snapshot signing and session encryption both need a key. Seed a
|
|
19
|
+
// deterministic one for tests; `??=` leaves a real key alone when the app's
|
|
20
|
+
// .env is present.
|
|
21
|
+
Bun.env.APP_KEY ??= 'test-app-key-aaaaaaaaaaaaaaaaaaaaaaaa';
|
|
22
|
+
app = await createTestApp(() => import('../bootstrap/app.ts').then((m) => m.default));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(() => app.close());
|
|
26
|
+
|
|
27
|
+
describe('scaffold', () => {
|
|
28
|
+
test('boots and serves the home page', async () => {
|
|
29
|
+
const res = await app.get('/');
|
|
30
|
+
|
|
31
|
+
res.assertOk();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('serves every file-based page', async () => {
|
|
35
|
+
(await app.get('/about')).assertOk();
|
|
36
|
+
(await app.get('/contact')).assertOk();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('answers HEAD on the home route, as probes and load balancers do', async () => {
|
|
40
|
+
const res = await app.head('/');
|
|
41
|
+
expect(res.status).toBeLessThan(400);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('returns 404 for an unregistered path', async () => {
|
|
45
|
+
(await app.get('/definitely-not-a-route')).assertNotFound();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('the contact page component', () => {
|
|
50
|
+
test('renders its content without a server or a browser', async () => {
|
|
51
|
+
const page = await FlowTest.mount(ContactPage);
|
|
52
|
+
|
|
53
|
+
page.assertSee('Contact');
|
|
54
|
+
page.assertSee('hello@example.com');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"exactOptionalPropertyTypes": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"emitDecoratorMetadata": false,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"jsxImportSource": "@zerotal/flow",
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"paths": {
|
|
14
|
+
"@app/*": ["./app/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"exclude": ["node_modules", ".zerotal"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// zt.ts — Zerotal CLI entry point.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4
|
+
// Managed by the framework. All orchestration lives in @zerotal/core's
|
|
5
|
+
// startZerotal(); this file just hands it the app's bootstrap module.
|
|
6
|
+
//
|
|
7
|
+
// Run commands with: bun zt <command>
|
|
8
|
+
// bun zt serve [--port=3000] [--dev] Start the HTTP server
|
|
9
|
+
// bun zt worker Start the background worker
|
|
10
|
+
// bun zt test Run tests with the app booted
|
|
11
|
+
// bun zt list Show all available commands
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────
|
|
13
|
+
import { startZerotal } from "zerotal";
|
|
14
|
+
|
|
15
|
+
await startZerotal(() => import("./bootstrap/app"));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { env, requireEnv } from "zerotal";
|
|
2
|
+
import { AppConfig } from "zerotal/config";
|
|
3
|
+
|
|
4
|
+
export default AppConfig({
|
|
5
|
+
name: "{{name}}",
|
|
6
|
+
url: env("APP_URL", "http://localhost:3000"),
|
|
7
|
+
/**
|
|
8
|
+
* The APP_KEY is used for encryption and should be set to a random 32
|
|
9
|
+
* character string. Use `bun zt key:generate` to generate a secure key.
|
|
10
|
+
*/
|
|
11
|
+
key: requireEnv("APP_KEY"),
|
|
12
|
+
|
|
13
|
+
cors: {
|
|
14
|
+
origin: env("CORS_ORIGIN", "*"),
|
|
15
|
+
credentials: false,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
throttle: {
|
|
19
|
+
maxAttempts: 120,
|
|
20
|
+
windowSeconds: 60,
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
assets: {
|
|
24
|
+
entrypoint: "resources/js/app.ts",
|
|
25
|
+
outDir: "public",
|
|
26
|
+
prefix: env("ASSETS_PREFIX", "/"),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"zt": "bun zt.ts",
|
|
8
|
+
"dev": "bun zt.ts serve --dev",
|
|
9
|
+
"start": "bun zt.ts serve",
|
|
10
|
+
"test": "bun zt.ts test"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@zerotal/flow": "{{zerotal_version}}",
|
|
14
|
+
"zerotal": "{{zerotal_version}}"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"bun-plugin-tailwind": "latest",
|
|
18
|
+
"tailwindcss": "^4.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function Logo({ class: className }: { class?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
class={className}
|
|
5
|
+
viewBox="0 0 64 64"
|
|
6
|
+
fill="none"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-label="Zerotal"
|
|
10
|
+
>
|
|
11
|
+
<defs>
|
|
12
|
+
<linearGradient id="zt-grad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
|
13
|
+
<stop stop-color="#818cf8" />
|
|
14
|
+
<stop offset="0.5" stop-color="#6366f1" />
|
|
15
|
+
<stop offset="1" stop-color="#7c3aed" />
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient id="zt-shine" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
|
|
18
|
+
<stop stop-color="#ffffff" stop-opacity="0.35" />
|
|
19
|
+
<stop offset="0.55" stop-color="#ffffff" stop-opacity="0" />
|
|
20
|
+
</linearGradient>
|
|
21
|
+
</defs>
|
|
22
|
+
<rect width="64" height="64" rx="18" fill="url(#zt-grad)" />
|
|
23
|
+
<rect width="64" height="64" rx="18" fill="url(#zt-shine)" />
|
|
24
|
+
<rect
|
|
25
|
+
x="0.75"
|
|
26
|
+
y="0.75"
|
|
27
|
+
width="62.5"
|
|
28
|
+
height="62.5"
|
|
29
|
+
rx="17.25"
|
|
30
|
+
stroke="#ffffff"
|
|
31
|
+
stroke-opacity="0.18"
|
|
32
|
+
stroke-width="1.5"
|
|
33
|
+
/>
|
|
34
|
+
<text
|
|
35
|
+
x="32"
|
|
36
|
+
y="41"
|
|
37
|
+
text-anchor="middle"
|
|
38
|
+
font-family="system-ui, -apple-system, Segoe UI, sans-serif"
|
|
39
|
+
font-size="20"
|
|
40
|
+
font-weight="800"
|
|
41
|
+
letter-spacing="0.5"
|
|
42
|
+
fill="#ffffff"
|
|
43
|
+
>
|
|
44
|
+
ZT
|
|
45
|
+
</text>
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Ambient declarations for non-code assets imported from TypeScript and bundled
|
|
2
|
+
// by `bun zt serve` (see the `assets` block in config/app.ts). These let
|
|
3
|
+
// side-effect imports like `import "../css/app.css"` type-check.
|
|
4
|
+
declare module "*.css";
|
|
5
|
+
declare module "*.scss";
|
|
6
|
+
declare module "*.svg";
|
|
7
|
+
declare module "*.png";
|
|
8
|
+
declare module "*.jpg";
|
|
9
|
+
declare module "*.jpeg";
|
|
10
|
+
declare module "*.gif";
|
|
11
|
+
declare module "*.webp";
|
|
12
|
+
declare module "*.woff";
|
|
13
|
+
declare module "*.woff2";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Children } from "zerotal/view";
|
|
2
|
+
import { Logo } from "../components/Logo";
|
|
3
|
+
|
|
4
|
+
interface AppLayoutProps {
|
|
5
|
+
title: string;
|
|
6
|
+
/** Current request path — used to highlight the active nav item. */
|
|
7
|
+
path?: string;
|
|
8
|
+
children?: Children;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const NAV = [
|
|
12
|
+
{ href: "/", label: "Home" },
|
|
13
|
+
{ href: "/about", label: "About" },
|
|
14
|
+
{ href: "/contact", label: "Contact" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export default function AppLayout({ title, path = "/", children }: AppLayoutProps) {
|
|
18
|
+
const navLink =
|
|
19
|
+
"rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900";
|
|
20
|
+
const activeLink = "bg-indigo-50 text-indigo-700";
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
27
|
+
<title>{title}</title>
|
|
28
|
+
<link rel="stylesheet" href="/app.css" />
|
|
29
|
+
<script type="module" src="/app.js"></script>
|
|
30
|
+
</head>
|
|
31
|
+
<body class="min-h-screen bg-linear-to-b from-white via-white to-indigo-50 text-gray-900 antialiased">
|
|
32
|
+
<header class="sticky top-0 z-10 border-b border-gray-200/70 bg-white/80 backdrop-blur">
|
|
33
|
+
<nav class="mx-auto flex max-w-5xl items-center justify-between px-6 py-3">
|
|
34
|
+
<a href="/" class="flex items-center gap-2 font-bold tracking-tight">
|
|
35
|
+
<Logo class="h-7 w-7" />
|
|
36
|
+
<span>Zerotal</span>
|
|
37
|
+
</a>
|
|
38
|
+
|
|
39
|
+
<div class="flex items-center gap-1">
|
|
40
|
+
{NAV.map((item) => {
|
|
41
|
+
const isActive = item.href === "/" ? path === "/" : path.startsWith(item.href);
|
|
42
|
+
return (
|
|
43
|
+
<a href={item.href} class={`${navLink} ${isActive ? activeLink : ""}`}>
|
|
44
|
+
{item.label}
|
|
45
|
+
</a>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
</nav>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<main class="mx-auto max-w-3xl px-6 py-14">{children}</main>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HttpContext } from "zerotal";
|
|
2
|
+
import AppLayout from "../layouts/AppLayout";
|
|
3
|
+
|
|
4
|
+
export default function About(ctx: HttpContext, { title }: { title: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<AppLayout title={title} path={ctx.url.pathname}>
|
|
7
|
+
<section class="py-4">
|
|
8
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">About</h1>
|
|
9
|
+
<div class="mt-6 space-y-4 text-lg leading-relaxed text-gray-600">
|
|
10
|
+
<p>
|
|
11
|
+
This is an example app built with{" "}
|
|
12
|
+
<span class="font-medium text-gray-900">Zerotal</span> and its server-rendered JSX view
|
|
13
|
+
layer.
|
|
14
|
+
</p>
|
|
15
|
+
<p>
|
|
16
|
+
Routes are declared in{" "}
|
|
17
|
+
<code class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600">
|
|
18
|
+
routes/index.ts
|
|
19
|
+
</code>{" "}
|
|
20
|
+
and render pages from{" "}
|
|
21
|
+
<code class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600">
|
|
22
|
+
resources/js/pages
|
|
23
|
+
</code>
|
|
24
|
+
. This page is served at <span class="font-medium text-gray-900">/about</span>.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</AppLayout>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HttpContext } from "zerotal";
|
|
2
|
+
import AppLayout from "../layouts/AppLayout";
|
|
3
|
+
|
|
4
|
+
export default function Contact(ctx: HttpContext, { title }: { title: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<AppLayout title={title} path={ctx.url.pathname}>
|
|
7
|
+
<section class="py-4">
|
|
8
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">Contact</h1>
|
|
9
|
+
<p class="mt-4 text-lg leading-relaxed text-gray-600">
|
|
10
|
+
Have a question or feedback? We'd love to hear from you.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<div class="mt-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
14
|
+
<dl class="space-y-4 text-sm">
|
|
15
|
+
<div class="flex items-center justify-between gap-4">
|
|
16
|
+
<dt class="font-medium text-gray-500">Email</dt>
|
|
17
|
+
<dd>
|
|
18
|
+
<a
|
|
19
|
+
href="mailto:hello@example.com"
|
|
20
|
+
class="font-medium text-indigo-600 hover:text-indigo-700"
|
|
21
|
+
>
|
|
22
|
+
hello@example.com
|
|
23
|
+
</a>
|
|
24
|
+
</dd>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="flex items-center justify-between gap-4">
|
|
27
|
+
<dt class="font-medium text-gray-500">GitHub</dt>
|
|
28
|
+
<dd>
|
|
29
|
+
<a
|
|
30
|
+
href="https://github.com"
|
|
31
|
+
class="font-medium text-indigo-600 hover:text-indigo-700"
|
|
32
|
+
>
|
|
33
|
+
github.com/your-org
|
|
34
|
+
</a>
|
|
35
|
+
</dd>
|
|
36
|
+
</div>
|
|
37
|
+
</dl>
|
|
38
|
+
|
|
39
|
+
<a
|
|
40
|
+
href="mailto:hello@example.com"
|
|
41
|
+
class="mt-6 inline-block w-full rounded-lg bg-indigo-600 px-4 py-2.5 text-center text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700"
|
|
42
|
+
>
|
|
43
|
+
Email us
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
</section>
|
|
47
|
+
</AppLayout>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { HttpContext } from "zerotal";
|
|
2
|
+
import AppLayout from "../layouts/AppLayout";
|
|
3
|
+
|
|
4
|
+
export default function Welcome(ctx: HttpContext, { title }: { title: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<AppLayout title={title} path={ctx.url.pathname}>
|
|
7
|
+
<section class="py-10 text-center">
|
|
8
|
+
<span class="inline-flex items-center gap-2 rounded-full bg-green-50 px-3 py-1 text-sm font-medium text-green-700 ring-1 ring-green-600/20">
|
|
9
|
+
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
|
10
|
+
The install worked successfully!
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<h1 class="mx-auto mt-6 max-w-2xl text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
14
|
+
Build server-driven apps with <span class="text-indigo-600">Zerotal</span>
|
|
15
|
+
</h1>
|
|
16
|
+
|
|
17
|
+
<p class="mx-auto mt-4 max-w-xl text-lg leading-relaxed text-gray-600">
|
|
18
|
+
Server-rendered JSX views, routing, and Tailwind — out of the box. Edit{" "}
|
|
19
|
+
<code class="rounded bg-gray-100 px-1.5 py-0.5 font-mono text-sm text-indigo-600">
|
|
20
|
+
resources/js/pages/welcome.tsx
|
|
21
|
+
</code>{" "}
|
|
22
|
+
to get started.
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<div class="mt-8 flex items-center justify-center gap-3">
|
|
26
|
+
<a
|
|
27
|
+
href="/about"
|
|
28
|
+
class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700"
|
|
29
|
+
>
|
|
30
|
+
Learn more
|
|
31
|
+
</a>
|
|
32
|
+
<a
|
|
33
|
+
href="/contact"
|
|
34
|
+
class="rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50"
|
|
35
|
+
>
|
|
36
|
+
Get in touch
|
|
37
|
+
</a>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
</AppLayout>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Router, view, type HttpContext } from "zerotal";
|
|
2
|
+
import Welcome from "../resources/js/pages/welcome";
|
|
3
|
+
import About from "../resources/js/pages/about";
|
|
4
|
+
import Contact from "../resources/js/pages/contact";
|
|
5
|
+
|
|
6
|
+
Router.get("/", () => view(Welcome, { title: "Welcome to Zerotal" }));
|
|
7
|
+
Router.get("/about", () => view(About, { title: "About" }));
|
|
8
|
+
Router.get("/contact", () => view(Contact, { title: "Contact" }));
|
|
9
|
+
|
|
10
|
+
Router.get("/api", (http: HttpContext) => {
|
|
11
|
+
return http.json({ message: "Hello, API!" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
Router.get("/api/:id", (http: HttpContext) => {
|
|
15
|
+
return http.json({ id: http.params.id, message: "Hello, API!" });
|
|
16
|
+
});
|