create-koppajs 1.1.0 → 1.2.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.
Files changed (27) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +119 -122
  3. package/bin/create-koppajs.js +158 -13
  4. package/package.json +2 -1
  5. package/template-overlays/router/ARCHITECTURE.md +86 -0
  6. package/template-overlays/router/CHANGELOG.md +44 -0
  7. package/template-overlays/router/DEVELOPMENT_RULES.md +57 -0
  8. package/template-overlays/router/README.md +243 -0
  9. package/template-overlays/router/ROADMAP.md +34 -0
  10. package/template-overlays/router/TESTING_STRATEGY.md +67 -0
  11. package/template-overlays/router/docs/adr/0001-keep-the-starter-minimal.md +32 -0
  12. package/template-overlays/router/docs/architecture/module-boundaries.md +39 -0
  13. package/template-overlays/router/docs/meta/maintenance.md +38 -0
  14. package/template-overlays/router/docs/specs/README.md +19 -0
  15. package/template-overlays/router/docs/specs/app-bootstrap.md +42 -0
  16. package/template-overlays/router/docs/specs/router-navigation.md +41 -0
  17. package/template-overlays/router/index.html +14 -0
  18. package/template-overlays/router/package.json +74 -0
  19. package/template-overlays/router/pnpm-lock.yaml +3793 -0
  20. package/template-overlays/router/src/app-view.kpa +128 -0
  21. package/template-overlays/router/src/home-page.kpa +100 -0
  22. package/template-overlays/router/src/main.ts +89 -0
  23. package/template-overlays/router/src/not-found-page.kpa +69 -0
  24. package/template-overlays/router/src/router-page.kpa +102 -0
  25. package/template-overlays/router/src/style.css +51 -0
  26. package/template-overlays/router/tests/e2e/app.spec.ts +38 -0
  27. package/template-overlays/router/tests/integration/main-bootstrap.test.ts +150 -0
