create-janux 0.1.0 → 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-janux",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new Janux app: bunx create-janux my-app",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,34 @@
1
+ # __APP_NAME__
2
+
3
+ A [Janux](https://github.com/aralroca/Janux) app. One component, two faces: this task board is a UI for you **and** a set of typed tools for your copilot.
4
+
5
+ ## Run it
6
+
7
+ ```bash
8
+ bun install
9
+ bun run dev # http://localhost:3000
10
+ bun test # the example unit test — no browser needed
11
+ ```
12
+
13
+ Enable the copilot (optional): copy `.env.example` to `.env` and set `JANUX_MODEL` or one provider API key.
14
+
15
+ ## What's inside
16
+
17
+ | File | Shows |
18
+ |---|---|
19
+ | `src/components/TaskBoard.tsx` | State, derived, intents, a `confirm` guard, a debounced persist effect, events |
20
+ | `src/components/ThemeToggle.tsx` | Two islands sharing a store |
21
+ | `src/stores.ts` | Shared state as `store://theme` |
22
+ | `src/server/tasks.api.ts` | api(): endpoint + client stub + agent tool from one definition |
23
+ | `src/components/Copilot.tsx` | The copilot chrome: turn protocol + proposals with human approve |
24
+ | `src/components/TaskBoard.test.ts` | Testing intents & guards without a browser |
25
+
26
+ ## See the second face
27
+
28
+ ```bash
29
+ curl -s localhost:3000/_janux/manifest | jq # what agents see
30
+ ```
31
+
32
+ Try asking the copilot: *"add a task to buy milk, then show me what's left"* — and watch `tasks.clearDone` come back as a proposal you approve on screen.
33
+
34
+ Docs: start with the [tutorial](https://github.com/aralroca/Janux/tree/main/apps/docs/content/tutorial) — it builds exactly this app.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "__APP_NAME__",
3
3
  "private": true,
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "janux dev",
@@ -1,5 +1,8 @@
1
1
  import { defineAgent } from '@janux/agent';
2
2
 
3
3
  export default defineAgent({
4
- instructions: 'You are this app’s copilot. Use the counter tools and api.hello.greet.',
4
+ instructions:
5
+ 'You are this task app’s copilot. Use the tasks.* tools to add, toggle, filter and clear ' +
6
+ 'tasks, api.tasks.taskStats for stats, and theme.toggle for appearance. clearDone needs ' +
7
+ 'human approval — propose it, never insist.',
5
8
  });
@@ -1,4 +1,7 @@
1
1
  import { boot } from 'janux/client';
2
- import { Counter } from './components/Counter';
2
+ import { TaskBoard } from './components/TaskBoard';
3
+ import { ThemeToggle } from './components/ThemeToggle';
4
+ import { Copilot } from './components/Copilot';
5
+ import { theme } from './stores';
3
6
 
4
- boot({ defs: [Counter] });
7
+ boot({ defs: [TaskBoard, ThemeToggle, Copilot, theme] });
@@ -0,0 +1,103 @@
1
+ import { component, intent, schema, str, bool, enums, list } from 'janux';
2
+
3
+ let wire: any[] = [];
4
+
5
+ async function postAgent(messages: unknown[], path: string): Promise<any> {
6
+ const response = await fetch('/_janux/agent', {
7
+ method: 'POST',
8
+ headers: { 'content-type': 'application/json' },
9
+ body: JSON.stringify({ messages, path }),
10
+ });
11
+
12
+ return response.json();
13
+ }
14
+
15
+ async function converse(state: any, path: string): Promise<void> {
16
+ let reply = await postAgent(wire, path);
17
+
18
+ while (reply.type === 'ui_calls') {
19
+ wire = reply.messages;
20
+ for (const call of reply.calls) {
21
+ const result = await (window as any).janux
22
+ .call(call.name, call.input)
23
+ .catch((error: unknown) => ({ error: String(error) }));
24
+
25
+ if (result?.status === 'proposal') state.proposal = { id: result.id, tool: call.name };
26
+ wire.push({ role: 'tool', toolCallId: call.id, content: JSON.stringify(result ?? null) });
27
+ }
28
+ reply = await postAgent(wire, path);
29
+ }
30
+ wire = reply.messages ?? wire;
31
+ state.messages.push({ role: 'assistant', text: reply.type === 'setup' ? reply.message : reply.text });
32
+ }
33
+
34
+ export const Copilot = component({
35
+ name: 'copilot',
36
+ description: 'Built-in copilot operating this app through the agent bridge.',
37
+
38
+ state: schema({
39
+ open: bool(),
40
+ busy: bool(),
41
+ messages: list({ role: enums(['user', 'assistant']), text: str() }),
42
+ proposal: schema({ id: str(), tool: str() }).nullable(),
43
+ }),
44
+
45
+ intents: {
46
+ toggle: intent({ description: 'Open/close the copilot', run: ({ state }: any) => (state.open = !state.open) }),
47
+ send: intent({
48
+ description: 'Send a message to the copilot',
49
+ input: schema({ text: str().min(1) }),
50
+ run: async ({ state, input }: any) => {
51
+ state.messages.push({ role: 'user', text: input.text });
52
+ state.busy = true;
53
+ wire.push({ role: 'user', content: input.text });
54
+ await converse(state, window.location.pathname).finally(() => (state.busy = false));
55
+ },
56
+ }),
57
+ approve: intent({
58
+ guard: 'forbidden',
59
+ run: async ({ state }: any) => {
60
+ await (window as any).janux.approve(state.proposal.id);
61
+ state.messages.push({ role: 'assistant', text: `Approved ${state.proposal.tool} ✔` });
62
+ state.proposal = null;
63
+ },
64
+ }),
65
+ reject: intent({
66
+ guard: 'forbidden',
67
+ run: ({ state }: any) => {
68
+ (window as any).janux.reject(state.proposal?.id);
69
+ state.proposal = null;
70
+ },
71
+ }),
72
+ },
73
+
74
+ view: ({ state, intents }: any) => (
75
+ <aside class="copilot">
76
+ <button class="copilot-toggle" on={intents.toggle}>
77
+ {state.open ? '×' : '✦ Copilot'}
78
+ </button>
79
+ {state.open ? (
80
+ <div class="copilot-panel">
81
+ <ol class="chat">
82
+ {state.messages.map((message: any, index: number) => (
83
+ <li key={String(index)} class={message.role}>
84
+ {message.text}
85
+ </li>
86
+ ))}
87
+ </ol>
88
+ {state.proposal ? (
89
+ <div class="proposal">
90
+ <p>Run “{state.proposal.tool}”?</p>
91
+ <button on={intents.approve}>Approve</button>
92
+ <button on={intents.reject}>Reject</button>
93
+ </div>
94
+ ) : null}
95
+ <form intent={intents.send}>
96
+ <input name="text" placeholder={state.busy ? 'Thinking…' : 'Try: add a task to buy milk'} />
97
+ <button type="submit">Send</button>
98
+ </form>
99
+ </div>
100
+ ) : null}
101
+ </aside>
102
+ ),
103
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createInstance } from 'janux';
3
+ import { TaskBoard } from './TaskBoard';
4
+
5
+ describe('TaskBoard', () => {
6
+ it('adds, toggles and counts tasks', async () => {
7
+ const board = createInstance(TaskBoard);
8
+
9
+ await board.intents.add!({ title: 'Ship it' });
10
+ await board.intents.add!({ title: 'Write tests' });
11
+ const [first] = board.snapshot().tasks as any[];
12
+
13
+ await board.intents.toggle!({ id: first.id });
14
+ expect((board.snapshot().tasks as any[])[0].done).toBe(true);
15
+ expect(board.derived.remaining).toBe(1);
16
+ });
17
+
18
+ it('rejects empty titles', () => {
19
+ const board = createInstance(TaskBoard);
20
+
21
+ expect(board.intents.add!({ title: '' })).rejects.toThrow(/below min/);
22
+ });
23
+
24
+ it('clearDone is a proposal for agents — nothing happens until approval', async () => {
25
+ let proposal: any;
26
+ const board = createInstance(TaskBoard, { onProposal: (p) => (proposal = p) });
27
+
28
+ await board.intents.add!({ title: 'done thing' });
29
+ await board.intents.toggle!({ id: (board.snapshot().tasks as any[])[0].id });
30
+
31
+ const result: any = await board.intents.clearDone!({}, { origin: 'agent' });
32
+
33
+ expect(result.status).toBe('proposal');
34
+ expect(board.snapshot().tasks).toHaveLength(1);
35
+ await proposal.execute();
36
+ expect(board.snapshot().tasks).toHaveLength(0);
37
+ });
38
+ });
@@ -0,0 +1,125 @@
1
+ import { component, intent, effect, schema, str, bool, int, enums, list } from 'janux';
2
+ import { saveTasks } from '../server/tasks.api';
3
+ import { theme } from '../stores';
4
+
5
+ const FILTERS = ['all', 'active', 'done'] as const;
6
+
7
+ function taskId(): string {
8
+ return `t_${Math.random().toString(36).slice(2, 9)}`;
9
+ }
10
+
11
+ function visibleTasks(tasks: any[], filter: string): any[] {
12
+ if (filter === 'active') return tasks.filter((task) => !task.done);
13
+ if (filter === 'done') return tasks.filter((task) => task.done);
14
+
15
+ return tasks;
16
+ }
17
+
18
+ export const TaskBoard = component({
19
+ name: 'tasks',
20
+ description: 'A task board. Agents can add, toggle, filter and (with approval) clear done tasks.',
21
+
22
+ state: schema({
23
+ tasks: list({ id: str(), title: str(), done: bool() }),
24
+ filter: enums([...FILTERS]),
25
+ }),
26
+
27
+ derived: {
28
+ remaining: (s: any) => s.tasks.filter((task: any) => !task.done).length,
29
+ },
30
+
31
+ effects: {
32
+ persist: effect({
33
+ description: 'Saves tasks to the server after changes settle',
34
+ when: (s: any) => s.tasks,
35
+ debounce: '400ms',
36
+ run: ({ state }: any) => saveTasks({ tasks: state.tasks }).then(() => {}),
37
+ }),
38
+ },
39
+
40
+ emits: { 'tasks.cleared': schema({ count: int() }) },
41
+
42
+ use: { theme },
43
+
44
+ intents: {
45
+ add: intent({
46
+ description: 'Add a task by title',
47
+ input: schema({ title: str().min(1) }),
48
+ run: ({ state, input }: any) => state.tasks.push({ id: taskId(), title: input.title, done: false }),
49
+ }),
50
+ toggle: intent({
51
+ description: 'Toggle a task done/undone by id',
52
+ input: schema({ id: str() }),
53
+ run: ({ state, input }: any) => {
54
+ const task = state.tasks.find((candidate: any) => candidate.id === input.id);
55
+
56
+ if (task) task.done = !task.done;
57
+ },
58
+ }),
59
+ remove: intent({
60
+ description: 'Delete a task by id',
61
+ input: schema({ id: str() }),
62
+ run: ({ state, input }: any) => {
63
+ state.tasks = state.tasks.filter((task: any) => task.id !== input.id);
64
+ },
65
+ }),
66
+ setFilter: intent({
67
+ description: 'Show all, active or done tasks',
68
+ input: schema({ filter: enums([...FILTERS]) }),
69
+ run: ({ state, input }: any) => (state.filter = input.filter),
70
+ }),
71
+ clearDone: intent({
72
+ description: 'Remove every completed task. Destructive — needs approval.',
73
+ guard: 'confirm',
74
+ run: ({ state, emit }: any) => {
75
+ const count = state.tasks.filter((task: any) => task.done).length;
76
+
77
+ state.tasks = state.tasks.filter((task: any) => !task.done);
78
+ emit('tasks.cleared', { count });
79
+ },
80
+ }),
81
+ },
82
+
83
+ view: ({ state, derived, intents }: any) => (
84
+ <section class="board">
85
+ <header>
86
+ <h2>Tasks</h2>
87
+ <span class="count">{derived.remaining} left</span>
88
+ </header>
89
+ <form intent={intents.add}>
90
+ <input name="title" placeholder="What needs doing?" autocomplete="off" />
91
+ <button type="submit">Add</button>
92
+ </form>
93
+ <nav class="filters">
94
+ {FILTERS.map((filter) => (
95
+ <button
96
+ key={filter}
97
+ class={state.filter === filter ? 'on' : undefined}
98
+ on={intents.setFilter}
99
+ data-input={JSON.stringify({ filter })}
100
+ >
101
+ {filter}
102
+ </button>
103
+ ))}
104
+ </nav>
105
+ <ul class="list">
106
+ {visibleTasks(state.tasks, state.filter).map((task: any) => (
107
+ <li key={task.id} class={task.done ? 'done' : undefined}>
108
+ <button class="check" on={intents.toggle} data-input={JSON.stringify({ id: task.id })}>
109
+ {task.done ? '✓' : ''}
110
+ </button>
111
+ <span>{task.title}</span>
112
+ <button class="x" on={intents.remove} data-input={JSON.stringify({ id: task.id })}>
113
+
114
+ </button>
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ <footer>
119
+ <button class="clear" on={intents.clearDone}>
120
+ Clear done
121
+ </button>
122
+ </footer>
123
+ </section>
124
+ ),
125
+ });
@@ -0,0 +1,23 @@
1
+ import { component, intent } from 'janux';
2
+ import { theme } from '../stores';
3
+
4
+ /** Second island sharing the theme store — cross-island state, no props. */
5
+ export const ThemeToggle = component({
6
+ name: 'theme-toggle',
7
+ description: 'Switches the shared theme between dark and light.',
8
+
9
+ use: { theme },
10
+
11
+ intents: {
12
+ toggle: intent({
13
+ description: 'Toggle dark/light mode',
14
+ run: ({ use }: any) => use.theme.intents.toggle({}),
15
+ }),
16
+ },
17
+
18
+ view: ({ use, intents }: any) => (
19
+ <button class="theme-toggle" on={intents.toggle}>
20
+ {use.theme.state.mode === 'dark' ? '☀️ Light' : '🌙 Dark'}
21
+ </button>
22
+ ),
23
+ });
@@ -1,14 +1,29 @@
1
- import { Counter } from '../components/Counter';
1
+ import { loadTasks } from '../server/tasks.api';
2
+ import { TaskBoard } from '../components/TaskBoard';
3
+ import { ThemeToggle } from '../components/ThemeToggle';
4
+ import { Copilot } from '../components/Copilot';
5
+
6
+ export const meta = {
7
+ title: 'Tasks — a Janux app',
8
+ description: 'A task board with two faces: a UI for you, typed tools for your copilot.',
9
+ };
10
+
11
+ export default async function Home() {
12
+ const saved: any = await loadTasks({});
2
13
 
3
- export default function Home() {
4
14
  return (
5
- <main>
6
- <h1>Welcome to Janux</h1>
7
- <p>This heading is static HTML (0 KB JS). The counter below is a resumable island.</p>
8
- <Counter />
9
- <p>
10
- Try the agent surface: <code>curl localhost:3000/_janux/manifest</code>
11
- </p>
12
- </main>
15
+ <div class="app">
16
+ <header class="topbar">
17
+ <span class="brand">✦ Tasks</span>
18
+ <ThemeToggle />
19
+ </header>
20
+ <main>
21
+ <TaskBoard initial={{ tasks: saved.tasks, filter: 'all' }} />
22
+ <p class="hint">
23
+ This board is also an agent surface — <code>curl localhost:3000/_janux/manifest</code>
24
+ </p>
25
+ </main>
26
+ <Copilot persist />
27
+ </div>
13
28
  );
14
29
  }
@@ -0,0 +1,31 @@
1
+ import { api } from '@janux/server';
2
+ import { schema, str, bool, int, list } from 'janux';
3
+
4
+ const TASK_SHAPE = { id: str(), title: str(), done: bool() };
5
+ const saved = new Map<string, unknown[]>();
6
+
7
+ export const loadTasks = api({
8
+ description: 'Load the saved task list',
9
+ output: schema({ tasks: list(TASK_SHAPE) }),
10
+ run: ({ ctx }) => ({ tasks: (saved.get(String(ctx.userId ?? 'anon')) as any[]) ?? [] }),
11
+ });
12
+
13
+ export const saveTasks = api({
14
+ description: 'Persist the task list',
15
+ input: schema({ tasks: list(TASK_SHAPE) }),
16
+ run: ({ input, ctx }) => {
17
+ saved.set(String(ctx.userId ?? 'anon'), input.tasks);
18
+
19
+ return { saved: input.tasks.length };
20
+ },
21
+ });
22
+
23
+ export const taskStats = api({
24
+ description: 'Aggregate stats over the saved tasks',
25
+ output: schema({ total: int(), done: int() }),
26
+ run: ({ ctx }) => {
27
+ const tasks = (saved.get(String(ctx.userId ?? 'anon')) as any[]) ?? [];
28
+
29
+ return { total: tasks.length, done: tasks.filter((task) => task.done).length };
30
+ },
31
+ });
@@ -0,0 +1,17 @@
1
+ import { store, intent, schema, enums } from 'janux';
2
+
3
+ /** Shared theme, projected to agents as store://theme. */
4
+ export const theme = store({
5
+ name: 'theme',
6
+ description: 'UI theme shared by every island.',
7
+ state: schema({ mode: enums(['dark', 'light']).default('dark') }),
8
+ intents: {
9
+ toggle: intent({
10
+ description: 'Switch between dark and light mode',
11
+ run: ({ state }) => {
12
+ state.mode = state.mode === 'dark' ? 'light' : 'dark';
13
+ if (typeof document !== 'undefined') document.documentElement.dataset.theme = state.mode;
14
+ },
15
+ }),
16
+ },
17
+ });
@@ -1,10 +1,24 @@
1
1
  :root {
2
- --ink: #1e1b4b;
2
+ --bg: #0e0d24;
3
+ --card: #1b1938;
4
+ --card-2: #242150;
5
+ --text: #e2e8f0;
6
+ --muted: #94a3b8;
7
+ --violet: #a78bfa;
8
+ --cyan: #22d3ee;
9
+ --border: #312e81;
10
+ --danger: #f87171;
11
+ }
12
+
13
+ :root[data-theme='light'] {
14
+ --bg: #f8fafc;
15
+ --card: #ffffff;
16
+ --card-2: #eef2ff;
17
+ --text: #0f172a;
18
+ --muted: #64748b;
3
19
  --violet: #7c3aed;
4
- --cyan: #06b6d4;
5
- --text: #334155;
20
+ --cyan: #0891b2;
6
21
  --border: #e2e8f0;
7
- --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
8
22
  }
9
23
 
10
24
  * {
@@ -13,208 +27,306 @@
13
27
 
14
28
  body {
15
29
  margin: 0;
16
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, sans-serif;
30
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
31
+ background: var(--bg);
17
32
  color: var(--text);
18
- background: #f8fafc;
19
- -webkit-font-smoothing: antialiased;
33
+ transition: background 0.25s, color 0.25s;
20
34
  }
21
35
 
22
- main {
23
- max-width: 960px;
36
+ .topbar {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ max-width: 560px;
24
41
  margin: 0 auto;
25
- padding: 48px 24px 120px;
42
+ padding: 22px 16px 0;
26
43
  }
27
44
 
28
- main.shop {
29
- display: grid;
30
- grid-template-columns: 1fr 400px;
31
- gap: 28px;
45
+ .brand {
46
+ font-weight: 800;
47
+ font-size: 20px;
48
+ background: linear-gradient(90deg, var(--violet), var(--cyan));
49
+ -webkit-background-clip: text;
50
+ background-clip: text;
51
+ color: transparent;
32
52
  }
33
53
 
34
- main.shop > h1 {
35
- grid-column: 1 / -1;
54
+ .theme-toggle {
55
+ padding: 7px 14px;
56
+ border: 1px solid var(--border);
57
+ border-radius: 999px;
58
+ background: var(--card);
59
+ color: var(--text);
60
+ cursor: pointer;
61
+ font-size: 13px;
36
62
  }
37
63
 
38
- h1 {
39
- margin: 0 0 20px;
40
- font-size: 2.1rem;
41
- font-weight: 800;
42
- letter-spacing: -0.03em;
43
- color: var(--ink);
64
+ main {
65
+ max-width: 560px;
66
+ margin: 24px auto 120px;
67
+ padding: 0 16px;
44
68
  }
45
69
 
46
- h2,
47
- h3 {
48
- color: var(--ink);
70
+ .hint {
71
+ text-align: center;
72
+ color: var(--muted);
73
+ font-size: 13px;
74
+ }
75
+
76
+ .hint code {
77
+ background: var(--card-2);
78
+ padding: 2px 8px;
79
+ border-radius: 6px;
80
+ font-size: 12px;
81
+ }
82
+
83
+ /* ── Board ──────────────────────────────────────────────────────── */
84
+ .board {
85
+ background: var(--card);
86
+ border: 1px solid var(--border);
87
+ border-radius: 18px;
88
+ padding: 22px;
89
+ box-shadow: 0 20px 50px -30px rgba(0, 0, 0, 0.6);
90
+ }
91
+
92
+ .board header {
93
+ display: flex;
94
+ justify-content: space-between;
95
+ align-items: baseline;
96
+ margin-bottom: 14px;
97
+ }
98
+
99
+ .board h2 {
100
+ margin: 0;
101
+ font-size: 1.3rem;
49
102
  letter-spacing: -0.02em;
50
103
  }
51
104
 
52
- a {
53
- color: var(--violet);
54
- font-weight: 600;
105
+ .board .count {
106
+ color: var(--muted);
107
+ font-size: 13px;
55
108
  }
56
109
 
57
- code {
58
- padding: 2px 6px;
59
- border-radius: 6px;
60
- background: #ede9fe;
61
- color: #5b21b6;
62
- font-family: var(--mono);
63
- font-size: 0.88em;
110
+ .board form {
111
+ display: flex;
112
+ gap: 8px;
113
+ }
114
+
115
+ .board input {
116
+ flex: 1;
117
+ padding: 11px 14px;
118
+ border: 1px solid var(--border);
119
+ border-radius: 10px;
120
+ background: var(--bg);
121
+ color: var(--text);
122
+ font-size: 14.5px;
123
+ outline: none;
64
124
  }
65
125
 
66
- button {
67
- padding: 8px 16px;
126
+ .board input:focus {
127
+ border-color: var(--violet);
128
+ }
129
+
130
+ .board button {
68
131
  border: none;
69
- border-radius: 9px;
70
- background: var(--ink);
71
- color: #fff;
72
- font-size: 14px;
73
- font-weight: 600;
132
+ border-radius: 10px;
74
133
  cursor: pointer;
75
- transition: transform 0.12s, opacity 0.12s;
134
+ font-weight: 600;
76
135
  }
77
136
 
78
- button:hover {
79
- opacity: 0.9;
80
- transform: translateY(-1px);
137
+ .board form button {
138
+ padding: 0 18px;
139
+ background: linear-gradient(90deg, #7c3aed, #06b6d4);
140
+ color: #fff;
81
141
  }
82
142
 
83
- /* ── Cart ───────────────────────────────────────────────────────── */
84
- .cart {
85
- padding: 24px;
86
- border: 1px solid var(--border);
87
- border-radius: 16px;
88
- background: #fff;
89
- box-shadow: 0 8px 30px -18px rgba(30, 27, 75, 0.35);
143
+ .filters {
144
+ display: flex;
145
+ gap: 6px;
146
+ margin: 14px 0 6px;
90
147
  }
91
148
 
92
- .cart ul {
93
- margin: 12px 0;
94
- padding: 0;
149
+ .filters button {
150
+ padding: 5px 14px;
151
+ border-radius: 999px;
152
+ background: transparent;
153
+ color: var(--muted);
154
+ border: 1px solid transparent;
155
+ font-size: 13px;
156
+ text-transform: capitalize;
157
+ }
158
+
159
+ .filters button.on {
160
+ background: var(--card-2);
161
+ color: var(--text);
162
+ border-color: var(--border);
163
+ }
164
+
165
+ .list {
95
166
  list-style: none;
167
+ margin: 8px 0;
168
+ padding: 0;
96
169
  }
97
170
 
98
- .cart .catalog li,
99
- .cart .items li {
171
+ .list li {
100
172
  display: flex;
101
173
  align-items: center;
102
- justify-content: space-between;
103
174
  gap: 12px;
104
175
  padding: 10px 4px;
105
176
  border-bottom: 1px solid var(--border);
177
+ }
178
+
179
+ .list li span {
180
+ flex: 1;
106
181
  font-size: 15px;
107
182
  }
108
183
 
109
- .cart .catalog button {
110
- background: linear-gradient(90deg, var(--violet), var(--cyan));
184
+ .list li.done span {
185
+ text-decoration: line-through;
186
+ color: var(--muted);
111
187
  }
112
188
 
113
- .cart .items li {
114
- color: var(--ink);
115
- font-weight: 600;
189
+ .check {
190
+ width: 26px;
191
+ height: 26px;
192
+ border-radius: 50%;
193
+ border: 2px solid var(--violet) !important;
194
+ background: transparent;
195
+ color: var(--cyan);
196
+ font-size: 14px;
197
+ line-height: 1;
116
198
  }
117
199
 
118
- .cart .total {
119
- margin: 16px 0 8px;
120
- font-size: 1.2rem;
121
- font-weight: 800;
122
- color: var(--ink);
200
+ li.done .check {
201
+ background: var(--violet);
202
+ color: #fff;
123
203
  }
124
204
 
125
- .cart .order {
126
- padding: 10px 14px;
127
- border-radius: 10px;
128
- background: #ecfdf5;
129
- color: #047857;
130
- font-weight: 600;
205
+ .x {
206
+ background: transparent;
207
+ color: var(--muted);
208
+ font-size: 14px;
209
+ padding: 4px 8px;
210
+ }
211
+
212
+ .x:hover {
213
+ color: var(--danger);
214
+ }
215
+
216
+ .board footer {
217
+ display: flex;
218
+ justify-content: flex-end;
219
+ margin-top: 10px;
220
+ }
221
+
222
+ .clear {
223
+ background: transparent;
224
+ color: var(--muted);
225
+ font-size: 13px;
226
+ padding: 6px 10px;
227
+ }
228
+
229
+ .clear:hover {
230
+ color: var(--danger);
131
231
  }
132
232
 
133
233
  /* ── Copilot ────────────────────────────────────────────────────── */
134
- .copilot {
234
+ .copilot-toggle {
235
+ position: fixed;
236
+ right: 22px;
237
+ bottom: 22px;
238
+ z-index: 50;
239
+ padding: 12px 20px;
240
+ border: none;
241
+ border-radius: 999px;
242
+ background: linear-gradient(90deg, #7c3aed, #06b6d4);
243
+ color: #fff;
244
+ font-weight: 700;
245
+ cursor: pointer;
246
+ box-shadow: 0 10px 30px -8px rgba(124, 58, 237, 0.6);
247
+ }
248
+
249
+ .copilot-panel {
250
+ position: fixed;
251
+ right: 22px;
252
+ bottom: 78px;
253
+ z-index: 50;
135
254
  display: flex;
136
255
  flex-direction: column;
137
- height: fit-content;
138
- padding: 20px;
256
+ width: 340px;
257
+ max-height: 480px;
139
258
  border: 1px solid var(--border);
140
259
  border-radius: 16px;
141
- background: #fff;
142
- box-shadow: 0 8px 30px -18px rgba(30, 27, 75, 0.35);
143
- }
144
-
145
- .copilot h3 {
146
- margin: 0 0 12px;
260
+ background: var(--card);
261
+ box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.5);
262
+ overflow: hidden;
147
263
  }
148
264
 
149
- .copilot .chat {
150
- margin: 0 0 12px;
151
- padding: 0;
265
+ .copilot-panel .chat {
266
+ flex: 1;
267
+ margin: 0;
268
+ padding: 14px;
152
269
  list-style: none;
153
- max-height: 340px;
154
270
  overflow-y: auto;
271
+ min-height: 100px;
155
272
  }
156
273
 
157
- .copilot .chat li {
274
+ .copilot-panel .chat li {
158
275
  margin-bottom: 8px;
159
276
  padding: 9px 13px;
160
277
  border-radius: 12px;
161
- font-size: 14px;
162
- line-height: 1.5;
278
+ font-size: 13.5px;
163
279
  max-width: 88%;
164
280
  }
165
281
 
166
- .copilot .chat li.user {
282
+ .copilot-panel .chat li.user {
167
283
  margin-left: auto;
168
- background: linear-gradient(90deg, var(--violet), #6d28d9);
284
+ background: #6d28d9;
169
285
  color: #fff;
170
286
  }
171
287
 
172
- .copilot .chat li.assistant {
173
- background: #f1f5f9;
174
- color: var(--ink);
288
+ .copilot-panel .chat li.assistant {
289
+ background: var(--card-2);
175
290
  }
176
291
 
177
- .copilot form {
292
+ .copilot-panel form {
178
293
  display: flex;
179
294
  gap: 8px;
295
+ padding: 10px;
296
+ border-top: 1px solid var(--border);
180
297
  }
181
298
 
182
- .copilot input {
299
+ .copilot-panel input {
183
300
  flex: 1;
184
- padding: 9px 13px;
301
+ padding: 9px 12px;
185
302
  border: 1px solid var(--border);
186
303
  border-radius: 9px;
187
- font-size: 14px;
304
+ background: var(--bg);
305
+ color: var(--text);
306
+ font-size: 13.5px;
188
307
  outline: none;
189
308
  }
190
309
 
191
- .copilot input:focus {
192
- border-color: var(--violet);
193
- box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);
310
+ .copilot-panel button {
311
+ border: none;
312
+ border-radius: 9px;
313
+ padding: 9px 14px;
314
+ background: var(--violet);
315
+ color: #fff;
316
+ font-weight: 600;
317
+ cursor: pointer;
194
318
  }
195
319
 
196
320
  .proposal {
197
- margin: 0 0 12px;
198
- padding: 14px;
199
- border: 1px solid #fde68a;
200
- border-radius: 12px;
201
- background: #fffbeb;
202
- }
203
-
204
- .proposal p {
205
- margin: 0 0 10px;
206
- font-size: 14px;
207
- color: #92400e;
208
- }
209
-
210
- .proposal button + button {
211
- margin-left: 8px;
212
- background: #e2e8f0;
213
- color: var(--ink);
321
+ margin: 0 14px 10px;
322
+ padding: 10px 12px;
323
+ border: 1px solid #fbbf24;
324
+ border-radius: 10px;
325
+ background: rgba(251, 191, 36, 0.1);
326
+ font-size: 13px;
214
327
  }
215
328
 
216
- @media (max-width: 900px) {
217
- main.shop {
218
- grid-template-columns: 1fr;
219
- }
329
+ .proposal button {
330
+ margin-right: 6px;
331
+ margin-top: 6px;
220
332
  }
@@ -1,29 +0,0 @@
1
- import { component, intent, schema, int } from 'janux';
2
-
3
- export const Counter = component({
4
- name: 'counter',
5
- description: 'A counter. Agents can read it (ui://counter) and bump it (counter.inc).',
6
-
7
- state: schema({ n: int() }),
8
-
9
- intents: {
10
- inc: intent({
11
- description: 'Increment the counter',
12
- input: schema({ by: int().default(1) }),
13
- run: ({ state, input }) => (state.n += input.by),
14
- }),
15
- reset: intent({
16
- description: 'Reset the counter to zero',
17
- guard: 'confirm',
18
- run: ({ state }) => (state.n = 0),
19
- }),
20
- },
21
-
22
- view: ({ state, intents }) => (
23
- <section>
24
- <output>{state.n}</output>
25
- <button on={intents.inc}>+1</button>
26
- <button on={intents.reset}>Reset</button>
27
- </section>
28
- ),
29
- });
@@ -1,8 +0,0 @@
1
- import { api } from '@janux/server';
2
- import { schema, str } from 'janux';
3
-
4
- export const greet = api({
5
- description: 'Greet someone by name',
6
- input: schema({ name: str() }),
7
- run: ({ input }) => ({ message: `Hello, ${input.name}! — from a Janux api() tool` }),
8
- });