@fapost/jig-ui 0.1.0-next.8 → 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.
@@ -1,5 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { jigVersion, MIN_JIG_VERSION, supports } from '../jig/client.js';
2
+ import { JigRefusal, JigShapeError, jigVersion, MIN_JIG_VERSION, readSpecs, readStatus, readTasks, supports, watchDir, writtenByReads, } from '../jig/client.js';
3
+ import { addProject, readProjects, removeProject } from '../registry/projects.js';
4
+ import { watchTree } from '../watch/watch.js';
3
5
  import { mainCheckout } from './checkout.js';
4
6
  /** Jig's version in the checkout: `null` when Jig is there but names no version Jig UI can read. */
5
7
  async function version(root) {
@@ -11,7 +13,7 @@ async function version(root) {
11
13
  }
12
14
  }
13
15
  /** The main checkout `path` names, or why it cannot be connected. A Jig below the minimum can. */
14
- export async function connectable(path) {
16
+ async function connectable(path) {
15
17
  const checkout = await mainCheckout(path);
16
18
  if (checkout.kind === 'not-a-repository') {
17
19
  return { code: 'not-a-repository', message: 'This is not a directory inside a git repository.' };
@@ -24,8 +26,53 @@ export async function connectable(path) {
24
26
  }
25
27
  return { root: checkout.root };
26
28
  }
27
- /** Reads a connected project's state; a project below the minimum Jig is not read any further. */
28
- export async function projectState({ id, path }) {
29
+ function count(tasks) {
30
+ return {
31
+ active: tasks.filter((t) => t.status === 'active' && !t.paused).length,
32
+ paused: tasks.filter((t) => t.paused).length,
33
+ ready: tasks.filter((t) => t.status === 'ready' && !t.paused).length,
34
+ };
35
+ }
36
+ /** A live task as a board's card shows it. */
37
+ const card = (t) => ({
38
+ id: t.id,
39
+ class: t.class,
40
+ status: t.status,
41
+ goal: t.goal,
42
+ paused: t.paused,
43
+ started: t.branch !== null,
44
+ gate: t.gate?.state ?? null,
45
+ autopilot: t.autopilot?.state ?? null,
46
+ pullRequest: t.pullRequest,
47
+ });
48
+ /** A live task in full; where its worktree is was never read, and an action Jig refuses is left out. */
49
+ const details = (t, attention) => ({
50
+ ...card(t),
51
+ issue: t.issue,
52
+ branch: t.branch,
53
+ baseBranch: t.baseBranch,
54
+ createdAt: t.createdAt,
55
+ updatedAt: t.updatedAt,
56
+ knowledgeConsolidated: t.knowledgeConsolidated,
57
+ worktree: t.worktree,
58
+ gateBy: t.gate?.by ?? null,
59
+ run: t.autopilot && {
60
+ mode: t.autopilot.mode,
61
+ stage: t.autopilot.stage,
62
+ stageAt: t.autopilot.stageAt,
63
+ stopReason: t.autopilot.stopReason,
64
+ stopAt: t.autopilot.stopAt,
65
+ phase: t.autopilot.phase,
66
+ },
67
+ housekeepingFlags: t.housekeepingFlags,
68
+ actions: t.actions.filter((a) => a.offered).map(({ id, command }) => ({ id, command })),
69
+ attention: attention.filter((a) => a.tasks.includes(t.id)),
70
+ });
71
+ /**
72
+ * Runs `read` on a connected project whose Jig Jig UI reads; a project gone, below the minimum, or
73
+ * refusing the read comes back as its state instead.
74
+ */
75
+ async function reading({ id, path }, read) {
29
76
  if (!existsSync(path))
30
77
  return { id, state: 'unavailable', reason: 'missing' };
31
78
  const v = await version(path);
@@ -33,5 +80,198 @@ export async function projectState({ id, path }) {
33
80
  return { id, state: 'unavailable', reason: 'no-jig' };
34
81
  if (v === null || !supports(v))
35
82
  return { id, state: 'unsupported', jigVersion: v, minimum: MIN_JIG_VERSION };
36
- return { id, state: 'ready', jigVersion: v };
83
+ try {
84
+ return await read(path, v);
85
+ }
86
+ catch (error) {
87
+ if (error instanceof JigShapeError)
88
+ return { id, state: 'incompatible', jigVersion: v };
89
+ // Only the code goes on: Jig's message may name a path.
90
+ return { id, state: 'unreadable', code: error instanceof JigRefusal ? error.code : 'failed' };
91
+ }
92
+ }
93
+ async function readProject(project) {
94
+ const read = await reading(project, async (root, jigVersion) => {
95
+ const [status, list, specs] = await Promise.all([readStatus(root), readTasks(root), readSpecs(root)]);
96
+ return { status, list, specs, jigVersion };
97
+ });
98
+ if (!('list' in read))
99
+ return { view: read, tasks: read, details: [], specs: read, revision: null };
100
+ const { status, list, specs, jigVersion } = read;
101
+ // Jig's live list holds active and ready tasks only.
102
+ const live = list.tasks.filter((t) => t.status === 'active' || t.status === 'ready');
103
+ return {
104
+ view: {
105
+ id: project.id,
106
+ state: 'ready',
107
+ jigVersion,
108
+ counts: count(list.tasks),
109
+ attention: status.attention,
110
+ housekeepingStale: status.housekeepingStale,
111
+ },
112
+ tasks: { state: 'ready', tasks: live.map(card), finished: list.hidden },
113
+ details: live.map((t) => details(t, status.attention)),
114
+ specs: { state: 'ready', specs },
115
+ revision: list.revision,
116
+ };
117
+ }
118
+ /** A read that may have caught Jig mid-write: worth one more try before it is believed. */
119
+ const torn = (read) => read.view.state === 'unreadable' || read.view.state === 'incompatible';
120
+ /**
121
+ * The local adapter: the registry in the config directory, Jig run in each main checkout, and a
122
+ * Watcher on each project's `.ai/` while someone listens. Then reads are served from what the
123
+ * watch last read, and a change is signalled only when a new read differs from it.
124
+ */
125
+ export function localSource(configDir, { rescanMs = 60_000, quietMs, maxWaitMs } = {}) {
126
+ const entries = new Map();
127
+ const listeners = new Set();
128
+ let rescan;
129
+ /** Counts the times following started; a read that finishes after a stop belongs to none. */
130
+ let session = 0;
131
+ const following = () => rescan !== undefined;
132
+ const entryOf = (project) => {
133
+ let entry = entries.get(project.id);
134
+ if (!entry)
135
+ entries.set(project.id, (entry = { project, held: false, dirty: false }));
136
+ return entry;
137
+ };
138
+ /** Keeps what was shown over one torn read; otherwise the read is what the project shows. */
139
+ function settle(entry, read) {
140
+ if (torn(read) && entry.shown && !torn(entry.shown) && !entry.held) {
141
+ entry.held = true;
142
+ return;
143
+ }
144
+ entry.held = false;
145
+ entry.shown = read;
146
+ entry.readIn = following() ? session : undefined;
147
+ }
148
+ /** Reads the project again — one read at a time — and signals when what it shows changed. */
149
+ function refresh(entry) {
150
+ if (entry.reading) {
151
+ entry.dirty = true;
152
+ return entry.reading;
153
+ }
154
+ entry.reading = (async () => {
155
+ do {
156
+ entry.dirty = false;
157
+ const before = JSON.stringify(entry.shown);
158
+ settle(entry, await readProject(entry.project));
159
+ if (following() && entries.get(entry.project.id) === entry && JSON.stringify(entry.shown) !== before) {
160
+ signal({ id: entry.project.id, revision: entry.shown?.revision ?? null });
161
+ }
162
+ } while (entry.dirty);
163
+ })().finally(() => (entry.reading = undefined));
164
+ return entry.reading;
165
+ }
166
+ function signal(change) {
167
+ for (const listener of listeners)
168
+ listener(change);
169
+ }
170
+ /** What the project shows: from the watch while following, read through Jig otherwise. */
171
+ async function shown(project) {
172
+ const entry = entryOf(project);
173
+ if (!(following() && entry.shown && entry.readIn === session))
174
+ await (entry.reading ?? refresh(entry));
175
+ return entry.shown;
176
+ }
177
+ function follow(entry) {
178
+ if (entry.watch?.open)
179
+ return;
180
+ entry.watch = watchTree(watchDir(entry.project.path), {
181
+ ignore: writtenByReads,
182
+ onChange: () => void refresh(entry),
183
+ quietMs,
184
+ maxWaitMs,
185
+ });
186
+ }
187
+ /**
188
+ * Follows every registered project and reads it again; forgets one no longer registered, such as
189
+ * one a read in flight brought back after its disconnect.
190
+ */
191
+ async function sweep() {
192
+ // An unreadable registry is the page's to show, through its own read.
193
+ const projects = await readProjects(configDir).catch(() => undefined);
194
+ if (!projects || !following())
195
+ return;
196
+ const ids = new Set(projects.map((p) => p.id));
197
+ for (const [id, entry] of entries) {
198
+ if (ids.has(id))
199
+ continue;
200
+ entry.watch?.close();
201
+ entries.delete(id);
202
+ }
203
+ for (const project of projects) {
204
+ const entry = entryOf(project);
205
+ follow(entry); // a watcher that failed, or a directory that came back
206
+ void refresh(entry);
207
+ }
208
+ }
209
+ function start() {
210
+ session++;
211
+ rescan = setInterval(() => void sweep(), rescanMs);
212
+ rescan.unref();
213
+ void sweep();
214
+ }
215
+ /** Stops following; what was read while following may be old by the next subscribe, so it goes. */
216
+ function stop() {
217
+ clearInterval(rescan);
218
+ rescan = undefined;
219
+ for (const entry of entries.values()) {
220
+ entry.watch?.close();
221
+ entry.watch = undefined;
222
+ entry.shown = undefined;
223
+ entry.held = false;
224
+ }
225
+ }
226
+ return {
227
+ async workspace() {
228
+ return Promise.all((await readProjects(configDir)).map(async (p) => (await shown(p)).view));
229
+ },
230
+ async connect(path) {
231
+ const found = await connectable(path);
232
+ if ('code' in found)
233
+ return found;
234
+ const { project, created } = await addProject(configDir, found.root);
235
+ const entry = entryOf(project);
236
+ if (following())
237
+ follow(entry);
238
+ await refresh(entry);
239
+ return { project: entry.shown.view, created };
240
+ },
241
+ async tasks(id) {
242
+ const project = (await readProjects(configDir)).find((p) => p.id === id);
243
+ return project && (await shown(project)).tasks;
244
+ },
245
+ async task(id, taskId) {
246
+ const project = (await readProjects(configDir)).find((p) => p.id === id);
247
+ if (!project)
248
+ return undefined;
249
+ const read = await shown(project);
250
+ if (read.tasks.state !== 'ready')
251
+ return read.tasks;
252
+ return { state: 'ready', task: read.details.find((t) => t.id === taskId) ?? null };
253
+ },
254
+ async specs(id) {
255
+ const project = (await readProjects(configDir)).find((p) => p.id === id);
256
+ return project && (await shown(project)).specs;
257
+ },
258
+ async disconnect(id) {
259
+ if (!(await removeProject(configDir, id)))
260
+ return false;
261
+ entries.get(id)?.watch?.close();
262
+ entries.delete(id);
263
+ if (following())
264
+ signal({ id, revision: null });
265
+ return true;
266
+ },
267
+ subscribe(listener) {
268
+ listeners.add(listener);
269
+ if (!following())
270
+ start();
271
+ return () => {
272
+ if (listeners.delete(listener) && listeners.size === 0)
273
+ stop();
274
+ };
275
+ },
276
+ };
37
277
  }
@@ -0,0 +1 @@
1
+ export { RegistryUnreadable } from '../registry/projects.js';
@@ -0,0 +1,56 @@
1
+ import { existsSync, watch } from 'node:fs';
2
+ /**
3
+ * Calls `onChange`, debounced, when anything under `dir` changes. It says only that something
4
+ * changed: an event is never read as data. A system watcher that fails becomes one change.
5
+ */
6
+ export function watchTree(dir, { ignore, onChange, quietMs = 1_000, maxWaitMs = 2_000 }) {
7
+ let quiet;
8
+ let first;
9
+ let watcher;
10
+ const settle = () => {
11
+ clearTimeout(quiet);
12
+ clearTimeout(first);
13
+ quiet = first = undefined;
14
+ };
15
+ const fire = () => {
16
+ settle();
17
+ onChange();
18
+ };
19
+ const close = () => {
20
+ settle();
21
+ watcher?.close();
22
+ watcher = undefined;
23
+ };
24
+ // Node 24.16-24.21 on Linux do not throw for a missing directory (24.0-24.15, 24.22 and macOS do): ask first.
25
+ if (!existsSync(dir))
26
+ return { open: false, close };
27
+ try {
28
+ watcher = watch(dir, { recursive: true }, (_event, file) => {
29
+ // Linux keeps watching a removed directory's inode and never says it is gone: close, so the owner watches it again.
30
+ if (!existsSync(dir)) {
31
+ close();
32
+ onChange();
33
+ return;
34
+ }
35
+ // Some systems name no file; that is still a change.
36
+ if (file !== null && ignore(file.replaceAll('\\', '/')))
37
+ return;
38
+ clearTimeout(quiet);
39
+ quiet = setTimeout(fire, quietMs);
40
+ first ??= setTimeout(fire, maxWaitMs);
41
+ });
42
+ watcher.on('error', () => {
43
+ close();
44
+ onChange();
45
+ });
46
+ }
47
+ catch {
48
+ watcher = undefined; // the directory is not there: the owner's next read says so
49
+ }
50
+ return {
51
+ get open() {
52
+ return watcher !== undefined;
53
+ },
54
+ close,
55
+ };
56
+ }
@@ -0,0 +1 @@
1
+ import{A as e,B as t,C as n,D as r,E as i,F as a,G as o,H as s,I as c,J as l,K as u,L as d,M as f,N as p,O as m,P as h,R as g,S as _,T as v,U as y,V as ee,W as b,X as x,Y as te,_ as ne,a as re,b as ie,c as ae,d as oe,f as S,g as C,h as w,i as T,j as se,k as E,l as D,m as ce,n as O,o as k,p as le,q as A,r as ue,s as de,t as fe,u as pe,v as j,w as M,x as N,y as P,z as F}from"./index-CYfTN3Dj.js";var I=t({__name:`BaseSeparator`,props:{orientation:{type:String,required:!1,default:`horizontal`},decorative:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},setup(e){let t=e,n=[`horizontal`,`vertical`];function r(e){return n.includes(e)}let c=p(()=>r(t.orientation)?t.orientation:`horizontal`),l=p(()=>c.value===`vertical`?t.orientation:void 0),d=p(()=>t.decorative?{role:`none`}:{"aria-orientation":l.value,role:`separator`});return(t,n)=>(y(),a(A(i),s({as:e.as,"as-child":e.asChild,"data-orientation":c.value},d.value),{default:u(()=>[o(t.$slots,`default`)]),_:3},16,[`as`,`as-child`,`data-orientation`]))}}),L=t({__name:`Separator`,props:{orientation:{type:String,required:!1,default:`horizontal`},decorative:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},setup(e){let t=e;return(e,n)=>(y(),a(I,te(ee(t)),{default:u(()=>[o(e.$slots,`default`)]),_:3},16))}}),R=t({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(y(),d(`div`,{"data-slot":`card-content`,class:l(A(r)(`px-4 group-data-[size=sm]/card:px-3`,t.class))},[o(e.$slots,`default`)],2))}}),z=t({__name:`CardDescription`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(y(),d(`div`,{"data-slot":`card-description`,class:l(A(r)(`text-xs/relaxed text-muted-foreground`,t.class))},[o(e.$slots,`default`)],2))}}),B=t({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(y(),d(`div`,{"data-slot":`card-header`,class:l(A(r)(`group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3`,t.class))},[o(e.$slots,`default`)],2))}}),me=t({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e;return(e,n)=>(y(),d(`div`,{"data-slot":`card-title`,class:l(A(r)(`cn-font-heading text-sm font-medium group-data-[size=sm]/card:text-sm`,t.class))},[o(e.$slots,`default`)],2))}}),V=t({__name:`Separator`,props:{orientation:{default:`horizontal`},decorative:{type:Boolean,default:!0},asChild:{type:Boolean},as:{},class:{type:[Boolean,null,String,Object,Array]}},setup(e){let t=e,n=S(t,`class`);return(e,i)=>(y(),a(A(L),s({"data-slot":`separator`},A(n),{class:A(r)(`shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch`,t.class)}),null,16,[`class`]))}}),he=[`aria-label`],ge={class:`flex items-baseline gap-3 border-b bg-muted/50 px-4 py-2`},_e={class:`font-semibold tracking-wider uppercase`},ve={key:0,class:`text-muted-foreground`},ye={class:`grid grid-cols-1 divide-y lg:grid-cols-2 lg:divide-x lg:divide-y-0`},H=t({__name:`CanvasSection`,props:{title:{},note:{}},setup(e){return(t,n)=>(y(),d(`section`,{"aria-label":e.title,class:`border-b`},[h(`h2`,ge,[h(`span`,_e,x(e.title),1),e.note?(y(),d(`span`,ve,x(e.note),1)):c(``,!0)]),h(`div`,ye,[(y(),d(f,null,b([`light`,`dark`],e=>h(`div`,{key:e,class:l([e,`flex flex-col gap-4 bg-background p-4 text-foreground`])},[o(t.$slots,`default`)],2)),64))])],8,he))}}),U=(e,t)=>({id:e,class:`T2`,status:`active`,goal:null,paused:!1,started:!0,gate:null,autopilot:null,pullRequest:null,...t}),W=(e,t)=>({url:`https://github.com/fapost-lab/jig-ui/pull/18`,state:e,ci:t,fetchedAt:`2026-10-18T09:40:00Z`}),G=[U(`design-system`,{class:`T3`,gate:`waiting`,goal:`shadcn-vue on Tailwind v4 with light and dark themes; #/canvas with every component in every variant; the three screens rebuilt on it.`}),U(`registry-schema`,{gate:`changed`,goal:`Version the registry file so a newer Jig UI can read an older one.`}),U(`pause-request`,{class:`T3`,autopilot:`stopped`,goal:`A drop on the paused column writes a sticky request through Jig; the card shows it with its age and a cancel.`}),U(`specs-and-epics`,{autopilot:`on`,goal:`Show each spec with its phases, progress and epic branch beside the board, from Jig's spec list.`}),U(`task-details`,{status:`ready`,pullRequest:W(`open`,`green`),goal:`Open a task's details from its card.`}),U(`refusal-fixture`,{class:`T0`,status:`ready`,pullRequest:W(`open`,`red`),goal:`Replace the refusal text in task-list-states.json with one Jig prints.`}),U(`docs-board`,{class:`T1`,status:`ready`,pullRequest:W(`open`,`pending`),goal:`Describe the five columns on the projects page of the docs.`}),U(`live-updates`,{status:`ready`,pullRequest:W(`merged`,`none`),goal:`Follow connected projects without a reload.`}),U(`no-pr-yet`,{class:`T1`,status:`ready`,goal:`Ready, nothing pushed.`}),U(`jig-init-hook`,{class:`T3`,paused:!0,goal:`Register a project in an installed Jig UI from jig init.`}),U(`file-backlog-task`,{class:null,started:!1,goal:`File a minimal backlog task from the page through jig task new.`}),U(`a-task-with-a-rather-long-identifier-that-wraps`,{goal:null})],be=[{id:`needs-you`,title:`Needs you`,count:3},{id:`in-progress`,title:`In progress`,count:1},{id:`ready`,title:`Ready`,count:5},{id:`backlog`,title:`Backlog`,count:1},{id:`paused`,title:`Paused`,count:1},{id:`unknown`,title:`A column this page does not know`,count:0}],K=[{kind:`gate`,subject:{type:`task`,id:`design-system`},title:`Design waits at its gate`,detail:null,todo:`Review the design and approve it: jig task gate design-system approved`,url:null},{kind:`pull-request-ci-failed`,subject:{type:`task`,id:`refusal-fixture`},title:`CI failed`,detail:`pull request #19`,todo:`Look at the failing check.`,url:`https://github.com/fapost-lab/jig-ui/pull/19`},{kind:`housekeeping`,subject:{type:`project`,id:null},title:`Housekeeping never ran`,detail:null,todo:`Run jig housekeeping.`,url:`https://example.com/docs`}],q=[{id:`approve-gate`,command:`jig task gate design-system approved`},{id:`answer-stop`,command:`jig task autopilot pause-request resume --answer <text>`},{id:`pause`,command:`jig task pause design-system`},{id:`something-new`,command:`jig task something-new design-system`}],J={state:`ready`,issue:null,baseBranch:`main`,createdAt:`2026-10-17`,updatedAt:`2026-10-18`,knowledgeConsolidated:!1,worktree:null,gateBy:null,run:null,housekeepingFlags:[],attention:[]},Y=[{...J,...G[2],gate:`approved`,gateBy:`human`,branch:`task/pause-request`,worktree:{uncommitted:4},issue:`#42`,pullRequest:W(`open`,`red`),run:{mode:`attended`,stage:`implement`,stageAt:`2026-10-18T09:00:00Z`,stopReason:`Which column takes a paused task that already has an open pull request?`,stopAt:`2026-10-18T09:12:00Z`,phase:null},housekeepingFlags:[`needs-consolidation`],attention:[K[0]],actions:q,column:{id:`needs-you`,title:`Needs you`}},{...J,...G[10],branch:null,actions:[{id:`start`,command:`jig task start file-backlog-task`}],column:{id:`backlog`,title:`Backlog`}}],X=(e,t={})=>({id:e,state:`ready`,jigVersion:`0.29.0`,counts:{active:11,paused:1,ready:3},attention:[],housekeepingStale:!1,...t}),Z=[X(`jig-ui`,{attention:K,housekeepingStale:!0}),X(`nextjs-store`,{counts:{active:0,paused:0,ready:0}}),{id:`old-jig`,state:`unsupported`,jigVersion:`0.26.0`,minimum:`0.29.0`},{id:`new-jig`,state:`incompatible`,jigVersion:`1.0.0`},{id:`busy`,state:`unreadable`,code:`locked`},{id:`moved`,state:`unavailable`,reason:`missing`}],Q=[{text:"`design-system` — design system and canvas",checked:!0,task:`design-system`,fog:!1},{text:"`specs-and-epics` — specs and epics (after: project board — `jig spec list`)",checked:!1,task:`specs-and-epics`,fog:!1},{text:"`old-task` — a task no longer live",checked:!1,task:`old-task`,fog:!1},{text:`invoices from orders, no task filed yet`,checked:!1,task:null,fog:!1},{text:"fog: registration by `jig init`",checked:!1,task:null,fog:!0}],xe=[{id:`jig-ui`,title:`Jig UI — a local control center for Jig projects`,missingFiles:[],release:null,epic:null,progress:{done:2,total:6,filed:2,fog:1},phases:[{phase:0,title:`Package and release`,done:1,total:1,filed:0,fog:0,items:[{...Q[0],text:`package and release`,task:null}]},{phase:1,title:`Read-only UI`,done:1,total:5,filed:2,fog:1,items:Q}]},{id:`billing`,title:`Billing in one place`,missingFiles:[],release:`minor`,epic:{branch:`epic/billing`,state:`open`,checkedOut:!1,missing:!1,fetched:!0},progress:{done:0,total:1,filed:0,fog:0},phases:[{phase:null,title:null,done:0,total:1,filed:0,fog:0,items:[Q[3]]}]},{id:`lost-epic`,title:`An epic whose branch is gone`,missingFiles:[],release:`major`,epic:{branch:`epic/lost`,state:`open`,checkedOut:!0,missing:!0,fetched:!1},progress:null,phases:[]},{id:`half-written`,title:null,missingFiles:[`spec.md`,`roadmap.md`],release:null,epic:null,progress:null,phases:[]}],Se={class:`flex gap-4`},Ce={class:`w-24 text-muted-foreground`},$={class:`flex items-center gap-2`},we={class:`flex flex-wrap gap-2`},Te={class:`flex max-w-sm flex-col gap-2`},Ee={class:`flex h-5 items-center gap-2`},De={class:`grid grid-cols-2 gap-2 xl:grid-cols-3`},Oe=t({__name:`CanvasView`,setup(t){let r=[`default`,`secondary`,`outline`,`ghost`,`destructive`,`link`],i=[`xs`,`sm`,`default`,`lg`],o=[`icon-xs`,`icon-sm`,`icon`,`icon-lg`],s=[`default`,`secondary`,`outline`,`ghost`,`destructive`,`link`],c=[`attention`,`good`,`bad`,`quiet`];return(t,p)=>(y(),d(`div`,null,[p[12]||=h(`div`,{class:`border-b px-4 py-3`},[h(`h1`,{class:`font-semibold tracking-wider uppercase`},`Canvas`),h(`p`,{class:`text-muted-foreground`},`Every component of the page in every variant and state; the left panel light, the right dark.`)],-1),F(H,{title:`Tones`,note:`lib/tones.ts — the only colours with a meaning`},{default:u(()=>[(y(),d(f,null,b(c,t=>h(`p`,{key:t,class:l(A(e)[t])},x(t)+` as a rule`,3)),64)),h(`p`,Se,[(y(),d(f,null,b(c,e=>h(`span`,{key:e,class:l(A(se)[e])},x(e),3)),64))])]),_:1}),p[13]||=h(`h2`,{class:`border-b px-4 py-2 font-bold tracking-widest uppercase`},`components/ui`,-1),F(H,{title:`Button`,note:`variant × size; icon sizes; disabled`},{default:u(()=>[(y(),d(f,null,b(r,e=>h(`div`,{key:e,class:`flex flex-wrap items-center gap-2`},[h(`span`,Ce,x(e),1),(y(),d(f,null,b(i,t=>F(A(v),{key:t,variant:e,size:t},{default:u(()=>[g(x(t),1)]),_:2},1032,[`variant`,`size`])),64)),F(A(v),{variant:e,disabled:``},{default:u(()=>[...p[0]||=[g(`disabled`,-1)]]),_:1},8,[`variant`])])),64)),h(`div`,$,[p[2]||=h(`span`,{class:`w-24 text-muted-foreground`},`icon`,-1),(y(),d(f,null,b(o,e=>F(A(v),{key:e,variant:`outline`,size:e,"aria-label":e},{default:u(()=>[F(A(m))]),_:1},8,[`size`,`aria-label`])),64)),F(A(v),{variant:`outline`,size:`sm`},{default:u(()=>[F(A(m),{"data-icon":`inline-start`}),p[1]||=g(`with icon`,-1)]),_:1})])]),_:1}),F(H,{title:`Badge`},{default:u(()=>[h(`div`,we,[(y(),d(f,null,b(s,e=>F(A(oe),{key:e,variant:e},{default:u(()=>[g(x(e),1)]),_:2},1032,[`variant`])),64))])]),_:1}),F(H,{title:`Card`},{default:u(()=>[F(A(pe),{class:`max-w-sm`},{default:u(()=>[F(A(B),null,{default:u(()=>[F(A(me),null,{default:u(()=>[...p[3]||=[g(`Card title`,-1)]]),_:1}),F(A(z),null,{default:u(()=>[...p[4]||=[g(`Card description, muted.`,-1)]]),_:1})]),_:1}),F(A(R),null,{default:u(()=>[...p[5]||=[g(`Card content.`,-1)]]),_:1})]),_:1})]),_:1}),F(H,{title:`Input and Label`,note:`empty, filled, invalid, disabled`},{default:u(()=>[h(`div`,Te,[F(A(ue),null,{default:u(()=>[...p[6]||=[g(`Project path`,-1)]]),_:1}),F(A(T),{placeholder:`/path/to/project`}),F(A(T),{"model-value":`/Users/me/project`}),F(A(T),{"model-value":`/not/a/project`,"aria-invalid":`true`}),F(A(T),{"model-value":`disabled`,disabled:``})])]),_:1}),F(H,{title:`Separator`},{default:u(()=>[F(A(V)),h(`div`,Ee,[p[7]||=g(`left`,-1),F(A(V),{orientation:`vertical`}),p[8]||=g(`right`,-1)])]),_:1}),F(H,{title:`ToggleGroup`,note:`the Board / List switch`},{default:u(()=>[F(A(de),{type:`single`,variant:`outline`,size:`sm`,"model-value":`board`,"aria-label":`View`},{default:u(()=>[F(A(k),{value:`board`},{default:u(()=>[...p[9]||=[g(`Board`,-1)]]),_:1}),F(A(k),{value:`list`},{default:u(()=>[...p[10]||=[g(`List`,-1)]]),_:1})]),_:1})]),_:1}),F(H,{title:`Collapsible`,note:`open and folded`},{default:u(()=>[(y(),d(f,null,b([!0,!1],e=>F(A(_),{key:String(e),"default-open":e},{default:u(()=>[F(A(ie),{class:`w-full border-b bg-muted/50 px-3 py-1.5 text-left`},{default:u(()=>[g(`Trigger (`+x(e?`open`:`folded`)+`)`,1)]),_:2},1024),F(A(N),{class:`px-3 py-2`},{default:u(()=>[...p[11]||=[g(`Content`,-1)]]),_:1})]),_:2},1032,[`default-open`])),64))]),_:1}),p[14]||=h(`h2`,{class:`border-b px-4 py-2 font-bold tracking-widest uppercase`},`components`,-1),F(H,{title:`ProjectTabs`,note:`on the workspace; on a project; empty`},{default:u(()=>[F(E,{projects:A(Z),version:`0.1.0`},{default:u(()=>[F(M)]),_:1},8,[`projects`]),F(E,{projects:A(Z),current:`jig-ui`,version:`0.1.0`},null,8,[`projects`]),F(E,{projects:[]})]),_:1}),F(H,{title:`ThemeToggle`,note:`switches the whole page; the icon is the theme it switches to`},{default:u(()=>[h(`div`,null,[F(M)])]),_:1}),F(H,{title:`ProjectCard`,note:`ready with attention and stale housekeeping; nothing needs you; each state it cannot be read in`},{default:u(()=>[(y(!0),d(f,null,b(A(Z),e=>(y(),a(fe,{key:e.id,project:e},null,8,[`project`]))),128))]),_:1}),F(H,{title:`ConnectForm`,note:`idle, busy, refused`},{default:u(()=>[F(O,{busy:!1}),F(O,{busy:!0,"model-value":`/Users/me/project`}),F(O,{busy:!1,"model-value":`/Users/me/project/.claude/worktrees/x`,refusal:`That is a worktree. Connect the main checkout instead.`})]),_:1}),F(H,{title:`AttentionItem`,note:`no link; a pull request; another link`},{default:u(()=>[(y(!0),d(f,null,b(A(K),(e,t)=>(y(),a(re,{key:t,item:e},null,8,[`item`]))),128))]),_:1}),F(H,{title:`ColumnHeader`,note:`each column, and one this page does not know`},{default:u(()=>[(y(!0),d(f,null,b(A(be),e=>(y(),a(n,{id:e.id,key:e.id,title:e.title,count:e.count},null,8,[`id`,`title`,`count`]))),128))]),_:1}),F(H,{title:`TaskCard`,note:`every note a card carries; open with details, while reading, unreadable`},{default:u(()=>[h(`div`,De,[(y(!0),d(f,null,b(A(G),e=>(y(),a(D,{key:e.id,project:`jig-ui`,task:e,open:!1},null,8,[`task`]))),128)),F(D,{project:`jig-ui`,task:A(G)[2],open:!0,details:A(Y)[0]},null,8,[`task`,`details`]),F(D,{project:`jig-ui`,task:A(G)[0],open:!0},null,8,[`task`]),F(D,{project:`jig-ui`,task:A(G)[1],open:!0,details:`unavailable`},null,8,[`task`])])]),_:1}),F(H,{title:`TaskRow`,note:`each note; selected`},{default:u(()=>[h(`div`,null,[(y(!0),d(f,null,b(A(G),(e,t)=>(y(),a(ae,{key:e.id,task:e,selected:t===2},null,8,[`task`,`selected`]))),128))])]),_:1}),F(H,{title:`TaskFacts`,note:`everything a task can carry; a task not started`},{default:u(()=>[(y(!0),d(f,null,b(A(Y),e=>(y(),a(ce,{key:e.id,details:e,class:`border-b pb-3`},null,8,[`details`]))),128))]),_:1}),F(H,{title:`TaskBrief`,note:`facts, actions and the link to the full page`},{default:u(()=>[F(le,{project:`jig-ui`,task:A(Y)[1].id,details:A(Y)[1]},null,8,[`task`,`details`])]),_:1}),F(H,{title:`ActionList`,note:`Jig's offers, one unknown id; none`},{default:u(()=>[F(w,{actions:A(q)},null,8,[`actions`]),F(w,{actions:[]})]),_:1}),F(H,{title:`CopyCommand`,note:`short; long enough to be cut`},{default:u(()=>[F(C,{command:`jig task pause design-system`}),F(C,{command:`jig task autopilot a-task-with-a-rather-long-identifier-that-wraps resume --answer 'a long answer to the question'`,class:`max-w-xs`})]),_:1}),F(H,{title:`ProgressBar`,note:`none, part, all, an empty roadmap`},{default:u(()=>[(y(),d(f,null,b([[0,4],[3,9],[5,5],[0,0]],([e,t])=>F(P,{key:`${e}/${t}`,done:e,total:t},null,8,[`done`,`total`])),64))]),_:1}),F(H,{title:`SpecItemRow`,note:`done with a live task; open with dependencies; a task no longer live; no task; fog`},{default:u(()=>[h(`ul`,null,[(y(!0),d(f,null,b(A(Q),(e,t)=>(y(),a(j,{key:t,item:e,project:`jig-ui`,live:t<2},null,8,[`item`,`live`]))),128))])]),_:1}),F(H,{title:`SpecCard`,note:`no epic, a finished phase folded; an open epic as of the last fetch; a lost epic branch; missing files`},{default:u(()=>[(y(!0),d(f,null,b(A(xe),e=>(y(),a(ne,{key:e.id,spec:e,project:`jig-ui`,live:[`design-system`,`specs-and-epics`]},null,8,[`spec`]))),128))]),_:1})]))}});export{Oe as default};