@@ -0,0 +1,128 @@
1
+ [template]
2
+ <div class="app-shell">
3
+ <header class="hero-panel">
4
+ <div class="hero-copy">
5
+ <img src="https://public-assets-1b57ca06-687a-4142-a525-0635f7649a5c.s3.eu-central-1.amazonaws.com/koppajs/koppajs-logo-text-900x226.png" width="320" alt="KoppaJS Logo" class="logo">
6
+ <p class="eyebrow">Router Starter</p>
7
+ <h1 class="title">Optional routing for new KoppaJS projects</h1>
8
+ <p class="lead">
9
+ This starter keeps the original lightweight baseline, adds
10
+ <code>@koppajs/koppajs-router</code>, and shows how route-based rendering
11
+ fits into a small app shell.
12
+ </p>
13
+ </div>
14
+
15
+ <nav class="main-nav" aria-label="Primary">
16
+ <a data-route="/" href="/" class="nav-link">Home</a>
17
+ <a data-route="/router" href="/router" class="nav-link">Router Page</a>
18
+ </nav>
19
+ </header>
20
+
21
+ <main id="app-outlet" class="app-outlet" aria-live="polite"></main>
22
+ </div>
23
+ [/template]
24
+
25
+ [css]
26
+ .app-shell {
27
+ width: min(1080px, calc(100vw - 2rem));
28
+ margin: 0 auto;
29
+ padding: 2rem 0 3rem;
30
+ }
31
+
32
+ .hero-panel {
33
+ display: grid;
34
+ gap: 1.5rem;
35
+ padding: 1.5rem;
36
+ border: 1px solid var(--border);
37
+ border-radius: 1.75rem;
38
+ background: linear-gradient(180deg, rgba(8, 17, 31, 0.88), rgba(8, 17, 31, 0.72));
39
+ box-shadow: var(--shadow);
40
+ backdrop-filter: blur(18px);
41
+ }
42
+
43
+ .hero-copy {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 0.9rem;
47
+ }
48
+
49
+ .logo {
50
+ width: min(320px, 72vw);
51
+ }
52
+
53
+ .eyebrow {
54
+ font-size: 0.82rem;
55
+ letter-spacing: 0.3em;
56
+ text-transform: uppercase;
57
+ color: var(--accent);
58
+ }
59
+
60
+ .title {
61
+ font-size: clamp(2.1rem, 5vw, 3.6rem);
62
+ line-height: 1.05;
63
+ max-width: 14ch;
64
+ }
65
+
66
+ .lead {
67
+ max-width: 58ch;
68
+ color: var(--muted);
69
+ line-height: 1.65;
70
+ }
71
+
72
+ .lead code {
73
+ color: var(--accent-strong);
74
+ }
75
+
76
+ .main-nav {
77
+ display: flex;
78
+ flex-wrap: wrap;
79
+ gap: 0.8rem;
80
+ }
81
+
82
+ .nav-link {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ min-height: 2.8rem;
87
+ padding: 0 1.15rem;
88
+ border-radius: 999px;
89
+ border: 1px solid rgba(100, 220, 232, 0.24);
90
+ background: rgba(100, 220, 232, 0.06);
91
+ color: var(--text);
92
+ text-decoration: none;
93
+ transition: background 0.25s ease, border-color 0.25s ease, transform 0.15s ease;
94
+ }
95
+
96
+ .nav-link:hover {
97
+ background: rgba(100, 220, 232, 0.12);
98
+ border-color: rgba(100, 220, 232, 0.4);
99
+ }
100
+
101
+ .nav-link:active {
102
+ transform: translateY(1px);
103
+ }
104
+
105
+ .nav-link.is-active,
106
+ .nav-link[aria-current="page"] {
107
+ background: var(--accent);
108
+ border-color: var(--accent);
109
+ color: #07111e;
110
+ font-weight: 700;
111
+ }
112
+
113
+ .app-outlet {
114
+ padding-top: 1.5rem;
115
+ }
116
+
117
+ @media (max-width: 640px) {
118
+ .app-shell {
119
+ width: min(100vw - 1rem, 1080px);
120
+ padding-top: 0.75rem;
121
+ }
122
+
123
+ .hero-panel {
124
+ padding: 1.15rem;
125
+ border-radius: 1.25rem;
126
+ }
127
+ }
128
+ [/css]
@@ -0,0 +1,100 @@
1
+ [template]
2
+ <section class="page-card">
3
+ <div class="page-copy">
4
+ <p class="section-label">Home route</p>
5
+ <h2 class="page-title">A small starter, now with real navigation</h2>
6
+ <p class="page-text">
7
+ The home page keeps the original interactive counter so the starter still
8
+ shows local state, while the navigation above now switches routes through
9
+ the published KoppaJS router.
10
+ </p>
11
+
12
+ <div class="page-actions">
13
+ <a data-route="/router" href="/router" class="page-link">Open the second page</a>
14
+ </div>
15
+ </div>
16
+
17
+ <counter-component></counter-component>
18
+ </section>
19
+ [/template]
20
+
21
+ [css]
22
+ .page-card {
23
+ display: grid;
24
+ grid-template-columns: minmax(0, 1.1fr) auto;
25
+ gap: 1.5rem;
26
+ align-items: start;
27
+ padding: 1.5rem;
28
+ border: 1px solid var(--border);
29
+ border-radius: 1.75rem;
30
+ background: var(--surface);
31
+ box-shadow: var(--shadow);
32
+ backdrop-filter: blur(18px);
33
+ }
34
+
35
+ .page-copy {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 1rem;
39
+ }
40
+
41
+ .section-label {
42
+ color: var(--accent);
43
+ font-size: 0.8rem;
44
+ letter-spacing: 0.28em;
45
+ text-transform: uppercase;
46
+ }
47
+
48
+ .page-title {
49
+ font-size: clamp(1.7rem, 3vw, 2.7rem);
50
+ line-height: 1.1;
51
+ }
52
+
53
+ .page-text {
54
+ color: var(--muted);
55
+ line-height: 1.7;
56
+ max-width: 54ch;
57
+ }
58
+
59
+ .page-actions {
60
+ display: flex;
61
+ flex-wrap: wrap;
62
+ gap: 0.8rem;
63
+ }
64
+
65
+ .page-link {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ min-height: 2.8rem;
70
+ padding: 0 1.25rem;
71
+ border-radius: 999px;
72
+ background: rgba(100, 220, 232, 0.1);
73
+ border: 1px solid rgba(100, 220, 232, 0.3);
74
+ color: var(--accent-strong);
75
+ text-decoration: none;
76
+ transition: background 0.25s ease, transform 0.15s ease;
77
+ }
78
+
79
+ .page-link:hover {
80
+ background: rgba(100, 220, 232, 0.18);
81
+ }
82
+
83
+ .page-link:active {
84
+ transform: translateY(1px);
85
+ }
86
+
87
+ @media (max-width: 840px) {
88
+ .page-card {
89
+ grid-template-columns: 1fr;
90
+ justify-items: start;
91
+ }
92
+ }
93
+
94
+ @media (max-width: 640px) {
95
+ .page-card {
96
+ padding: 1.15rem;
97
+ border-radius: 1.25rem;
98
+ }
99
+ }
100
+ [/css]
@@ -0,0 +1,89 @@
1
+ import { Core } from "@koppajs/koppajs-core";
2
+ import {
3
+ KOPPAJS_ROUTE_CHANGE_EVENT,
4
+ KoppajsRouter,
5
+ type RouteDefinition,
6
+ } from "@koppajs/koppajs-router";
7
+
8
+ import appView from "./app-view.kpa";
9
+ import counterComponent from "./counter-component.kpa";
10
+ import homePage from "./home-page.kpa";
11
+ import notFoundPage from "./not-found-page.kpa";
12
+ import routerPage from "./router-page.kpa";
13
+
14
+ const ROUTE_LINK_SELECTOR = "a[data-route]";
15
+
16
+ const routes = [
17
+ {
18
+ path: "/",
19
+ name: "home",
20
+ title: "Home",
21
+ description: "Landing page for the KoppaJS router starter.",
22
+ componentTag: "home-page",
23
+ },
24
+ {
25
+ path: "/router",
26
+ name: "router",
27
+ title: "Router",
28
+ description: "Second page showing how the KoppaJS router starter works.",
29
+ componentTag: "router-page",
30
+ },
31
+ {
32
+ path: "*",
33
+ name: "not-found",
34
+ title: "Not found",
35
+ description: "Fallback page for unmatched routes in the router starter.",
36
+ componentTag: "not-found-page",
37
+ },
38
+ ] satisfies readonly RouteDefinition[];
39
+
40
+ let routerStarted = false;
41
+
42
+ Core.take(appView, "app-view");
43
+ Core.take(counterComponent, "counter-component");
44
+ Core.take(homePage, "home-page");
45
+ Core.take(routerPage, "router-page");
46
+ Core.take(notFoundPage, "not-found-page");
47
+ Core();
48
+
49
+ bootRouter();
50
+
51
+ function bootRouter() {
52
+ if (routerStarted) {
53
+ return;
54
+ }
55
+
56
+ const outlet = document.querySelector<HTMLElement>("#app-outlet");
57
+
58
+ if (!outlet) {
59
+ window.requestAnimationFrame(bootRouter);
60
+ return;
61
+ }
62
+
63
+ routerStarted = true;
64
+
65
+ const router = new KoppajsRouter({
66
+ routes,
67
+ outlet,
68
+ root: document,
69
+ });
70
+
71
+ const syncRouteLinks = () => {
72
+ document
73
+ .querySelectorAll<HTMLAnchorElement>(ROUTE_LINK_SELECTOR)
74
+ .forEach((link) => {
75
+ const routeTarget = link.getAttribute("data-route");
76
+
77
+ if (!routeTarget) {
78
+ return;
79
+ }
80
+
81
+ link.setAttribute("href", router.hrefFor(routeTarget));
82
+ });
83
+ };
84
+
85
+ window.addEventListener(KOPPAJS_ROUTE_CHANGE_EVENT, syncRouteLinks);
86
+
87
+ router.init();
88
+ syncRouteLinks();
89
+ }
@@ -0,0 +1,69 @@
1
+ [template]
2
+ <section class="page-card page-card--missing">
3
+ <p class="section-label">404</p>
4
+ <h2 class="page-title">Page not found</h2>
5
+ <p class="page-text">
6
+ The router starter includes an explicit catch-all route, so unmatched URLs
7
+ still render a predictable fallback page instead of failing silently.
8
+ </p>
9
+
10
+ <div class="page-actions">
11
+ <a data-route="/" href="/" class="page-link">Return home</a>
12
+ </div>
13
+ </section>
14
+ [/template]
15
+
16
+ [css]
17
+ .page-card {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 1rem;
21
+ padding: 1.5rem;
22
+ border: 1px solid rgba(248, 113, 113, 0.24);
23
+ border-radius: 1.75rem;
24
+ background: linear-gradient(180deg, rgba(61, 12, 22, 0.5), rgba(11, 22, 40, 0.88));
25
+ box-shadow: var(--shadow);
26
+ }
27
+
28
+ .section-label {
29
+ color: #fda4af;
30
+ font-size: 0.8rem;
31
+ letter-spacing: 0.28em;
32
+ text-transform: uppercase;
33
+ }
34
+
35
+ .page-title {
36
+ font-size: clamp(1.7rem, 3vw, 2.5rem);
37
+ line-height: 1.1;
38
+ }
39
+
40
+ .page-text {
41
+ color: #fecdd3;
42
+ line-height: 1.7;
43
+ max-width: 58ch;
44
+ }
45
+
46
+ .page-actions {
47
+ display: flex;
48
+ }
49
+
50
+ .page-link {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ min-height: 2.8rem;
55
+ padding: 0 1.25rem;
56
+ border-radius: 999px;
57
+ background: rgba(253, 164, 175, 0.12);
58
+ border: 1px solid rgba(253, 164, 175, 0.4);
59
+ color: #fff1f2;
60
+ text-decoration: none;
61
+ }
62
+
63
+ @media (max-width: 640px) {
64
+ .page-card {
65
+ padding: 1.15rem;
66
+ border-radius: 1.25rem;
67
+ }
68
+ }
69
+ [/css]
@@ -0,0 +1,102 @@
1
+ [template]
2
+ <section class="page-card">
3
+ <div class="page-copy">
4
+ <p class="section-label">Second page</p>
5
+ <h2 class="page-title">Router is active</h2>
6
+ <p class="page-text">
7
+ This view is rendered into <code>#app-outlet</code> by
8
+ <code>KoppajsRouter</code>. The active navigation state and the browser
9
+ URL are both managed by the router runtime.
10
+ </p>
11
+
12
+ <ul class="feature-list">
13
+ <li>Routes are defined in <code>src/main.ts</code>.</li>
14
+ <li>Links opt into delegated navigation through <code>data-route</code>.</li>
15
+ <li>A catch-all route is already wired for unmatched paths.</li>
16
+ </ul>
17
+
18
+ <div class="page-actions">
19
+ <a data-route="/" href="/" class="page-link">Back to home</a>
20
+ </div>
21
+ </div>
22
+ </section>
23
+ [/template]
24
+
25
+ [css]
26
+ .page-card {
27
+ padding: 1.5rem;
28
+ border: 1px solid var(--border);
29
+ border-radius: 1.75rem;
30
+ background: var(--surface-strong);
31
+ box-shadow: var(--shadow);
32
+ backdrop-filter: blur(18px);
33
+ }
34
+
35
+ .page-copy {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 1rem;
39
+ }
40
+
41
+ .section-label {
42
+ color: var(--accent);
43
+ font-size: 0.8rem;
44
+ letter-spacing: 0.28em;
45
+ text-transform: uppercase;
46
+ }
47
+
48
+ .page-title {
49
+ font-size: clamp(1.7rem, 3vw, 2.5rem);
50
+ line-height: 1.1;
51
+ }
52
+
53
+ .page-text {
54
+ color: var(--muted);
55
+ line-height: 1.7;
56
+ max-width: 58ch;
57
+ }
58
+
59
+ code {
60
+ color: var(--accent-strong);
61
+ }
62
+
63
+ .feature-list {
64
+ list-style: none;
65
+ display: grid;
66
+ gap: 0.8rem;
67
+ color: var(--text);
68
+ }
69
+
70
+ .feature-list li {
71
+ padding: 0.95rem 1rem;
72
+ border-radius: 1rem;
73
+ border: 1px solid rgba(100, 220, 232, 0.18);
74
+ background: rgba(100, 220, 232, 0.06);
75
+ }
76
+
77
+ .page-actions {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ gap: 0.8rem;
81
+ }
82
+
83
+ .page-link {
84
+ display: inline-flex;
85
+ align-items: center;
86
+ justify-content: center;
87
+ min-height: 2.8rem;
88
+ padding: 0 1.25rem;
89
+ border-radius: 999px;
90
+ background: rgba(100, 220, 232, 0.1);
91
+ border: 1px solid rgba(100, 220, 232, 0.3);
92
+ color: var(--accent-strong);
93
+ text-decoration: none;
94
+ }
95
+
96
+ @media (max-width: 640px) {
97
+ .page-card {
98
+ padding: 1.15rem;
99
+ border-radius: 1.25rem;
100
+ }
101
+ }
102
+ [/css]
@@ -0,0 +1,51 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --app-bg: #08111f;
4
+ --app-bg-accent: #102742;
5
+ --surface: rgba(9, 16, 31, 0.78);
6
+ --surface-strong: rgba(11, 22, 40, 0.92);
7
+ --border: rgba(148, 163, 184, 0.2);
8
+ --text: #edf6ff;
9
+ --muted: #a7bfd7;
10
+ --accent: #64dce8;
11
+ --accent-strong: #97f3fb;
12
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
13
+ font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ html {
23
+ min-height: 100%;
24
+ background:
25
+ radial-gradient(
26
+ circle at top left,
27
+ rgba(100, 220, 232, 0.16),
28
+ transparent 30%
29
+ ),
30
+ radial-gradient(
31
+ circle at bottom right,
32
+ rgba(16, 39, 66, 0.55),
33
+ transparent 32%
34
+ ),
35
+ linear-gradient(
36
+ 160deg,
37
+ var(--app-bg) 0%,
38
+ #060c18 45%,
39
+ var(--app-bg-accent) 100%
40
+ );
41
+ }
42
+
43
+ body {
44
+ min-height: 100vh;
45
+ color: var(--text);
46
+ }
47
+
48
+ a,
49
+ button {
50
+ font: inherit;
51
+ }
@@ -0,0 +1,38 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("renders the router starter, keeps the counter, and navigates between routes", async ({
4
+ page,
5
+ }) => {
6
+ await page.goto("/");
7
+
8
+ await expect(page.getByText("Router Starter")).toBeVisible();
9
+ await expect(
10
+ page.getByRole("heading", {
11
+ name: "A small starter, now with real navigation",
12
+ }),
13
+ ).toBeVisible();
14
+
15
+ const counterValue = page.getByRole("status");
16
+
17
+ await expect(counterValue).toHaveText("0");
18
+
19
+ await page.getByRole("button", { name: "Increment counter" }).click();
20
+ await expect(counterValue).toHaveText("1");
21
+
22
+ await page.getByRole("link", { name: "Router Page" }).click();
23
+
24
+ await expect(page).toHaveURL(/\/router$/);
25
+ await expect(
26
+ page.getByRole("heading", { name: "Router is active" }),
27
+ ).toBeVisible();
28
+ await expect(page.getByRole("link", { name: "Router Page" })).toHaveAttribute(
29
+ "aria-current",
30
+ "page",
31
+ );
32
+
33
+ await page.goto("/missing");
34
+
35
+ await expect(
36
+ page.getByRole("heading", { name: "Page not found" }),
37
+ ).toBeVisible();
38
+ });