create-sanifyfe 0.1.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sanifyfe",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold project Sanify baru — bun create sanifyfe",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,10 +25,21 @@ bun install
25
25
  ```
26
26
  src/
27
27
  main.ts entry: memuat app-root
28
- app.ts root komponen + definisi router
29
- state/todos.ts state global (persisted + cross-tab sync)
30
- components/ komponen UI (todo-item)
31
- pages/ halaman per-route (todo-page, about-page)
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 |
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@sanify/core": "^0.1.0"
12
+ "@sanify/core": "^0.2.0"
13
13
  },
14
14
  "devDependencies": {
15
15
  "typescript": "^5.9.0",
@@ -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 "./pages/todo-page.ts";
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`<todo-page></todo-page>`,
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-10 text-center">
13
- <p class="text-slate-500">Halaman tidak ditemukan.</p>
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
- return () => html`<div>${view}</div>`;
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 property)
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-200">
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,3 +1,3 @@
1
- // main.ts — entry aplikasi: cukup memuat app-root (komponen lain ikut terdaftar)
1
+ // main.ts — entry: cukup memuat app-root (komponen lain ikut terdaftar)
2
2
 
3
3
  import "./app.ts";
@@ -1,16 +1,24 @@
1
- // pages/about-page.ts — halaman tentang
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 mb-2">Tentang</h1>
9
- <p class="text-slate-600 mb-4">
10
- Contoh kecil yang dibangun dengan Sanify: signal fine-grained, Web
11
- Components Light DOM, persist + cross-tab sync, dan router.
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">&larr; 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/todo-page.ts — halaman daftar todo
1
+ // pages/todos-page.ts — list keyed (For) + persisted + computed
2
2
 
3
- import { component, html, signal } from "@sanify/core";
4
- import { todos, remaining, addTodo } from "../state/todos.ts";
3
+ import { component, html, signal, For } from "@sanify/core";
4
+ import { todos, remaining, addTodo, type Todo } from "../state/todos.ts";
5
5
  import "../components/todo-item.ts";
6
6
 
7
- component("todo-page", () => {
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">Todo</h1>
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> belum selesai buka di dua
20
- tab untuk lihat sync.
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">
@@ -37,17 +36,16 @@ component("todo-page", () => {
37
36
  </div>
38
37
 
39
38
  <ul>
39
+ ${For(
40
+ () => todos(),
41
+ (todo: () => Todo) => html`<todo-item .todo=${todo}></todo-item>`,
42
+ { key: (t: Todo) => t.id },
43
+ )}
40
44
  ${() =>
41
45
  todos().length === 0
42
46
  ? html`<li class="text-slate-400 py-2">Belum ada tugas.</li>`
43
- : todos().map((t) => html`<todo-item .todo=${t}></todo-item>`)}
47
+ : null}
44
48
  </ul>
45
-
46
- <div class="mt-4 text-sm">
47
- <a data-link href="/about" class="text-blue-600 hover:underline"
48
- >Tentang &rarr;</a
49
- >
50
- </div>
51
49
  </div>
52
50
  `;
53
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,14 +1,14 @@
1
- // state/todos.ts — state global todo (persisted + cross-tab sync)
1
+ // state/todos.ts — daftar todo: persisted (localStorage + cross-tab) + computed
2
2
 
3
3
  import { computed, persisted } from "@sanify/core";
4
4
 
5
5
  export interface Todo {
6
- id: number;
6
+ id: string;
7
7
  text: string;
8
8
  done: boolean;
9
9
  }
10
10
 
11
- export const [todos, setTodos] = persisted<Todo[]>("sanify:todos", [], {
11
+ export const [todos, setTodos] = persisted<Todo[]>("app:todos", [], {
12
12
  sync: true,
13
13
  });
14
14
 
@@ -17,13 +17,16 @@ export const remaining = computed(() => todos().filter((t) => !t.done).length);
17
17
  export function addTodo(text: string): void {
18
18
  const trimmed = text.trim();
19
19
  if (!trimmed) return;
20
- setTodos((prev) => [...prev, { id: Date.now(), text: trimmed, done: false }]);
20
+ setTodos((prev) => [
21
+ ...prev,
22
+ { id: crypto.randomUUID(), text: trimmed, done: false },
23
+ ]);
21
24
  }
22
25
 
23
- export function toggleTodo(id: number): void {
26
+ export function toggleTodo(id: string): void {
24
27
  setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
25
28
  }
26
29
 
27
- export function removeTodo(id: number): void {
30
+ export function removeTodo(id: string): void {
28
31
  setTodos((prev) => prev.filter((t) => t.id !== id));
29
32
  }