create-sanifyfe 0.1.1 → 0.1.3
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 +10 -3
- package/templates/default/src/app.ts +19 -0
- package/templates/default/src/components/todo-item.ts +36 -0
- package/templates/default/src/main.ts +2 -155
- package/templates/default/src/pages/about-page.ts +16 -0
- package/templates/default/src/pages/todo-page.ts +58 -0
- package/templates/default/src/state/todos.ts +32 -0
- package/templates/default/tsconfig.json +1 -0
package/package.json
CHANGED
|
@@ -22,6 +22,13 @@ bun install
|
|
|
22
22
|
|
|
23
23
|
## Struktur
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
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)
|
|
32
|
+
index.html halaman host, memuat main.js hasil bundling
|
|
33
|
+
dev-server.ts server dev Bun, transpile TS on-the-fly
|
|
34
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// app.ts — root komponen + router
|
|
2
|
+
|
|
3
|
+
import { component, html, router } from "@sanify/core";
|
|
4
|
+
import "./pages/todo-page.ts";
|
|
5
|
+
import "./pages/about-page.ts";
|
|
6
|
+
|
|
7
|
+
component("app-root", () => {
|
|
8
|
+
const view = router({
|
|
9
|
+
"/": () => html`<todo-page></todo-page>`,
|
|
10
|
+
"/about": () => html`<about-page></about-page>`,
|
|
11
|
+
"*": () => html`
|
|
12
|
+
<div class="max-w-md mx-auto mt-10 text-center">
|
|
13
|
+
<p class="text-slate-500">Halaman tidak ditemukan.</p>
|
|
14
|
+
<a data-link href="/" class="text-blue-600 hover:underline">Beranda</a>
|
|
15
|
+
</div>
|
|
16
|
+
`,
|
|
17
|
+
});
|
|
18
|
+
return () => html`<div>${view}</div>`;
|
|
19
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// components/todo-item.ts — satu baris todo (menerima objek lewat property)
|
|
2
|
+
|
|
3
|
+
import { component, html, type ComponentContext } from "@sanify/core";
|
|
4
|
+
import { toggleTodo, removeTodo, type Todo } from "../state/todos.ts";
|
|
5
|
+
|
|
6
|
+
interface TodoItemProps {
|
|
7
|
+
todo: Todo;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
component<TodoItemProps>(
|
|
11
|
+
"todo-item",
|
|
12
|
+
({ props }: ComponentContext<TodoItemProps>) => {
|
|
13
|
+
return () => html`
|
|
14
|
+
<li class="flex items-center gap-3 py-2 border-b border-slate-200">
|
|
15
|
+
<input
|
|
16
|
+
type="checkbox"
|
|
17
|
+
class="h-4 w-4"
|
|
18
|
+
.checked=${() => props.todo().done}
|
|
19
|
+
@change=${() => toggleTodo(props.todo().id)}
|
|
20
|
+
/>
|
|
21
|
+
<span
|
|
22
|
+
class=${() =>
|
|
23
|
+
props.todo().done ? "flex-1 line-through text-slate-400" : "flex-1"}
|
|
24
|
+
>${() => props.todo().text}</span
|
|
25
|
+
>
|
|
26
|
+
<button
|
|
27
|
+
class="text-red-500 hover:text-red-700 text-sm"
|
|
28
|
+
@click=${() => removeTodo(props.todo().id)}
|
|
29
|
+
>
|
|
30
|
+
hapus
|
|
31
|
+
</button>
|
|
32
|
+
</li>
|
|
33
|
+
`;
|
|
34
|
+
},
|
|
35
|
+
{ props: ["todo"] },
|
|
36
|
+
);
|
|
@@ -1,156 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Todo dengan persist + cross-tab sync, routing, props dua arah, list & conditional.
|
|
1
|
+
// main.ts — entry aplikasi: cukup memuat app-root (komponen lain ikut terdaftar)
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
component,
|
|
6
|
-
html,
|
|
7
|
-
signal,
|
|
8
|
-
computed,
|
|
9
|
-
persisted,
|
|
10
|
-
router,
|
|
11
|
-
navigate,
|
|
12
|
-
type ComponentContext,
|
|
13
|
-
} from "@sanify/core";
|
|
14
|
-
|
|
15
|
-
// ── State global (persisted + cross-tab sync) ───────────────
|
|
16
|
-
interface Todo {
|
|
17
|
-
id: number;
|
|
18
|
-
text: string;
|
|
19
|
-
done: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const [todos, setTodos] = persisted<Todo[]>("sanify:todos", [], { sync: true });
|
|
23
|
-
|
|
24
|
-
const remaining = computed(() => todos().filter((t) => !t.done).length);
|
|
25
|
-
|
|
26
|
-
function addTodo(text: string): void {
|
|
27
|
-
const trimmed = text.trim();
|
|
28
|
-
if (!trimmed) return;
|
|
29
|
-
setTodos((prev) => [...prev, { id: Date.now(), text: trimmed, done: false }]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function toggleTodo(id: number): void {
|
|
33
|
-
setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function removeTodo(id: number): void {
|
|
37
|
-
setTodos((prev) => prev.filter((t) => t.id !== id));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── Komponen: satu baris todo (menerima objek lewat property) ─
|
|
41
|
-
interface TodoItemProps {
|
|
42
|
-
todo: Todo;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
component<TodoItemProps>(
|
|
46
|
-
"todo-item",
|
|
47
|
-
({ props }: ComponentContext<TodoItemProps>) => {
|
|
48
|
-
return () => html`
|
|
49
|
-
<li class="flex items-center gap-3 py-2 border-b border-slate-200">
|
|
50
|
-
<input
|
|
51
|
-
type="checkbox"
|
|
52
|
-
class="h-4 w-4"
|
|
53
|
-
.checked=${() => props.todo().done}
|
|
54
|
-
@change=${() => toggleTodo(props.todo().id)}
|
|
55
|
-
/>
|
|
56
|
-
<span
|
|
57
|
-
class=${() =>
|
|
58
|
-
props.todo().done
|
|
59
|
-
? "flex-1 line-through text-slate-400"
|
|
60
|
-
: "flex-1"}
|
|
61
|
-
>${() => props.todo().text}</span
|
|
62
|
-
>
|
|
63
|
-
<button
|
|
64
|
-
class="text-red-500 hover:text-red-700 text-sm"
|
|
65
|
-
@click=${() => removeTodo(props.todo().id)}
|
|
66
|
-
>
|
|
67
|
-
hapus
|
|
68
|
-
</button>
|
|
69
|
-
</li>
|
|
70
|
-
`;
|
|
71
|
-
},
|
|
72
|
-
{ props: ["todo"] },
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
// ── Halaman: daftar todo ────────────────────────────────────
|
|
76
|
-
component("todo-page", () => {
|
|
77
|
-
const [draft, setDraft] = signal("");
|
|
78
|
-
|
|
79
|
-
const submit = () => {
|
|
80
|
-
addTodo(draft());
|
|
81
|
-
setDraft("");
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
return () => html`
|
|
85
|
-
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
|
|
86
|
-
<h1 class="text-2xl font-bold mb-1">Todo</h1>
|
|
87
|
-
<p class="text-sm text-slate-500 mb-4">
|
|
88
|
-
Sisa <strong>${() => remaining()}</strong> belum selesai
|
|
89
|
-
— buka di dua tab untuk lihat sync.
|
|
90
|
-
</p>
|
|
91
|
-
|
|
92
|
-
<div class="flex gap-2 mb-4">
|
|
93
|
-
<input
|
|
94
|
-
class="flex-1 border border-slate-300 rounded px-3 py-2"
|
|
95
|
-
placeholder="Tambah tugas..."
|
|
96
|
-
.value=${() => draft()}
|
|
97
|
-
@input=${(e: Event) => setDraft((e.target as HTMLInputElement).value)}
|
|
98
|
-
@keydown=${(e: KeyboardEvent) => e.key === "Enter" && submit()}
|
|
99
|
-
/>
|
|
100
|
-
<button
|
|
101
|
-
class="bg-slate-800 text-white rounded px-4 py-2 hover:bg-slate-700"
|
|
102
|
-
@click=${submit}
|
|
103
|
-
>
|
|
104
|
-
Tambah
|
|
105
|
-
</button>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<ul>
|
|
109
|
-
${() =>
|
|
110
|
-
todos().length === 0
|
|
111
|
-
? html`<li class="text-slate-400 py-2">Belum ada tugas.</li>`
|
|
112
|
-
: todos().map((t) => html`<todo-item .todo=${t}></todo-item>`)}
|
|
113
|
-
</ul>
|
|
114
|
-
|
|
115
|
-
<div class="mt-4 text-sm">
|
|
116
|
-
<a data-link href="/about" class="text-blue-600 hover:underline"
|
|
117
|
-
>Tentang →</a
|
|
118
|
-
>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
`;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// ── Halaman: about ──────────────────────────────────────────
|
|
125
|
-
component("about-page", () => {
|
|
126
|
-
return () => html`
|
|
127
|
-
<div class="max-w-md mx-auto mt-10 p-6 bg-white rounded-xl shadow">
|
|
128
|
-
<h1 class="text-2xl font-bold mb-2">Tentang</h1>
|
|
129
|
-
<p class="text-slate-600 mb-4">
|
|
130
|
-
Contoh kecil yang dibangun dengan Sanify: signal fine-grained,
|
|
131
|
-
Web Components Light DOM, persist + cross-tab sync, dan router.
|
|
132
|
-
</p>
|
|
133
|
-
<a data-link href="/" class="text-blue-600 hover:underline"
|
|
134
|
-
>← Kembali</a
|
|
135
|
-
>
|
|
136
|
-
</div>
|
|
137
|
-
`;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ── Root + router ───────────────────────────────────────────
|
|
141
|
-
component("app-root", () => {
|
|
142
|
-
const view = router({
|
|
143
|
-
"/": () => html`<todo-page></todo-page>`,
|
|
144
|
-
"/about": () => html`<about-page></about-page>`,
|
|
145
|
-
"*": () => html`
|
|
146
|
-
<div class="max-w-md mx-auto mt-10 text-center">
|
|
147
|
-
<p class="text-slate-500">Halaman tidak ditemukan.</p>
|
|
148
|
-
<a data-link href="/" class="text-blue-600 hover:underline">Beranda</a>
|
|
149
|
-
</div>
|
|
150
|
-
`,
|
|
151
|
-
});
|
|
152
|
-
return () => html`<div>${view}</div>`;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// hindari unused-import error untuk navigate (tersedia utk pemakaian manual)
|
|
156
|
-
void navigate;
|
|
3
|
+
import "./app.ts";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// pages/about-page.ts — halaman tentang
|
|
2
|
+
|
|
3
|
+
import { component, html } from "@sanify/core";
|
|
4
|
+
|
|
5
|
+
component("about-page", () => {
|
|
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.
|
|
12
|
+
</p>
|
|
13
|
+
<a data-link href="/" class="text-blue-600 hover:underline">← Kembali</a>
|
|
14
|
+
</div>
|
|
15
|
+
`;
|
|
16
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// pages/todo-page.ts — halaman daftar todo
|
|
2
|
+
|
|
3
|
+
import { component, html, signal, For } from "@sanify/core";
|
|
4
|
+
import { todos, remaining, addTodo, type Todo } from "../state/todos.ts";
|
|
5
|
+
import "../components/todo-item.ts";
|
|
6
|
+
|
|
7
|
+
component("todo-page", () => {
|
|
8
|
+
const [draft, setDraft] = signal("");
|
|
9
|
+
|
|
10
|
+
const submit = () => {
|
|
11
|
+
addTodo(draft());
|
|
12
|
+
setDraft("");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return () => html`
|
|
16
|
+
<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>
|
|
18
|
+
<p class="text-sm text-slate-500 mb-4">
|
|
19
|
+
Sisa <strong>${() => remaining()}</strong> belum selesai — buka di dua
|
|
20
|
+
tab untuk lihat sync.
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<div class="flex gap-2 mb-4">
|
|
24
|
+
<input
|
|
25
|
+
class="flex-1 border border-slate-300 rounded px-3 py-2"
|
|
26
|
+
placeholder="Tambah tugas..."
|
|
27
|
+
.value=${() => draft()}
|
|
28
|
+
@input=${(e: Event) => setDraft((e.target as HTMLInputElement).value)}
|
|
29
|
+
@keydown=${(e: KeyboardEvent) => e.key === "Enter" && submit()}
|
|
30
|
+
/>
|
|
31
|
+
<button
|
|
32
|
+
class="bg-slate-800 text-white rounded px-4 py-2 hover:bg-slate-700"
|
|
33
|
+
@click=${submit}
|
|
34
|
+
>
|
|
35
|
+
Tambah
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<ul>
|
|
40
|
+
${For(
|
|
41
|
+
() => todos(),
|
|
42
|
+
(todo: () => Todo) => html`<todo-item .todo=${todo}></todo-item>`,
|
|
43
|
+
{ key: (t: Todo) => t.id },
|
|
44
|
+
)}
|
|
45
|
+
${() =>
|
|
46
|
+
todos().length === 0
|
|
47
|
+
? html`<li class="text-slate-400 py-2">Belum ada tugas.</li>`
|
|
48
|
+
: null}
|
|
49
|
+
</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
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// state/todos.ts — state global todo (persisted + cross-tab sync)
|
|
2
|
+
|
|
3
|
+
import { computed, persisted } from "@sanify/core";
|
|
4
|
+
|
|
5
|
+
export interface Todo {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
done: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const [todos, setTodos] = persisted<Todo[]>("sanify:todos", [], {
|
|
12
|
+
sync: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const remaining = computed(() => todos().filter((t) => !t.done).length);
|
|
16
|
+
|
|
17
|
+
export function addTodo(text: string): void {
|
|
18
|
+
const trimmed = text.trim();
|
|
19
|
+
if (!trimmed) return;
|
|
20
|
+
setTodos((prev) => [
|
|
21
|
+
...prev,
|
|
22
|
+
{ id: crypto.randomUUID(), text: trimmed, done: false },
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toggleTodo(id: string): void {
|
|
27
|
+
setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function removeTodo(id: string): void {
|
|
31
|
+
setTodos((prev) => prev.filter((t) => t.id !== id));
|
|
32
|
+
}
|