create-tinyjoin 0.0.5 → 0.1.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/README.md +70 -4
- package/cli.js +4 -4
- package/package.json +1 -1
- package/templates/AGENTS.md.hbs +67 -12
- package/templates/README.md.hbs +74 -13
- package/templates/client/index.html.hbs +48 -45
- package/templates/client/package.json.hbs +4 -2
- package/templates/client/public/favicon.svg +6 -0
- package/templates/client/public/js.svg +5 -0
- package/templates/client/public/ts.svg +6 -0
- package/templates/client/src/app.ts.hbs +37 -0
- package/templates/client/src/button.css.hbs +30 -0
- package/templates/client/src/button.ts.hbs +19 -0
- package/templates/client/src/database.ts.hbs +62 -25
- package/templates/client/src/error.css.hbs +6 -0
- package/templates/client/src/error.ts.hbs +39 -0
- package/templates/client/src/index.ts.hbs +7 -0
- package/templates/client/src/info.css.hbs +93 -0
- package/templates/client/src/info.ts.hbs +42 -0
- package/templates/client/src/input.css.hbs +22 -0
- package/templates/client/src/input.ts.hbs +16 -0
- package/templates/client/src/loading.css.hbs +19 -0
- package/templates/client/src/loading.ts.hbs +12 -0
- package/templates/client/src/title.css.hbs +16 -0
- package/templates/client/src/title.ts.hbs +16 -0
- package/templates/client/src/todoInput.css.hbs +16 -0
- package/templates/client/src/todoInput.ts.hbs +50 -0
- package/templates/client/src/todoItem.css.hbs +33 -0
- package/templates/client/src/todoItem.ts.hbs +31 -0
- package/templates/client/src/todoList.css.hbs +15 -0
- package/templates/client/src/todoList.ts.hbs +89 -0
- package/templates/client/src/topBar.css.hbs +13 -0
- package/templates/client/src/topBar.ts.hbs +13 -0
- package/templates/client/vite.config.ts.hbs +6 -0
- package/templates/client/src/main.ts.hbs +0 -159
- package/templates/client/src/style.css.hbs +0 -199
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import './style.css';
|
|
2
|
-
|
|
3
|
-
import {openDatabase, type Database, type Todo} from './database';
|
|
4
|
-
|
|
5
|
-
const form = requiredElement<HTMLFormElement>('[data-testid="todo-form"]');
|
|
6
|
-
const titleInput = requiredElement<HTMLInputElement>(
|
|
7
|
-
'[data-testid="todo-title"]',
|
|
8
|
-
);
|
|
9
|
-
const todosElement = requiredElement<HTMLUListElement>('[data-testid="todos"]');
|
|
10
|
-
const statusElement = requiredElement<HTMLElement>('[data-testid="status"]');
|
|
11
|
-
const errorElement = requiredElement<HTMLElement>('[data-testid="error"]');
|
|
12
|
-
|
|
13
|
-
async function start(): Promise<void> {
|
|
14
|
-
const database = await openDatabase();
|
|
15
|
-
const unsubscribe = database.subscribe({tables: ['todos']}, () => {
|
|
16
|
-
runAction(() => renderTodos(database));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
addEventListener(
|
|
20
|
-
'pagehide',
|
|
21
|
-
() => {
|
|
22
|
-
unsubscribe();
|
|
23
|
-
void database.close();
|
|
24
|
-
},
|
|
25
|
-
{once: true},
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
form.addEventListener('submit', (event) => {
|
|
29
|
-
event.preventDefault();
|
|
30
|
-
const title = titleInput.value.trim();
|
|
31
|
-
if (!title) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
runAction(async () => {
|
|
35
|
-
await database.sql`
|
|
36
|
-
INSERT INTO todos (id, title, done)
|
|
37
|
-
VALUES (${crypto.randomUUID()}, ${title}, ${false})
|
|
38
|
-
`;
|
|
39
|
-
form.reset();
|
|
40
|
-
titleInput.focus();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
todosElement.addEventListener('change', (event) => {
|
|
45
|
-
const checkbox = event.target;
|
|
46
|
-
const id =
|
|
47
|
-
checkbox instanceof HTMLInputElement
|
|
48
|
-
? checkbox.dataset.todoId
|
|
49
|
-
: undefined;
|
|
50
|
-
if (
|
|
51
|
-
!(checkbox instanceof HTMLInputElement) ||
|
|
52
|
-
checkbox.dataset.action !== 'toggle' ||
|
|
53
|
-
!id
|
|
54
|
-
) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
runAction(async () => {
|
|
58
|
-
await database.sql`
|
|
59
|
-
UPDATE todos SET done = ${checkbox.checked}
|
|
60
|
-
WHERE id = ${id}
|
|
61
|
-
`;
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
todosElement.addEventListener('click', (event) => {
|
|
66
|
-
const target = event.target;
|
|
67
|
-
const button =
|
|
68
|
-
target instanceof Element
|
|
69
|
-
? target.closest<HTMLButtonElement>('button[data-action="delete"]')
|
|
70
|
-
: null;
|
|
71
|
-
const id = button?.dataset.todoId;
|
|
72
|
-
if (!id) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
runAction(async () => {
|
|
76
|
-
await database.sql`DELETE FROM todos WHERE id = ${id}`;
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
await renderTodos(database);
|
|
81
|
-
titleInput.focus();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function renderTodos(database: Database): Promise<void> {
|
|
85
|
-
todosElement.setAttribute('aria-busy', 'true');
|
|
86
|
-
const {rows} = await database.query<Todo>(
|
|
87
|
-
'SELECT id, title, done FROM todos ORDER BY title, id',
|
|
88
|
-
);
|
|
89
|
-
todosElement.replaceChildren(
|
|
90
|
-
...(rows.length === 0 ? [createEmptyState()] : rows.map(createTodo)),
|
|
91
|
-
);
|
|
92
|
-
todosElement.setAttribute('aria-busy', 'false');
|
|
93
|
-
statusElement.textContent = 'Ready';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function createTodo(todo: Todo): HTMLLIElement {
|
|
97
|
-
const item = document.createElement('li');
|
|
98
|
-
item.className = `todo${todo.done ? ' is-done' : ''}`;
|
|
99
|
-
item.dataset.todoId = todo.id;
|
|
100
|
-
|
|
101
|
-
const label = document.createElement('label');
|
|
102
|
-
const checkbox = document.createElement('input');
|
|
103
|
-
checkbox.type = 'checkbox';
|
|
104
|
-
checkbox.checked = todo.done;
|
|
105
|
-
checkbox.dataset.action = 'toggle';
|
|
106
|
-
checkbox.dataset.todoId = todo.id;
|
|
107
|
-
checkbox.setAttribute(
|
|
108
|
-
'aria-label',
|
|
109
|
-
`Mark ${todo.title} as ${todo.done ? 'not done' : 'done'}`,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const title = document.createElement('span');
|
|
113
|
-
title.textContent = todo.title;
|
|
114
|
-
label.append(checkbox, title);
|
|
115
|
-
|
|
116
|
-
const deleteButton = document.createElement('button');
|
|
117
|
-
deleteButton.type = 'button';
|
|
118
|
-
deleteButton.className = 'delete';
|
|
119
|
-
deleteButton.dataset.action = 'delete';
|
|
120
|
-
deleteButton.dataset.todoId = todo.id;
|
|
121
|
-
deleteButton.setAttribute('aria-label', `Delete ${todo.title}`);
|
|
122
|
-
deleteButton.textContent = 'Delete';
|
|
123
|
-
|
|
124
|
-
item.append(label, deleteButton);
|
|
125
|
-
return item;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function createEmptyState(): HTMLLIElement {
|
|
129
|
-
const item = document.createElement('li');
|
|
130
|
-
item.className = 'empty';
|
|
131
|
-
item.textContent = 'No todos yet. Add your first one above.';
|
|
132
|
-
return item;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function runAction(action: () => Promise<unknown>): void {
|
|
136
|
-
clearError();
|
|
137
|
-
void action().catch(showError);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function clearError(): void {
|
|
141
|
-
errorElement.hidden = true;
|
|
142
|
-
errorElement.textContent = '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function showError(error: unknown): void {
|
|
146
|
-
statusElement.textContent = 'Something went wrong.';
|
|
147
|
-
errorElement.hidden = false;
|
|
148
|
-
errorElement.textContent = error instanceof Error ? error.message : String(error);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function requiredElement<ElementType extends Element>(selector: string): ElementType {
|
|
152
|
-
const element = document.querySelector<ElementType>(selector);
|
|
153
|
-
if (!element) {
|
|
154
|
-
throw new Error(`Missing starter element: ${selector}`);
|
|
155
|
-
}
|
|
156
|
-
return element;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
void start().catch(showError);
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color: #20251f;
|
|
3
|
-
background: #f4f6f1;
|
|
4
|
-
font-family:
|
|
5
|
-
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
6
|
-
sans-serif;
|
|
7
|
-
font-synthesis: none;
|
|
8
|
-
text-rendering: optimizeLegibility;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
* {
|
|
12
|
-
box-sizing: border-box;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
body {
|
|
16
|
-
min-width: 20rem;
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
margin: 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
button,
|
|
22
|
-
input {
|
|
23
|
-
font: inherit;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
button {
|
|
27
|
-
cursor: pointer;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
main {
|
|
31
|
-
width: min(36rem, calc(100% - 2rem));
|
|
32
|
-
margin: 0 auto;
|
|
33
|
-
padding: 5rem 0 3rem;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
header {
|
|
37
|
-
margin-bottom: 2rem;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.eyebrow {
|
|
41
|
-
margin: 0 0 0.5rem;
|
|
42
|
-
color: #397253;
|
|
43
|
-
font-size: 0.78rem;
|
|
44
|
-
font-weight: 800;
|
|
45
|
-
letter-spacing: 0.08em;
|
|
46
|
-
text-transform: uppercase;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
h1 {
|
|
50
|
-
margin: 0;
|
|
51
|
-
font-family: Georgia, "Times New Roman", serif;
|
|
52
|
-
font-size: clamp(3rem, 12vw, 5rem);
|
|
53
|
-
line-height: 0.95;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.intro {
|
|
57
|
-
margin: 1rem 0 0;
|
|
58
|
-
color: #697066;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
form {
|
|
62
|
-
padding: 1rem;
|
|
63
|
-
border: 1px solid #d7ddd3;
|
|
64
|
-
border-radius: 1rem;
|
|
65
|
-
background: #fff;
|
|
66
|
-
box-shadow: 0 1rem 3rem rgb(32 49 38 / 8%);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
form label {
|
|
70
|
-
display: block;
|
|
71
|
-
margin-bottom: 0.65rem;
|
|
72
|
-
font-size: 0.85rem;
|
|
73
|
-
font-weight: 750;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.composer {
|
|
77
|
-
display: flex;
|
|
78
|
-
gap: 0.6rem;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.composer input {
|
|
82
|
-
min-width: 0;
|
|
83
|
-
flex: 1;
|
|
84
|
-
padding: 0.8rem 0.9rem;
|
|
85
|
-
border: 1px solid #cbd3c7;
|
|
86
|
-
border-radius: 0.7rem;
|
|
87
|
-
color: inherit;
|
|
88
|
-
background: #fbfcfa;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.composer input:focus {
|
|
92
|
-
border-color: #397253;
|
|
93
|
-
outline: 3px solid rgb(57 114 83 / 15%);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.composer button,
|
|
97
|
-
.delete {
|
|
98
|
-
border: 0;
|
|
99
|
-
border-radius: 0.7rem;
|
|
100
|
-
font-weight: 750;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.composer button {
|
|
104
|
-
padding: 0.8rem 1.15rem;
|
|
105
|
-
color: #fff;
|
|
106
|
-
background: #397253;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.status,
|
|
110
|
-
.error {
|
|
111
|
-
min-height: 1.4rem;
|
|
112
|
-
margin: 1rem 0 0.5rem;
|
|
113
|
-
color: #697066;
|
|
114
|
-
font-size: 0.85rem;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.error {
|
|
118
|
-
color: #a0382e;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
ul {
|
|
122
|
-
display: grid;
|
|
123
|
-
gap: 0.65rem;
|
|
124
|
-
margin: 0;
|
|
125
|
-
padding: 0;
|
|
126
|
-
list-style: none;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.todo,
|
|
130
|
-
.empty {
|
|
131
|
-
border: 1px solid #d7ddd3;
|
|
132
|
-
border-radius: 0.85rem;
|
|
133
|
-
background: #fff;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.todo {
|
|
137
|
-
display: flex;
|
|
138
|
-
align-items: center;
|
|
139
|
-
justify-content: space-between;
|
|
140
|
-
gap: 1rem;
|
|
141
|
-
padding: 0.8rem 0.9rem;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.todo label {
|
|
145
|
-
display: flex;
|
|
146
|
-
min-width: 0;
|
|
147
|
-
flex: 1;
|
|
148
|
-
align-items: center;
|
|
149
|
-
gap: 0.75rem;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.todo input {
|
|
153
|
-
width: 1.15rem;
|
|
154
|
-
height: 1.15rem;
|
|
155
|
-
accent-color: #397253;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.todo span {
|
|
159
|
-
overflow-wrap: anywhere;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.todo.is-done span {
|
|
163
|
-
color: #8a9088;
|
|
164
|
-
text-decoration: line-through;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.delete {
|
|
168
|
-
padding: 0.45rem 0.65rem;
|
|
169
|
-
color: #7b423c;
|
|
170
|
-
background: #f7ecea;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.empty {
|
|
174
|
-
padding: 2rem;
|
|
175
|
-
color: #7b8278;
|
|
176
|
-
text-align: center;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
footer {
|
|
180
|
-
margin-top: 2rem;
|
|
181
|
-
color: #7b8278;
|
|
182
|
-
font-size: 0.82rem;
|
|
183
|
-
text-align: center;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
a {
|
|
187
|
-
color: #397253;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
@media (max-width: 32rem) {
|
|
191
|
-
main {
|
|
192
|
-
padding-top: 2.5rem;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.composer {
|
|
196
|
-
align-items: stretch;
|
|
197
|
-
flex-direction: column;
|
|
198
|
-
}
|
|
199
|
-
}
|