create-sanifyfe 0.1.3 → 0.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.
- package/package.json +1 -1
- package/templates/default/README.md +15 -4
- package/templates/default/package.json +1 -1
- package/templates/default/src/app.ts +33 -6
- package/templates/default/src/components/live-clock.ts +14 -0
- package/templates/default/src/components/nav-bar.ts +37 -0
- package/templates/default/src/components/todo-item.ts +2 -2
- package/templates/default/src/components/users-sidebar.ts +29 -0
- package/templates/default/src/data/users.ts +28 -0
- package/templates/default/src/main.ts +1 -1
- package/templates/default/src/pages/about-page.ts +16 -8
- package/templates/default/src/pages/home-page.ts +32 -0
- package/templates/default/src/pages/settings-page.ts +47 -0
- package/templates/default/src/pages/{todo-page.ts → todos-page.ts} +5 -12
- package/templates/default/src/pages/user-detail.ts +32 -0
- package/templates/default/src/pages/user-list.ts +11 -0
- package/templates/default/src/state/settings.ts +12 -0
- package/templates/default/src/state/todos.ts +2 -2
package/package.json
CHANGED
|
@@ -25,10 +25,21 @@ bun install
|
|
|
25
25
|
```
|
|
26
26
|
src/
|
|
27
27
|
main.ts entry: memuat app-root
|
|
28
|
-
app.ts root
|
|
29
|
-
state/todos
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
app.ts root + router (termasuk nested route + outlet)
|
|
29
|
+
state/ todos (persisted), settings (createStore nested)
|
|
30
|
+
data/ data contoh + simulasi fetch async
|
|
31
|
+
components/ nav-bar, users-sidebar, todo-item, live-clock
|
|
32
|
+
pages/ home, todos, settings, user-list, user-detail, about
|
|
32
33
|
index.html halaman host, memuat main.js hasil bundling
|
|
33
34
|
dev-server.ts server dev Bun, transpile TS on-the-fly
|
|
34
35
|
```
|
|
36
|
+
|
|
37
|
+
## Fitur yang dicontohkan
|
|
38
|
+
|
|
39
|
+
| Halaman | Fitur Sanify |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| Home | `signal`, `onMount`/`onCleanup` (live-clock) |
|
|
42
|
+
| Todos | `For` (list keyed), `persisted` + cross-tab, `computed` |
|
|
43
|
+
| Settings | `createStore` objek nested fine-grained (update by path) |
|
|
44
|
+
| Users | router **nested + outlet** (layout bertahan), `params()` reaktif, `resource` (fetch async) |
|
|
45
|
+
| About | `query()` reaktif |
|
|
@@ -1,19 +1,46 @@
|
|
|
1
|
-
// app.ts — root komponen + router
|
|
1
|
+
// app.ts — root komponen + router (termasuk nested route + outlet)
|
|
2
2
|
|
|
3
3
|
import { component, html, router } from "@sanify/core";
|
|
4
|
-
import "./
|
|
4
|
+
import "./components/nav-bar.ts";
|
|
5
|
+
import "./components/users-sidebar.ts";
|
|
6
|
+
import "./pages/home-page.ts";
|
|
7
|
+
import "./pages/todos-page.ts";
|
|
8
|
+
import "./pages/settings-page.ts";
|
|
9
|
+
import "./pages/user-list.ts";
|
|
10
|
+
import "./pages/user-detail.ts";
|
|
5
11
|
import "./pages/about-page.ts";
|
|
6
12
|
|
|
7
13
|
component("app-root", () => {
|
|
8
14
|
const view = router({
|
|
9
|
-
"/": () => html`<
|
|
15
|
+
"/": () => html`<home-page></home-page>`,
|
|
16
|
+
"/todos": () => html`<todos-page></todos-page>`,
|
|
17
|
+
"/settings": () => html`<settings-page></settings-page>`,
|
|
18
|
+
// nested route: layout dengan outlet — sidebar bertahan saat ganti user
|
|
19
|
+
"/users": {
|
|
20
|
+
layout: (ctx) => html`
|
|
21
|
+
<div class="max-w-3xl mx-auto mt-8 flex gap-6">
|
|
22
|
+
<users-sidebar></users-sidebar>
|
|
23
|
+
<section class="flex-1 bg-white rounded-xl shadow p-6 min-h-40">
|
|
24
|
+
${ctx.outlet}
|
|
25
|
+
</section>
|
|
26
|
+
</div>
|
|
27
|
+
`,
|
|
28
|
+
children: {
|
|
29
|
+
"/": () => html`<user-list></user-list>`,
|
|
30
|
+
"/:id": () => html`<user-detail></user-detail>`,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
10
33
|
"/about": () => html`<about-page></about-page>`,
|
|
11
34
|
"*": () => html`
|
|
12
|
-
<div class="max-w-md mx-auto mt-
|
|
13
|
-
<p class="text-
|
|
35
|
+
<div class="max-w-md mx-auto mt-16 text-center text-slate-500">
|
|
36
|
+
<p class="text-lg">Halaman tidak ditemukan.</p>
|
|
14
37
|
<a data-link href="/" class="text-blue-600 hover:underline">Beranda</a>
|
|
15
38
|
</div>
|
|
16
39
|
`,
|
|
17
40
|
});
|
|
18
|
-
|
|
41
|
+
|
|
42
|
+
return () => html`
|
|
43
|
+
<nav-bar></nav-bar>
|
|
44
|
+
<main>${view}</main>
|
|
45
|
+
`;
|
|
19
46
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// components/live-clock.ts — demo lifecycle: onMount mulai interval, onCleanup stop
|
|
2
|
+
|
|
3
|
+
import { component, html, signal, onMount, onCleanup } from "@sanify/core";
|
|
4
|
+
|
|
5
|
+
component("live-clock", () => {
|
|
6
|
+
const [now, setNow] = signal(new Date().toLocaleTimeString());
|
|
7
|
+
|
|
8
|
+
onMount(() => {
|
|
9
|
+
const id = setInterval(() => setNow(new Date().toLocaleTimeString()), 1000);
|
|
10
|
+
onCleanup(() => clearInterval(id));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return () => html`<span class="font-mono">${() => now()}</span>`;
|
|
14
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// components/nav-bar.ts — navigasi atas (SPA link via data-link)
|
|
2
|
+
|
|
3
|
+
import { component, html, current } from "@sanify/core";
|
|
4
|
+
|
|
5
|
+
const links = [
|
|
6
|
+
{ href: "/", label: "Home" },
|
|
7
|
+
{ href: "/todos", label: "Todos" },
|
|
8
|
+
{ href: "/settings", label: "Settings" },
|
|
9
|
+
{ href: "/users", label: "Users" },
|
|
10
|
+
{ href: "/about", label: "About" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
component("nav-bar", () => {
|
|
14
|
+
const isActive = (href: string) =>
|
|
15
|
+
href === "/" ? current() === "/" : current().startsWith(href);
|
|
16
|
+
|
|
17
|
+
return () => html`
|
|
18
|
+
<nav class="bg-white border-b border-slate-200">
|
|
19
|
+
<div class="max-w-3xl mx-auto px-4 h-12 flex items-center gap-4 text-sm">
|
|
20
|
+
<span class="font-bold text-slate-800">Sanify</span>
|
|
21
|
+
${links.map(
|
|
22
|
+
(l) => html`
|
|
23
|
+
<a
|
|
24
|
+
data-link
|
|
25
|
+
href=${l.href}
|
|
26
|
+
class=${() =>
|
|
27
|
+
isActive(l.href)
|
|
28
|
+
? "text-blue-600 font-medium"
|
|
29
|
+
: "text-slate-500 hover:text-slate-800"}
|
|
30
|
+
>${l.label}</a
|
|
31
|
+
>
|
|
32
|
+
`,
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
</nav>
|
|
36
|
+
`;
|
|
37
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// components/todo-item.ts — satu baris todo (menerima objek lewat
|
|
1
|
+
// components/todo-item.ts — satu baris todo (menerima objek lewat .prop)
|
|
2
2
|
|
|
3
3
|
import { component, html, type ComponentContext } from "@sanify/core";
|
|
4
4
|
import { toggleTodo, removeTodo, type Todo } from "../state/todos.ts";
|
|
@@ -11,7 +11,7 @@ component<TodoItemProps>(
|
|
|
11
11
|
"todo-item",
|
|
12
12
|
({ props }: ComponentContext<TodoItemProps>) => {
|
|
13
13
|
return () => html`
|
|
14
|
-
<li class="flex items-center gap-3 py-2 border-b border-slate-
|
|
14
|
+
<li class="flex items-center gap-3 py-2 border-b border-slate-100">
|
|
15
15
|
<input
|
|
16
16
|
type="checkbox"
|
|
17
17
|
class="h-4 w-4"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// components/users-sidebar.ts — daftar user (bagian layout nested yang bertahan)
|
|
2
|
+
|
|
3
|
+
import { component, html, params } from "@sanify/core";
|
|
4
|
+
import { users } from "../data/users.ts";
|
|
5
|
+
|
|
6
|
+
component("users-sidebar", () => {
|
|
7
|
+
return () => html`
|
|
8
|
+
<aside class="w-40 shrink-0">
|
|
9
|
+
<h2 class="text-xs uppercase text-slate-400 mb-2">Users</h2>
|
|
10
|
+
<ul class="space-y-1">
|
|
11
|
+
${users.map(
|
|
12
|
+
(u) => html`
|
|
13
|
+
<li>
|
|
14
|
+
<a
|
|
15
|
+
data-link
|
|
16
|
+
href=${`/users/${u.id}`}
|
|
17
|
+
class=${() =>
|
|
18
|
+
params().id === u.id
|
|
19
|
+
? "text-blue-600 font-medium"
|
|
20
|
+
: "text-slate-600 hover:text-slate-900"}
|
|
21
|
+
>${u.name}</a
|
|
22
|
+
>
|
|
23
|
+
</li>
|
|
24
|
+
`,
|
|
25
|
+
)}
|
|
26
|
+
</ul>
|
|
27
|
+
</aside>
|
|
28
|
+
`;
|
|
29
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// data/users.ts — data contoh + simulasi fetch async (untuk demo resource())
|
|
2
|
+
|
|
3
|
+
export interface User {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Profile extends User {
|
|
9
|
+
email: string;
|
|
10
|
+
bio: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const users: User[] = [
|
|
14
|
+
{ id: "ada", name: "Ada Lovelace" },
|
|
15
|
+
{ id: "alan", name: "Alan Turing" },
|
|
16
|
+
{ id: "grace", name: "Grace Hopper" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Tanpa jaringan: tiru latensi + kemungkinan error agar resource() terlihat.
|
|
20
|
+
export function fetchProfile(id: string): Promise<Profile> {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
const u = users.find((x) => x.id === id);
|
|
24
|
+
if (!u) reject(new Error(`User "${id}" tidak ditemukan`));
|
|
25
|
+
else resolve({ ...u, email: `${u.id}@sanify.dev`, bio: `Profil ${u.name}.` });
|
|
26
|
+
}, 400);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
// pages/about-page.ts —
|
|
1
|
+
// pages/about-page.ts — info + demo query() reaktif
|
|
2
2
|
|
|
3
|
-
import { component, html } from "@sanify/core";
|
|
3
|
+
import { component, html, query } from "@sanify/core";
|
|
4
4
|
|
|
5
5
|
component("about-page", () => {
|
|
6
6
|
return () => html`
|
|
7
|
-
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
|
|
8
|
-
<h1 class="text-2xl font-bold
|
|
9
|
-
<p class="text-slate-600
|
|
10
|
-
|
|
11
|
-
Components
|
|
7
|
+
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow space-y-2">
|
|
8
|
+
<h1 class="text-2xl font-bold">About</h1>
|
|
9
|
+
<p class="text-sm text-slate-600">
|
|
10
|
+
Dibangun dengan Sanify — framework reaktif fine-grained di atas Web
|
|
11
|
+
Components, tanpa virtual DOM.
|
|
12
|
+
</p>
|
|
13
|
+
<p class="text-sm">
|
|
14
|
+
Coba query string:
|
|
15
|
+
<a data-link href="/about?msg=hai" class="text-blue-600 hover:underline"
|
|
16
|
+
>/about?msg=hai</a
|
|
17
|
+
>
|
|
18
|
+
</p>
|
|
19
|
+
<p class="text-sm">
|
|
20
|
+
query.msg = <strong>${() => query().get("msg") ?? "(kosong)"}</strong>
|
|
12
21
|
</p>
|
|
13
|
-
<a data-link href="/" class="text-blue-600 hover:underline">← Kembali</a>
|
|
14
22
|
</div>
|
|
15
23
|
`;
|
|
16
24
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// pages/home-page.ts — intro + signal counter + komponen lifecycle
|
|
2
|
+
|
|
3
|
+
import { component, html, signal } from "@sanify/core";
|
|
4
|
+
import "../components/live-clock.ts";
|
|
5
|
+
|
|
6
|
+
component("home-page", () => {
|
|
7
|
+
const [count, setCount] = signal(0);
|
|
8
|
+
|
|
9
|
+
return () => html`
|
|
10
|
+
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow space-y-4">
|
|
11
|
+
<h1 class="text-2xl font-bold">Sanify starter</h1>
|
|
12
|
+
<p class="text-sm text-slate-600">
|
|
13
|
+
Contoh kecil yang memamerkan fitur inti: signal, store, list keyed,
|
|
14
|
+
router nested, resource.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<div class="flex items-center gap-3">
|
|
18
|
+
<button
|
|
19
|
+
class="bg-slate-800 text-white rounded px-3 py-1 hover:bg-slate-700"
|
|
20
|
+
@click=${() => setCount((n) => n + 1)}
|
|
21
|
+
>
|
|
22
|
+
+1
|
|
23
|
+
</button>
|
|
24
|
+
<span class="text-sm">signal: <strong>${() => count()}</strong></span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<p class="text-sm text-slate-500">
|
|
28
|
+
onMount/onCleanup → <live-clock></live-clock>
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// pages/settings-page.ts — createStore nested fine-grained (update by path)
|
|
2
|
+
|
|
3
|
+
import { component, html } from "@sanify/core";
|
|
4
|
+
import { settings, setSettings } from "../state/settings.ts";
|
|
5
|
+
|
|
6
|
+
component("settings-page", () => {
|
|
7
|
+
return () => html`
|
|
8
|
+
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow space-y-4">
|
|
9
|
+
<h1 class="text-2xl font-bold">Settings</h1>
|
|
10
|
+
<p class="text-sm text-slate-500">
|
|
11
|
+
createStore + update by path — edit satu field tidak menyentuh field lain.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<label class="block">
|
|
15
|
+
<span class="text-sm text-slate-600">Nama</span>
|
|
16
|
+
<input
|
|
17
|
+
class="mt-1 w-full border border-slate-300 rounded px-3 py-2"
|
|
18
|
+
.value=${() => settings.profile.name}
|
|
19
|
+
@input=${(e: Event) =>
|
|
20
|
+
setSettings("profile", "name", (e.target as HTMLInputElement).value)}
|
|
21
|
+
/>
|
|
22
|
+
</label>
|
|
23
|
+
|
|
24
|
+
<div class="flex items-center gap-3">
|
|
25
|
+
<span class="text-sm text-slate-600">Umur</span>
|
|
26
|
+
<button
|
|
27
|
+
class="border rounded w-8 h-8"
|
|
28
|
+
@click=${() => setSettings("profile", "age", (a) => a - 1)}
|
|
29
|
+
>
|
|
30
|
+
−
|
|
31
|
+
</button>
|
|
32
|
+
<strong>${() => settings.profile.age}</strong>
|
|
33
|
+
<button
|
|
34
|
+
class="border rounded w-8 h-8"
|
|
35
|
+
@click=${() => setSettings("profile", "age", (a) => a + 1)}
|
|
36
|
+
>
|
|
37
|
+
+
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<p class="text-sm">
|
|
42
|
+
Halo, <strong>${() => settings.profile.name}</strong> (umur
|
|
43
|
+
${() => settings.profile.age}).
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
`;
|
|
47
|
+
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// pages/
|
|
1
|
+
// pages/todos-page.ts — list keyed (For) + persisted + computed
|
|
2
2
|
|
|
3
3
|
import { component, html, signal, For } from "@sanify/core";
|
|
4
4
|
import { todos, remaining, addTodo, type Todo } from "../state/todos.ts";
|
|
5
5
|
import "../components/todo-item.ts";
|
|
6
6
|
|
|
7
|
-
component("
|
|
7
|
+
component("todos-page", () => {
|
|
8
8
|
const [draft, setDraft] = signal("");
|
|
9
|
-
|
|
10
9
|
const submit = () => {
|
|
11
10
|
addTodo(draft());
|
|
12
11
|
setDraft("");
|
|
@@ -14,10 +13,10 @@ component("todo-page", () => {
|
|
|
14
13
|
|
|
15
14
|
return () => html`
|
|
16
15
|
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
|
|
17
|
-
<h1 class="text-2xl font-bold mb-1">
|
|
16
|
+
<h1 class="text-2xl font-bold mb-1">Todos</h1>
|
|
18
17
|
<p class="text-sm text-slate-500 mb-4">
|
|
19
|
-
Sisa <strong>${() => remaining()}</strong>
|
|
20
|
-
tab
|
|
18
|
+
Sisa <strong>${() => remaining()}</strong> — persisted + cross-tab (buka
|
|
19
|
+
dua tab).
|
|
21
20
|
</p>
|
|
22
21
|
|
|
23
22
|
<div class="flex gap-2 mb-4">
|
|
@@ -47,12 +46,6 @@ component("todo-page", () => {
|
|
|
47
46
|
? html`<li class="text-slate-400 py-2">Belum ada tugas.</li>`
|
|
48
47
|
: null}
|
|
49
48
|
</ul>
|
|
50
|
-
|
|
51
|
-
<div class="mt-4 text-sm">
|
|
52
|
-
<a data-link href="/about" class="text-blue-600 hover:underline"
|
|
53
|
-
>Tentang →</a
|
|
54
|
-
>
|
|
55
|
-
</div>
|
|
56
49
|
</div>
|
|
57
50
|
`;
|
|
58
51
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// pages/user-detail.ts — resource() + params() reaktif (refetch saat :id berubah,
|
|
2
|
+
// tanpa membangun ulang komponen karena route-nya sama)
|
|
3
|
+
|
|
4
|
+
import { component, html, resource, params } from "@sanify/core";
|
|
5
|
+
import { fetchProfile } from "../data/users.ts";
|
|
6
|
+
|
|
7
|
+
component("user-detail", () => {
|
|
8
|
+
const profile = resource(() => fetchProfile(params().id ?? ""));
|
|
9
|
+
|
|
10
|
+
return () => html`
|
|
11
|
+
<div>
|
|
12
|
+
${() =>
|
|
13
|
+
profile.loading()
|
|
14
|
+
? html`<p class="text-slate-400">Memuat…</p>`
|
|
15
|
+
: null}
|
|
16
|
+
${() =>
|
|
17
|
+
profile.error()
|
|
18
|
+
? html`<p class="text-red-500">${String(profile.error())}</p>`
|
|
19
|
+
: null}
|
|
20
|
+
${() => {
|
|
21
|
+
const p = profile.data();
|
|
22
|
+
return p
|
|
23
|
+
? html`
|
|
24
|
+
<h2 class="text-xl font-bold">${p.name}</h2>
|
|
25
|
+
<p class="text-sm text-slate-500">${p.email}</p>
|
|
26
|
+
<p class="mt-2 text-slate-700">${p.bio}</p>
|
|
27
|
+
`
|
|
28
|
+
: null;
|
|
29
|
+
}}
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// pages/user-list.ts — index route di bawah layout /users
|
|
2
|
+
|
|
3
|
+
import { component, html } from "@sanify/core";
|
|
4
|
+
|
|
5
|
+
component("user-list", () => {
|
|
6
|
+
return () => html`
|
|
7
|
+
<p class="text-slate-500">
|
|
8
|
+
Pilih user di kiri untuk melihat profil (resource + params reaktif).
|
|
9
|
+
</p>
|
|
10
|
+
`;
|
|
11
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// state/settings.ts — store objek nested fine-grained (createStore)
|
|
2
|
+
// Edit satu field tidak memicu pembaca field lain.
|
|
3
|
+
|
|
4
|
+
import { createStore } from "@sanify/core";
|
|
5
|
+
|
|
6
|
+
export interface Settings {
|
|
7
|
+
profile: { name: string; age: number };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const [settings, setSettings] = createStore<Settings>({
|
|
11
|
+
profile: { name: "Tamu", age: 20 },
|
|
12
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// state/todos.ts —
|
|
1
|
+
// state/todos.ts — daftar todo: persisted (localStorage + cross-tab) + computed
|
|
2
2
|
|
|
3
3
|
import { computed, persisted } from "@sanify/core";
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ export interface Todo {
|
|
|
8
8
|
done: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const [todos, setTodos] = persisted<Todo[]>("
|
|
11
|
+
export const [todos, setTodos] = persisted<Todo[]>("app:todos", [], {
|
|
12
12
|
sync: true,
|
|
13
13
|
});
|
|
14
14
|
|