jac-coder 0.1.0__py3-none-any.whl

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 (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. jac_coder-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,321 @@
1
+ # Core Jac Patterns — Complete Working Examples
2
+
3
+ ## Basic Module with Entry Block
4
+
5
+ ```jac
6
+ """A simple greeting module."""
7
+
8
+ def greet(name: str) -> str {
9
+ return f"Hello, {name}!";
10
+ }
11
+
12
+ with entry {
13
+ message = greet("World");
14
+ print(message);
15
+ }
16
+ ```
17
+
18
+ ## Object with Fields and Methods
19
+
20
+ ```jac
21
+ """Object archetype with has declarations and methods."""
22
+
23
+ obj Person {
24
+ has name: str = "",
25
+ age: int = 0,
26
+ email: str = "";
27
+
28
+ def greet() -> str {
29
+ return f"Hi, I'm {self.name}, age {self.age}";
30
+ }
31
+
32
+ def is_adult() -> bool {
33
+ return self.age >= 18;
34
+ }
35
+ }
36
+
37
+ with entry {
38
+ p = Person(name="Alice", age=30, email="alice@example.com");
39
+ print(p.greet());
40
+ print(f"Adult: {p.is_adult()}");
41
+ }
42
+ ```
43
+
44
+ ## Interface and Implementation Separation
45
+
46
+ Declaration (`calculator.jac`):
47
+
48
+ ```jac
49
+ obj Calculator {
50
+ has result: float = 0.0;
51
+
52
+ def add(x: float) -> float;
53
+ def subtract(x: float) -> float;
54
+ def reset() -> None;
55
+ def get_result() -> float;
56
+ }
57
+ ```
58
+
59
+ Implementation (`impl/calculator.impl.jac`):
60
+
61
+ ```jac
62
+ impl Calculator.add(x: float) -> float {
63
+ self.result += x;
64
+ return self.result;
65
+ }
66
+
67
+ impl Calculator.subtract(x: float) -> float {
68
+ self.result -= x;
69
+ return self.result;
70
+ }
71
+
72
+ impl Calculator.reset() -> None {
73
+ self.result = 0.0;
74
+ }
75
+
76
+ impl Calculator.get_result() -> float {
77
+ return self.result;
78
+ }
79
+ ```
80
+
81
+ ## Walker Traversal
82
+
83
+ ```jac
84
+ """Walker that traverses a city graph."""
85
+
86
+ node City {
87
+ has name: str = "",
88
+ population: int = 0;
89
+ }
90
+
91
+ edge Road {
92
+ has distance: float = 0.0;
93
+ }
94
+
95
+ walker Explorer {
96
+ has visited: list[str] = [];
97
+
98
+ can visit_city with City entry {
99
+ self.visited.append(here.name);
100
+ print(f"Visiting {here.name} (pop: {here.population})");
101
+ visit [-->];
102
+ }
103
+ }
104
+
105
+ with entry {
106
+ nyc = City(name="New York", population=8_300_000);
107
+ la = City(name="Los Angeles", population=3_900_000);
108
+ chi = City(name="Chicago", population=2_700_000);
109
+
110
+ root ++> nyc;
111
+ nyc +>:Road(distance=790.0):+> chi;
112
+ chi +>:Road(distance=2015.0):+> la;
113
+
114
+ explorer = root spawn Explorer();
115
+ }
116
+ ```
117
+
118
+ ## Social Network Graph
119
+
120
+ ```jac
121
+ """Graph-based social network with typed edges."""
122
+
123
+ node User {
124
+ has username: str = "",
125
+ bio: str = "";
126
+ }
127
+
128
+ edge Follows {
129
+ has since: str = "";
130
+ }
131
+
132
+ with entry {
133
+ alice = User(username="alice", bio="Developer");
134
+ bob = User(username="bob", bio="Designer");
135
+ carol = User(username="carol", bio="Manager");
136
+
137
+ alice +>:Follows(since="2024-01"):+> bob;
138
+ alice +>:Follows(since="2024-03"):+> carol;
139
+ bob +>:Follows(since="2024-02"):+> carol;
140
+ }
141
+ ```
142
+
143
+ ## AI-Powered Function (by llm)
144
+
145
+ ```jac
146
+ """Using by llm() for AI-powered functions."""
147
+
148
+ import from byllm.lib { Model }
149
+
150
+ glob model: Model = Model(model_name="openai/gpt-4o-mini");
151
+
152
+ def summarize(text: str) -> str by model(
153
+ reason="Summarize the given text in 2-3 sentences"
154
+ );
155
+
156
+ enum Sentiment { POSITIVE, NEGATIVE, NEUTRAL }
157
+
158
+ def classify_sentiment(text: str) -> Sentiment by model(
159
+ reason="Classify the sentiment of the text"
160
+ );
161
+
162
+ with entry {
163
+ result = summarize("Long article text here...");
164
+ print(result);
165
+ }
166
+ ```
167
+
168
+ ## Test Blocks
169
+
170
+ ```jac
171
+ """Tests for the calculator module."""
172
+
173
+ import from calculator { Calculator }
174
+
175
+ test "calculator addition" {
176
+ calc = Calculator();
177
+ calc.add(5.0);
178
+ assert calc.get_result() == 5.0;
179
+ calc.add(3.0);
180
+ assert calc.get_result() == 8.0;
181
+ }
182
+
183
+ test "calculator reset" {
184
+ calc = Calculator();
185
+ calc.add(10.0);
186
+ calc.reset();
187
+ assert calc.get_result() == 0.0;
188
+ }
189
+ ```
190
+
191
+ ## Enum and Match/Case
192
+
193
+ ```jac
194
+ """Enum types and pattern matching."""
195
+
196
+ enum Color {
197
+ RED = "red",
198
+ GREEN = "green",
199
+ BLUE = "blue"
200
+ }
201
+
202
+ def describe_value(x: object) -> str {
203
+ match x {
204
+ case int():
205
+ return f"Integer: {x}";
206
+ case str():
207
+ return f"String: {x}";
208
+ case list():
209
+ return f"List with {len(x)} items";
210
+ case _:
211
+ return "Unknown type";
212
+ }
213
+ }
214
+
215
+ with entry {
216
+ print(describe_value(42));
217
+ print(describe_value("hello"));
218
+ }
219
+ ```
220
+
221
+ ## Async/Await
222
+
223
+ ```jac
224
+ """Async operations in Jac."""
225
+
226
+ import asyncio;
227
+
228
+ async def fetch_data(url: str) -> str {
229
+ await asyncio.sleep(0.1);
230
+ return f"Data from {url}";
231
+ }
232
+
233
+ with entry {
234
+ result = asyncio.run(fetch_data("https://example.com"));
235
+ print(result);
236
+ }
237
+ ```
238
+
239
+ ## CRUD Backend with Graph
240
+
241
+ ```jac
242
+ """Complete CRUD backend using nodes and def:pub endpoints."""
243
+
244
+ import from uuid { uuid4 }
245
+
246
+ node Product {
247
+ has id: str = "";
248
+ has name: str = "";
249
+ has price: float = 0.0;
250
+ has category: str = "";
251
+ }
252
+
253
+ def:pub add_product(name: str, price: float, category: str = "") -> dict {
254
+ product = (root ++> Product(
255
+ id=str(uuid4()), name=name, price=price, category=category
256
+ ))[0];
257
+ return {"id": product.id, "name": product.name, "price": product.price};
258
+ }
259
+
260
+ def:pub get_products(category: str = "") -> list {
261
+ all_products = [root-->][?:Product];
262
+ if category {
263
+ all_products = [p for p in all_products if p.category == category];
264
+ }
265
+ return [{"id": p.id, "name": p.name, "price": p.price}
266
+ for p in all_products];
267
+ }
268
+
269
+ def:pub delete_product(product_id: str) -> dict {
270
+ for p in [root-->][?:Product] {
271
+ if p.id == product_id {
272
+ root del--> p;
273
+ return {"success": True};
274
+ }
275
+ }
276
+ return {"success": False, "error": "Not found"};
277
+ }
278
+ ```
279
+
280
+ ## Multi-Node Walker Endpoint
281
+
282
+ ```jac
283
+ """Walker that traverses Order → OrderItems in one request."""
284
+
285
+ node Order {
286
+ has id: str = "";
287
+ has status: str = "pending";
288
+ has total: float = 0.0;
289
+ }
290
+
291
+ node OrderItem {
292
+ has name: str = "";
293
+ has qty: int = 0;
294
+ has price: float = 0.0;
295
+ }
296
+
297
+ walker :pub get_order_details {
298
+ has order_id: str = "";
299
+
300
+ can find_order with Root entry {
301
+ for order in [-->][?:Order] {
302
+ if order.id == self.order_id {
303
+ visit order;
304
+ return;
305
+ }
306
+ }
307
+ report {"error": "Order not found"};
308
+ }
309
+
310
+ can collect_items with Order entry {
311
+ items = [{"name": i.name, "qty": i.qty, "price": i.price}
312
+ for i in [-->][?:OrderItem]];
313
+ report {
314
+ "order_id": here.id,
315
+ "status": here.status,
316
+ "items": items,
317
+ "total": here.total
318
+ };
319
+ }
320
+ }
321
+ ```
@@ -0,0 +1,321 @@
1
+ # Example: Fullstack Todo App
2
+
3
+ Complete fullstack app: backend nodes, def:priv endpoints, hook, components, auth, file-based routing.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ todo-app/
9
+ ├── jac.toml
10
+ ├── main.jac # ALL backend + cl { app() }
11
+ ├── components/
12
+ │ ├── Layout.cl.jac # Root layout
13
+ │ ├── Header.cl.jac # Nav bar
14
+ │ ├── TodoList.cl.jac # List + add form (calls hook)
15
+ │ ├── TodoItem.cl.jac # Single item display
16
+ │ └── AddTodoForm.cl.jac # Input form
17
+ ├── hooks/
18
+ │ └── useTodos.cl.jac # Data hook (sv import + state)
19
+ ├── pages/
20
+ │ ├── layout.jac # Root layout with nav
21
+ │ ├── (public)/login.jac # Login page
22
+ │ └── (auth)/
23
+ │ ├── layout.jac # Auth guard
24
+ │ └── dashboard.jac # Dashboard
25
+ └── styles/global.css
26
+ ```
27
+
28
+ ## main.jac — Backend + Entry
29
+
30
+ ```jac
31
+ import from uuid { uuid4 }
32
+ import from datetime { datetime }
33
+
34
+ node Todo {
35
+ has id: str = "";
36
+ has title: str = "";
37
+ has completed: bool = False;
38
+ has created_at: str = "";
39
+
40
+ def postinit {
41
+ if not self.id { self.id = str(uuid4()); }
42
+ if not self.created_at { self.created_at = datetime.now().isoformat(); }
43
+ }
44
+ }
45
+
46
+ def:priv get_todos() -> list {
47
+ return [{"id": t.id, "title": t.title, "completed": t.completed}
48
+ for t in [root-->][?:Todo]];
49
+ }
50
+
51
+ def:priv add_todo(title: str) -> dict {
52
+ todo = (root ++> Todo(title=title))[0];
53
+ return {"id": todo.id, "title": todo.title, "completed": False};
54
+ }
55
+
56
+ def:priv toggle_todo(id: str) -> dict {
57
+ for todo in [root-->][?:Todo] {
58
+ if todo.id == id {
59
+ todo.completed = not todo.completed;
60
+ return {"id": todo.id, "completed": todo.completed};
61
+ }
62
+ }
63
+ return {"error": "not found"};
64
+ }
65
+
66
+ def:priv delete_todo(id: str) -> dict {
67
+ for todo in [root-->][?:Todo] {
68
+ if todo.id == id {
69
+ root del--> todo;
70
+ return {"deleted": id};
71
+ }
72
+ }
73
+ return {"error": "not found"};
74
+ }
75
+
76
+ cl import from .components.Layout { Layout }
77
+ cl {
78
+ def:pub app() -> JsxElement { return <Layout />; }
79
+ }
80
+ ```
81
+
82
+ ## hooks/useTodos.cl.jac — Data Hook
83
+
84
+ ```jac
85
+ sv import from ..main { get_todos, add_todo, toggle_todo, delete_todo }
86
+
87
+ def:pub useTodos() -> dict {
88
+ has todos: list = [];
89
+ has loading: bool = True;
90
+ has error: str = "";
91
+
92
+ async can with entry {
93
+ try {
94
+ result = await get_todos();
95
+ todos = result or [];
96
+ } except Exception as e {
97
+ error = str(e);
98
+ }
99
+ loading = False;
100
+ }
101
+
102
+ async def handleAdd(title: str) -> None {
103
+ if not title { return; }
104
+ result = await add_todo(title);
105
+ if result and not result.error {
106
+ todos = todos + [result];
107
+ }
108
+ }
109
+
110
+ async def handleToggle(id: str) -> None {
111
+ result = await toggle_todo(id);
112
+ if result and not result.error {
113
+ todos = [
114
+ ({"id": t["id"], "title": t["title"],
115
+ "completed": result["completed"]}
116
+ if t["id"] == id else t)
117
+ for t in todos
118
+ ];
119
+ }
120
+ }
121
+
122
+ async def handleDelete(id: str) -> None {
123
+ result = await delete_todo(id);
124
+ if result and not result.error {
125
+ todos = [t for t in todos if t["id"] != id];
126
+ }
127
+ }
128
+
129
+ return {
130
+ "todos": todos, "loading": loading, "error": error,
131
+ "handleAdd": handleAdd, "handleToggle": handleToggle,
132
+ "handleDelete": handleDelete
133
+ };
134
+ }
135
+ ```
136
+
137
+ ## components/Layout.cl.jac — Root Layout
138
+
139
+ ```jac
140
+ import "..styles.global.css";
141
+ import from ".Header" { Header }
142
+ import from ".TodoList" { TodoList }
143
+
144
+ def:pub Layout() -> JsxElement {
145
+ return (
146
+ <div className="min-h-screen bg-gray-50">
147
+ <Header title="Todo App" />
148
+ <main className="max-w-2xl mx-auto px-4 py-8">
149
+ <TodoList />
150
+ </main>
151
+ </div>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ## components/Header.cl.jac
157
+
158
+ ```jac
159
+ def:pub Header(props: dict) -> JsxElement {
160
+ title = props.title or "";
161
+ return (
162
+ <header className="border-b px-6 py-4">
163
+ <h1 className="text-xl font-bold">{title}</h1>
164
+ </header>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ## components/TodoItem.cl.jac
170
+
171
+ ```jac
172
+ def:pub TodoItem(props: dict) -> JsxElement {
173
+ todo = props.todo or {};
174
+ onToggle = props.onToggle or None;
175
+ onDelete = props.onDelete or None;
176
+ todoId = todo["id"] or "";
177
+ title = todo["title"] or "";
178
+ completed = todo["completed"] or False;
179
+
180
+ def handle_toggle() -> None {
181
+ if onToggle { onToggle(todoId); }
182
+ }
183
+
184
+ def handle_delete() -> None {
185
+ if onDelete { onDelete(todoId); }
186
+ }
187
+
188
+ return (
189
+ <div className="flex items-center gap-3 p-3 border rounded-lg">
190
+ <button onClick={handle_toggle} className="w-5 h-5 rounded border-2">
191
+ {completed and "✓"}
192
+ </button>
193
+ <span class={completed and "flex-1 line-through text-gray-400" or "flex-1"}>
194
+ {title}
195
+ </span>
196
+ <button onClick={handle_delete} className="text-red-500">×</button>
197
+ </div>
198
+ );
199
+ }
200
+ ```
201
+
202
+ ## components/AddTodoForm.cl.jac
203
+
204
+ ```jac
205
+ def:pub AddTodoForm(props: dict) -> JsxElement {
206
+ onAdd = props.onAdd or None;
207
+ has inputValue: str = "";
208
+
209
+ def handleSubmit(e: any) -> None {
210
+ e.preventDefault();
211
+ if inputValue and onAdd {
212
+ onAdd(inputValue);
213
+ inputValue = "";
214
+ }
215
+ }
216
+
217
+ def handle_input(e: any) -> None {
218
+ inputValue = e.target.value;
219
+ }
220
+
221
+ return (
222
+ <form onSubmit={handleSubmit} className="flex gap-2">
223
+ <input value={inputValue} onChange={handle_input}
224
+ placeholder="What needs to be done?"
225
+ className="flex-1 px-4 py-2 border rounded-lg" />
226
+ <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">+</button>
227
+ </form>
228
+ );
229
+ }
230
+ ```
231
+
232
+ ## components/TodoList.cl.jac
233
+
234
+ ```jac
235
+ import from ".TodoItem" { TodoItem }
236
+ import from ".AddTodoForm" { AddTodoForm }
237
+ import from "..hooks.useTodos" { useTodos }
238
+
239
+ def:pub TodoList() -> JsxElement {
240
+ todoData = useTodos();
241
+ todos = todoData["todos"] or [];
242
+
243
+ if todoData["loading"] {
244
+ return <p className="text-gray-400 text-center py-8">Loading...</p>;
245
+ }
246
+
247
+ return (
248
+ <div>
249
+ {todoData["error"] and (<p className="text-red-500 mb-4">{todoData["error"]}</p>)}
250
+ <div className="mb-6"><AddTodoForm onAdd={todoData["handleAdd"]} /></div>
251
+ {len(todos) == 0 and (<p className="text-gray-400 text-center py-8">No todos yet.</p>)}
252
+ <div className="space-y-2">
253
+ {[
254
+ <TodoItem key={todo["id"]} todo={todo}
255
+ onToggle={todoData["handleToggle"]}
256
+ onDelete={todoData["handleDelete"]} />
257
+ for todo in todos
258
+ ]}
259
+ </div>
260
+ </div>
261
+ );
262
+ }
263
+ ```
264
+
265
+ ## pages/(public)/login.jac — Login Page
266
+
267
+ ```jac
268
+ cl import from "@jac/runtime" { jacLogin, useNavigate }
269
+
270
+ cl {
271
+ def:pub page() -> JsxElement {
272
+ has username: str = "";
273
+ has password: str = "";
274
+ has error: str = "";
275
+ has loading: bool = False;
276
+ navigate = useNavigate();
277
+
278
+ async def handleLogin(e: any) -> None {
279
+ e.preventDefault();
280
+ loading = True;
281
+ error = "";
282
+ success = await jacLogin(username, password);
283
+ if success { navigate("/dashboard"); }
284
+ else { error = "Invalid credentials"; }
285
+ loading = False;
286
+ }
287
+
288
+ def handle_user(e: any) -> None { username = e.target.value; }
289
+ def handle_pass(e: any) -> None { password = e.target.value; }
290
+
291
+ return (
292
+ <div className="max-w-sm mx-auto">
293
+ <h1 className="text-2xl font-bold mb-6">Login</h1>
294
+ {error and <div className="p-3 mb-4 text-red-500">{error}</div>}
295
+ <form onSubmit={handleLogin} className="space-y-4">
296
+ <input value={username} onChange={handle_user} placeholder="Username"
297
+ className="w-full px-4 py-2 border rounded-lg" />
298
+ <input type="password" value={password} onChange={handle_pass}
299
+ placeholder="Password" className="w-full px-4 py-2 border rounded-lg" />
300
+ <button type="submit" disabled={loading}
301
+ className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">
302
+ {loading and "Logging in..." or "Login"}
303
+ </button>
304
+ </form>
305
+ </div>
306
+ );
307
+ }
308
+ }
309
+ ```
310
+
311
+ ## pages/(auth)/layout.jac — Auth Guard
312
+
313
+ ```jac
314
+ cl import from "@jac/runtime" { AuthGuard, Outlet }
315
+
316
+ cl {
317
+ def:pub layout() -> JsxElement {
318
+ return <AuthGuard redirect="/login"><Outlet /></AuthGuard>;
319
+ }
320
+ }
321
+ ```