@imfusion/web-ui 0.10.1-dev.60.g7891a716 → 0.10.1-dev.63.gc947eab3
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/CHANGELOG.md +220 -46
- package/dist/web-ui-cli.js +148 -134
- package/docs/user-guide/AgentTooling.mdx +4 -4
- package/package.json +1 -1
- package/src/llms/install-templates/hooks/session-start.sh +14 -0
- package/src/llms/skills/imf-web-ui-audit/SKILL.md +4 -4
- package/src/llms/skills/imf-web-ui-conventions/topics/agent-tooling.md +8 -9
- package/src/llms/skills/imf-web-ui-conventions/topics/git.md +3 -3
- package/src/llms/skills/imf-web-ui-update/SKILL.md +5 -5
- package/src/llms/install-templates/hooks/baseline-staleness.sh +0 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## `0.11.0` 2026.10.20
|
|
4
4
|
|
|
5
5
|
### Highlights
|
|
6
6
|
|
|
7
|
+
#### A navbar that carries the brand and the account
|
|
8
|
+
|
|
9
|
+
`AppShell.Navbar` lays out its rail itself. It takes `logo` and `account`: the logo row sits on top, on the baseline of
|
|
10
|
+
Main's first heading, the nav items stack below it, and the account card fills the foot in line with the items. Leave out
|
|
11
|
+
`logo` for the ImFusion logo in the variant that fits the rail. The current `NavItem` is underlined while the others dim.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<AppShell.Navbar account={<AccountCard.Root>…</AccountCard.Root>}>
|
|
15
|
+
<AppShell.NavItem href="/projects" aria-current="page">
|
|
16
|
+
Projects
|
|
17
|
+
</AppShell.NavItem>
|
|
18
|
+
</AppShell.Navbar>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Below `md` the shell draws its own bar with the navbar's logo and the drawer toggle, so apps render no burger. The header
|
|
22
|
+
shows at every width and follows the navbar's `variant`. The aside has a close button in its drawer, both drawers keep
|
|
23
|
+
their own state through `defaultOpen`, and Main and both drawers scroll in the new `ScrollArea`. One gutter,
|
|
24
|
+
`--imf-ui-appshell-padding`, lines up the rail, the header, Main, and the aside.
|
|
25
|
+
[AppShell](src/components/app-shell/app-shell.mdx) ([WEBSDK-247](https://imfusion.atlassian.net/browse/WEBSDK-247))
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+
|
|
7
30
|
#### Paper Mono is the mono font
|
|
8
31
|
|
|
9
32
|
[Paper Mono](https://paper.design/mono) replaces CoFo Sans Mono as the default `--imf-ui-font-family-mono`. It is licensed
|
|
@@ -25,6 +48,188 @@ never changes the character width, so columns stay aligned. CoFo Sans Mono still
|
|
|
25
48
|

|
|
26
49
|

|
|
27
50
|
|
|
51
|
+
#### Skills that notice when they fall behind
|
|
52
|
+
|
|
53
|
+
The `SessionStart` hook compares the installed `imf-web-ui` skills with the copy in `node_modules/@imfusion/web-ui`. When
|
|
54
|
+
they differ, the agent hears about it and offers `/imf-web-ui-update`. The installed skills carry no version marker, so a
|
|
55
|
+
package update that leaves the skills alone touches only `package.json` and the lockfile.
|
|
56
|
+
|
|
57
|
+
#### New and updated components
|
|
58
|
+
|
|
59
|
+
- [`Dialog`](src/components/dialog/dialog.mdx) is new: a modal window over a dimmed page, with every Base UI part.
|
|
60
|
+
`Dialog.Simple` builds one from props, `Dialog.Inline` opens one from a term in running text, and `Dialog.Status` adds a
|
|
61
|
+
tinted status column. The body scrolls while the header and footer stay in view
|
|
62
|
+
([WEBSDK-247](https://imfusion.atlassian.net/browse/WEBSDK-247)).
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
- [`ScrollArea`](src/components/scroll-area/scroll-area.mdx) is new. Its slim primary thumb floats over the content, and an
|
|
68
|
+
edge fades where more content waits behind it. `ScrollArea.Auto` covers the usual case.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+

|
|
72
|
+
|
|
73
|
+
- [`ColorSchemeToggle`](src/components/color-scheme-toggle/color-scheme-toggle.mdx) is new. It lets the user pick Light,
|
|
74
|
+
System, or Dark, stores the choice through `useColorScheme`, and needs no props.
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
- [`ProductLogo`](src/components/logo/product/product.stories.tsx#Products) is new. It renders the ImFusion Labels, Suite,
|
|
80
|
+
and SDK logos on the `Logo` sizes and colors; `cutout` cuts the name out of the plate.
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
- [`AccountCard`](src/components/account-card/account-card.mdx) is a compact row: the avatar in a lead column, the text
|
|
86
|
+
beside it, organization and role under the subtitle. `withBreakHints` breaks a long email at its `@` and dots.
|
|
87
|
+
|
|
88
|
+

|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
- [`Select`](src/components/select/select.mdx) items take a muted second line through `Select.ItemDescription`, such as
|
|
92
|
+
what a role allows. `Select.Root` takes a `width` of `auto`, `wide`, or `full`, and the popup sits 4px off its trigger.
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
- [`Table`](src/components/table/table.stories.tsx#Density) reads lighter: muted mono labels over a 1px rule, no row
|
|
98
|
+
rules, and a neutral selected row. `Table.Root` takes a `density`, and `action` shrinks a column of buttons to its
|
|
99
|
+
content.
|
|
100
|
+
|
|
101
|
+

|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
- [`ToggleGroup`](src/components/toggle-group/toggle-group.mdx) and `Toggle` take a `size` of `sm` or `md`. A small group
|
|
105
|
+
is a tight row of 1.5rem toggles with a 1px border.
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+

|
|
109
|
+
|
|
110
|
+
- [`Separator`](src/components/separator/separator.stories.tsx#All) takes `tone="minor"` for a neutral line, a quiet
|
|
111
|
+
break inside a page.
|
|
112
|
+
|
|
113
|
+

|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
- [`Typo.UnorderedList`](src/components/typo/typo.mdx) marks its items with a short dash, primary on the main, support,
|
|
117
|
+
and minor variants ([WEBSDK-270](https://imfusion.atlassian.net/browse/WEBSDK-270)).
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+

|
|
121
|
+
|
|
122
|
+
#### Important fixes
|
|
123
|
+
|
|
124
|
+
- `Typo.Hero`, `Typo.H3`, `Typo.H4`, `Typo.Lead`, and `Typo.Blockquote` keep letter pairs such as "rt" and "ty" apart:
|
|
125
|
+
their tracking scales with the viewport.
|
|
126
|
+
- An empty `Select.ItemIndicator` draws the `Check` icon in the item's text color. Base UI fell back to a ✔️ emoji.
|
|
127
|
+
- The docked aside ends where `AppShell.Main` begins at every width; at 1024px it covered up to 13px of Main.
|
|
128
|
+
- `Chip` stays on one line and ends in an ellipsis when its label doesn't fit.
|
|
129
|
+
- `imf-web-ui-setup` and `imf-web-ui-audit` keep tsconfig `paths` away from Vite. `vite-tsconfig-paths` loaded the React
|
|
130
|
+
types package as React and broke builds that install Web UI from the registry.
|
|
131
|
+
|
|
132
|
+
#### Migrating from 0.10
|
|
133
|
+
|
|
134
|
+
Pass the brand and the account card to `AppShell.Navbar` and drop the Sections that held them. A card passed as `account`
|
|
135
|
+
lines up with the nav items without the 8px inset:
|
|
136
|
+
|
|
137
|
+
```diff
|
|
138
|
+
- <AppShell.Navbar>
|
|
139
|
+
- <AppShell.Section><MyLogo /></AppShell.Section>
|
|
140
|
+
+ <AppShell.Navbar logo={<MyLogo />} account={<AccountCard.Root>…</AccountCard.Root>}>
|
|
141
|
+
<AppShell.NavItem href="/projects">Projects</AppShell.NavItem>
|
|
142
|
+
- <AppShell.Section><AccountCard.Root>…</AccountCard.Root></AppShell.Section>
|
|
143
|
+
</AppShell.Navbar>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Remove the burger and your own logo from `AppShell.Header`; `AppShell.Burger` is no longer exported, and phones would
|
|
147
|
+
show two logos:
|
|
148
|
+
|
|
149
|
+
```diff
|
|
150
|
+
<AppShell.Header>
|
|
151
|
+
- <AppShell.Burger />
|
|
152
|
+
- <MyLogo />
|
|
153
|
+
<UserMenu />
|
|
154
|
+
</AppShell.Header>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Remove a header color that matched a branded navbar; the header takes the navbar's `variant`. A custom surface goes in
|
|
158
|
+
`--imf-ui-appshell-header-bg`:
|
|
159
|
+
|
|
160
|
+
```diff
|
|
161
|
+
- .header { background: var(--imf-ui-color-bg-primary); }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The header wraps its children in a row aligned to the inline end. Give your own row full width to span it:
|
|
165
|
+
|
|
166
|
+
```diff
|
|
167
|
+
.headerRow {
|
|
168
|
+
+ width: 100%;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Restyle the current nav item through the dim and underline properties; it has no background surface:
|
|
173
|
+
|
|
174
|
+
```diff
|
|
175
|
+
- .nav { --navitem-current: var(--imf-ui-color-bg-support); }
|
|
176
|
+
+ .nav { --imf-ui-appshell-navitem-dim: 60%; }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`--imf-ui-appshell-padding` now runs from 1.25rem to 3rem, the navbar is one gutter wider, and the aside pads on every
|
|
180
|
+
side. Set the properties on `AppShell.Root` to keep the old aside padding, and re-check pages tuned to the old values:
|
|
181
|
+
|
|
182
|
+
```diff
|
|
183
|
+
+ --imf-ui-appshell-aside-padding: var(--imf-ui-size-6) var(--imf-ui-appshell-padding);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`AppShell.Main`, the aside drawer, and `Drawer.Content` pad their content inside a `ScrollArea`. A `padding` on Main or
|
|
187
|
+
the aside adds to the gutter, so remove it. On `Drawer.Content`, set the padding through its property; a `padding`,
|
|
188
|
+
`gap`, or grid set on the part does not reach the children:
|
|
189
|
+
|
|
190
|
+
```diff
|
|
191
|
+
- .main { padding: var(--imf-ui-size-6); }
|
|
192
|
+
- .drawerContent { padding: 1rem; }
|
|
193
|
+
+ .drawerContent { --imf-ui-drawer-content-padding: 1rem; }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The navbar drawer opens from the inline start. Move CSS placed against its right edge to the left edge:
|
|
197
|
+
|
|
198
|
+
```diff
|
|
199
|
+
- inset-inline-end: 0;
|
|
200
|
+
+ inset-inline-start: 0;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`AccountCard` is a row, not a link. Move the Link's children into `AccountCard.Root` and put navigation on a button in
|
|
204
|
+
`AccountCard.Action`. Pass `size="md"` to keep the larger avatar. An account switcher on `AccountCard.Group` has no
|
|
205
|
+
replacement yet, so stay on 0.10 for it:
|
|
206
|
+
|
|
207
|
+
```diff
|
|
208
|
+
<AccountCard.Root>
|
|
209
|
+
- <AccountCard.Link href="/account">
|
|
210
|
+
- <AccountCard.Avatar>NB</AccountCard.Avatar>
|
|
211
|
+
+ <AccountCard.Avatar size="md">NB</AccountCard.Avatar>
|
|
212
|
+
<AccountCard.Title>Nik</AccountCard.Title>
|
|
213
|
+
- </AccountCard.Link>
|
|
214
|
+
+ <AccountCard.Action>
|
|
215
|
+
+ <IconButton variant="ghost" size="sm" aria-label="Account settings">…</IconButton>
|
|
216
|
+
+ </AccountCard.Action>
|
|
217
|
+
</AccountCard.Root>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Remove the staleness script from your pre-commit hook, then refresh the agent tooling. Keep `.agents/skills/imf-web-ui*`
|
|
221
|
+
out of your formatter, or the reformatted skills read as stale:
|
|
222
|
+
|
|
223
|
+
```diff
|
|
224
|
+
node_modules/.bin/vp staged
|
|
225
|
+
- .agents/hooks/imf-web-ui/baseline-staleness.sh
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npx web-ui install skills
|
|
230
|
+
npx web-ui install hooks
|
|
231
|
+
```
|
|
232
|
+
|
|
28
233
|
### Breaking
|
|
29
234
|
|
|
30
235
|
- **Breaking:** `AppShell.Main`, the AppShell aside drawer, and `Drawer.Content` scroll in a `ScrollArea` and pad their
|
|
@@ -57,64 +262,41 @@ never changes the character width, so columns stay aligned. CoFo Sans Mono still
|
|
|
57
262
|
- Remove a header color set to match the branded navbar. A custom surface still goes in `--imf-ui-appshell-header-bg`, which is unset by default and falls back to the navbar variant's surface.
|
|
58
263
|
- **Breaking:** `AppShell.Header` is a flex row, and it wraps its children in a container that aligns them to the inline end. [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
59
264
|
- Give your own row `width: 100%` to span the header.
|
|
265
|
+
- **Breaking:** `npx web-ui install hooks` removes `baseline-staleness.sh`, and the `SessionStart` hook reports installed
|
|
266
|
+
skills that differ from the package copy in `node_modules` instead. — Nikolaus Brunner <brunner@imfusion.com>
|
|
267
|
+
- Remove the `.agents/hooks/imf-web-ui/baseline-staleness.sh` line from the project's pre-commit hook before running
|
|
268
|
+
`install hooks`; a pre-commit that calls the missing script fails every commit.
|
|
269
|
+
- Keep `.agents/skills/imf-web-ui*` out of the project's formatter, or the reformatted skills read as stale.
|
|
60
270
|
|
|
61
271
|
### Added
|
|
62
272
|
|
|
273
|
+
- [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
274
|
+
- The shell's navigation toggle sets `aria-controls` from the navbar, and reads the `id` the navbar generates. A consumer `id` on the Navbar is what it points at.
|
|
275
|
+
- `AppShell.Navbar` and `AppShell.Aside` take `defaultOpen` and keep their own open state when `open` is omitted, and close an uncontrolled drawer when they dock. A controlled `open` is never changed by docking.
|
|
276
|
+
- An `AppShell.Aside` opens from the consumer's state, such as a row selection, through `open` and `onOpenChange`.
|
|
277
|
+
- `AppShell.Main` content is a flex column at least as tall as Main, so a child with `flex: 1` fills the rest.
|
|
278
|
+
- `AppShell.Aside` content wraps long words such as emails and IDs inside a narrow panel, in the docked aside and the drawer.
|
|
279
|
+
- An `AppShell.NavItem` followed inside the navbar drawer closes it, unless its `onClick` calls `event.preventDefault()`.
|
|
280
|
+
- `--imf-ui-appshell-heading-*`, `--imf-ui-appshell-navitem-dim`, `--imf-ui-appshell-navitem-underline-thickness`, and `--imf-ui-appshell-navitem-underline-offset` tune the logo row's strut and the NavItem underline.
|
|
281
|
+
- `AppShell.Aside` shows a close button in its drawer below `lg`, labelled by `closeLabel` (`Close panel` by default).
|
|
63
282
|
- `--imf-ui-size-backdrop-edge` (`38%`) and `--imf-ui-size-backdrop-band-height` (`30dvh`) are the public backdrop's geometry tokens. The `imf-web-ui-ux` skill describes the handover. — Nikolaus Brunner <brunner@imfusion.com>
|
|
64
|
-
- `ProductLogo` renders the ImFusion Labels, Suite, and SDK product logos on the `Logo` sizes and color variants. The name is solid by default and cut out of the plate with `cutout`. [Product](src/components/logo/product/product.stories.tsx#Default) — Nikolaus Brunner <brunner@imfusion.com>
|
|
65
283
|
- `imf-web-ui-update` proposes and `imf-web-ui-audit` reports aligning the core runtime packages (React, Base UI, Vite+) with the versions Web UI builds with. — Nikolaus Brunner <brunner@imfusion.com>
|
|
66
284
|
- `localWebUILink()` and a `paths` block in `tsconfig.json` let an app run against a symlinked web-ui checkout with one React and one set of types. `imf-web-ui-setup` proposes both and `imf-web-ui-audit` reports a missing one. — Nikolaus Brunner <brunner@imfusion.com>
|
|
67
|
-
- The shell's navigation toggle sets `aria-controls` from the navbar, and reads the `id` the navbar generates. A consumer `id` on the Navbar is what it points at. — Nikolaus Brunner <brunner@imfusion.com>
|
|
68
|
-
- `Separator` takes `tone="minor"` for a neutral line, a quiet break inside a page; the default stays primary. — Nikolaus Brunner <brunner@imfusion.com>
|
|
69
|
-
- `Select.ItemDescription` sets a muted second line under an item's text, such as what a role allows; the trigger shows only the item text, and the list keeps the trigger's width so descriptions wrap. — Nikolaus Brunner <brunner@imfusion.com>
|
|
70
|
-
- `Table.Root` takes `density` (`compact`, `comfortable`, `spacious`) for the cell padding. — Nikolaus Brunner <brunner@imfusion.com>
|
|
71
|
-
- `Table.HeaderCell` and `Table.Cell` take `action` for a column of buttons: it shrinks to its content, so the data columns keep the width. — Nikolaus Brunner <brunner@imfusion.com>
|
|
72
285
|
- [`useDisclosure`](src/hooks/use-disclosure.stories.tsx#UseDisclosure) keeps an open state that works controlled or uncontrolled, and returns `open`, `setOpen`, and `toggle`. — Nikolaus Brunner <brunner@imfusion.com>
|
|
73
|
-
- `AppShell.Navbar` and `AppShell.Aside` take `defaultOpen` and keep their own open state when `open` is omitted, and close an uncontrolled drawer when they dock. A controlled `open` is never changed by docking. — Nikolaus Brunner <brunner@imfusion.com>
|
|
74
|
-
- An `AppShell.Aside` opens from the consumer's state, such as a row selection, through `open` and `onOpenChange`. — Nikolaus Brunner <brunner@imfusion.com>
|
|
75
|
-
- `AppShell.Main` content is a flex column at least as tall as Main, so a child with `flex: 1` fills the rest. — Nikolaus Brunner <brunner@imfusion.com>
|
|
76
|
-
- `AppShell.Aside` content wraps long words such as emails and IDs inside a narrow panel, in the docked aside and the drawer. — Nikolaus Brunner <brunner@imfusion.com>
|
|
77
|
-
- An `AppShell.NavItem` followed inside the navbar drawer closes it, unless its `onClick` calls `event.preventDefault()`. — Nikolaus Brunner <brunner@imfusion.com>
|
|
78
|
-
- [`ColorSchemeToggle`](src/components/color-scheme-toggle/color-scheme-toggle.mdx) lets the user pick Light, System, or
|
|
79
|
-
Dark. It stores the choice through `useColorScheme` and needs no props; `label`, `lightLabel`, `systemLabel`, and
|
|
80
|
-
`darkLabel` replace the English text — Nikolaus Brunner <brunner@imfusion.com>
|
|
81
|
-
- [`ToggleGroup`](src/components/toggle-group/toggle-group.mdx) and `Toggle` take a `size` of `sm` or `md`. A small group
|
|
82
|
-
has no inset or gap, a 1px border, and 1.5rem toggles; a toggle takes its group's size unless it sets its own
|
|
83
|
-
— Nikolaus Brunner <brunner@imfusion.com>
|
|
84
286
|
- `Skeleton.Suspense` takes a `fallback` of bones, a `label`, and the content. It is a Suspense boundary whose fallback is a
|
|
85
287
|
busy `Skeleton.Root`; when the content mounts after the fallback showed, it fades in, and content that was
|
|
86
288
|
ready at mount appears without a fade. Use it for data loaded through Suspense, and `Skeleton.Root` for data loaded with
|
|
87
289
|
`useQuery` — Nikolaus Brunner <brunner@imfusion.com>
|
|
88
|
-
- `ScrollArea` is a scrolling region with a slim primary-colored thumb that floats over the content and shows while the
|
|
89
|
-
area is hovered or scrolled, and a fade at each edge with more content behind it. `ScrollArea.Auto` wraps children with
|
|
90
|
-
the scrollbars for `orientation` (`vertical`, `horizontal`, `both`, or `none`); the parts compose by hand for anything else
|
|
91
|
-
([WEBSDK-247](https://imfusion.atlassian.net/browse/WEBSDK-247)) — Nikolaus Brunner <brunner@imfusion.com>
|
|
92
|
-
- `Dialog` is a modal window over a dimmed page, adapted from Base UI with all its parts. `Dialog.Header` takes a
|
|
93
|
-
`variant` (`main`, `support`, `minor`, or `primary` with light text); `Dialog.Title` renders as `Typo.H2` and
|
|
94
|
-
`Dialog.Description` as `Typo.P`. `Dialog.Body` scrolls in a `ScrollArea` while header and footer stay in view.
|
|
95
|
-
`Dialog.Popup` comes in `sm`, `md`, `lg`, and `full`, and stops at 40rem high (`--imf-ui-dialog-max-height`) except in
|
|
96
|
-
`full`. `Dialog.Status` adds a tinted status column like `Card.Status`, on top of the width the size sets.
|
|
97
|
-
`Dialog.Simple` builds the whole dialog from props, and `Dialog.Inline` opens it from a term inside running text. Both
|
|
98
|
-
show the × close button only without `actions`; with them, the footer is the way out
|
|
99
|
-
([WEBSDK-247](https://imfusion.atlassian.net/browse/WEBSDK-247)) — Nikolaus Brunner <brunner@imfusion.com>
|
|
100
290
|
- Storybook — Nikolaus Brunner <brunner@imfusion.com>
|
|
101
291
|
- The token drawer offers Paper Mono as a mono font.
|
|
102
292
|
- Docs links open a single story when they name its export after the stories file:
|
|
103
293
|
`[Inline](src/components/code/code.stories.tsx#Inline)`.
|
|
104
|
-
- `withBreakHints` is exported. It inserts a break before each `@` and after each `.` in an email's string children, so the address wraps at its seams. [Account card](src/components/account-card/account-card.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
105
294
|
- `--imf-ui-accountcard-lead` and `--imf-ui-accountcard-padding-inline` set an `AccountCard`'s avatar column and inline padding, so a container can line it up with its own rows. [Account card](src/components/account-card/account-card.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
106
|
-
- `AppShell.Navbar` takes `logo` and `account`; the logo defaults to the ImFusion logo and lines up with the baseline of Main's first heading. [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
107
|
-
- `--imf-ui-appshell-heading-*`, `--imf-ui-appshell-navitem-dim`, `--imf-ui-appshell-navitem-underline-thickness`, and `--imf-ui-appshell-navitem-underline-offset` tune the logo row's strut and the NavItem underline. [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
108
|
-
- `AppShell.Aside` shows a close button in its drawer below `lg`, labelled by `closeLabel` (`Close panel` by default). [AppShell](src/components/app-shell/app-shell.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
109
|
-
- `Select.Root` takes a `width` of `auto`, `wide`, or `full`: `auto` keeps the 10rem trigger minimum, `wide` raises it to
|
|
110
|
-
15rem, and `full` fills the parent. `--imf-ui-select-trigger-min-width` on `Select.Root` sets the minimum from CSS.
|
|
111
|
-
[Select](src/components/select/select.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
112
295
|
|
|
113
296
|
### Changed
|
|
114
297
|
|
|
115
298
|
- The `project-structure` topic sets when a single-route part is inlined into its route file: a component under 50 lines, with no `use*` hook, no stylesheet or test of its own, leaving the route file under 250 lines. `imf-web-ui-audit` reports a breach as a Deviation. — Nikolaus Brunner <brunner@imfusion.com>
|
|
116
299
|
- The `Meter` track is translucent ink (8% `fg-main`), so it stays visible on tinted surfaces such as a selected table row. — Nikolaus Brunner <brunner@imfusion.com>
|
|
117
|
-
- `Table` reads lighter: the header has no band, its mono labels are muted with a 1px rule under them, body rows have no rules, and cells get more vertical padding. A selected row sits one neutral step darker than a hovered row, in place of the primary tint. — Nikolaus Brunner <brunner@imfusion.com>
|
|
118
300
|
- [`AppShell.Header`](src/components/app-shell/app-shell.mdx) is a page row that shows at every width, below the shell's bar on mobile. — Nikolaus Brunner <brunner@imfusion.com>
|
|
119
301
|
- The header pads its inline sides with `--imf-ui-appshell-padding`, so its content lines up with Main's.
|
|
120
302
|
- AppShell — Nikolaus Brunner <brunner@imfusion.com>
|
|
@@ -153,20 +335,12 @@ never changes the character width, so columns stay aligned. CoFo Sans Mono still
|
|
|
153
335
|
fades.
|
|
154
336
|
- Removed default padding from `Popover.Trigger` such that it keeps a neutral style.
|
|
155
337
|
- A ghost `Button` or `IconButton` hovers by fading its label or glyph, with no fill or edge, on main and oncolor surfaces alike, instead of inverting to a solid fill. `Dialog.Close` and the `AppShell.Aside` drawer close follow. [Button](src/components/button/button.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
156
|
-
- `Typo.UnorderedList` marks its items with a short primary-coloured dash on the main, support, and minor variants, and with a dash in the text colour on the others. It sets `role="list"` by default. [Typo](src/components/typo/typo.mdx) ([WEBSDK-270](https://imfusion.atlassian.net/browse/WEBSDK-270)) — Nikolaus Brunner <brunner@imfusion.com>
|
|
157
338
|
|
|
158
339
|
### Fixed
|
|
159
340
|
|
|
160
|
-
- `Typo.Hero`, `Typo.H3`, `Typo.H4`, `Typo.Lead`, and `Typo.Blockquote` no longer crowd their letters: `--imf-ui-font-letterspacing-hero` and `--imf-ui-font-letterspacing-brand` scale with the viewport, from about -0.0225em and -0.01em at 360px to -0.025em and -0.02em at 1920px, so pairs like "rt" and "ty" stay apart — Nikolaus Brunner <brunner@imfusion.com>
|
|
161
|
-
- The `Select` popup sits 4px below its trigger by default (`sideOffset` on `Select.Positioner`) and casts a hard shadow (`--imf-ui-shadow-4`). — Nikolaus Brunner <brunner@imfusion.com>
|
|
162
|
-
- An empty `Select.ItemIndicator` draws the `Check` icon in the item's text colour, so it stays visible on the highlighted item. Base UI's fallback was a `✔️` emoji in its own colour. — Nikolaus Brunner <brunner@imfusion.com>
|
|
163
|
-
- The `tooling` topic, `imf-web-ui-setup` and `imf-web-ui-audit` keep tsconfig `paths` away from Vite: `resolve.tsconfigPaths` or `vite-tsconfig-paths` loaded the React types package as React and broke a build that installs Web UI from the registry. The `#/` alias comes from `imports` in `package.json`. — Nikolaus Brunner <brunner@imfusion.com>
|
|
164
|
-
- The skill staleness hook stays quiet when `@imfusion/web-ui` is a symlinked checkout, whose version never matches the installed skills. — Nikolaus Brunner <brunner@imfusion.com>
|
|
165
341
|
- `Drawer.Popup` draws no focus ring around the whole panel when it takes focus on open; the controls inside keep theirs. — Nikolaus Brunner <brunner@imfusion.com>
|
|
166
|
-
- `Chip` stays on one line within its container's width and ends in an ellipsis when the label doesn't fit. [Chip](src/components/chip/chip.mdx) — Nikolaus Brunner <brunner@imfusion.com>
|
|
167
342
|
- Only the navbar drawer's own close shows while it is open; the header burger steps back until the drawer closes — Nikolaus Brunner <brunner@imfusion.com>
|
|
168
343
|
- `Skeleton.Root` fades only its own content in when loading ends; the rest of the page stays still, and no `AbortError` reaches the console — Nikolaus Brunner <brunner@imfusion.com>
|
|
169
|
-
- `AppShell`'s docked aside no longer leaves `AppShell.Main` up to 13px under it at 1024px: Main ends where the aside begins at every docked width — Nikolaus Brunner <brunner@imfusion.com>
|
|
170
344
|
- The Stop hook's verify gate blocks only a turn that edited files. An edit from an earlier turn no longer blocks later turns that only answer questions while the tree stays dirty — Nikolaus Brunner <brunner@imfusion.com>
|
|
171
345
|
- `imf-web-ui-ux` describes patterns and points to `imf-web-ui-components` for which components exist, instead of claiming which ones do not — Nikolaus Brunner <brunner@imfusion.com>
|
|
172
346
|
|
package/dist/web-ui-cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as e, r as t, t as n } from "./paths-B5Ht3ETc.js";
|
|
3
3
|
import { chmodSync as r, cpSync as i, existsSync as a, lstatSync as o, mkdirSync as s, readFileSync as c, readdirSync as l, readlinkSync as u, realpathSync as d, rmSync as f, symlinkSync as p, writeFileSync as m } from "node:fs";
|
|
4
|
-
import { dirname as
|
|
5
|
-
import { Command as
|
|
6
|
-
import * as
|
|
7
|
-
var
|
|
4
|
+
import { dirname as ee, relative as h, resolve as g } from "node:path";
|
|
5
|
+
import { Command as te, Option as ne } from "commander";
|
|
6
|
+
import * as _ from "@clack/prompts";
|
|
7
|
+
var v = {
|
|
8
8
|
harnesses: {
|
|
9
9
|
claude: {
|
|
10
10
|
label: "Claude Code",
|
|
@@ -22,162 +22,160 @@ var y = {
|
|
|
22
22
|
templatesDir: "src/llms/install-templates",
|
|
23
23
|
skillsSource: "src/llms/skills",
|
|
24
24
|
skillsDir: ".agents/skills",
|
|
25
|
-
linkDir: ".claude/skills"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return t === -1 || n === -1 ? null : e.slice(t, n + b.length);
|
|
25
|
+
linkDir: ".claude/skills"
|
|
26
|
+
}, re = `<!-- ${v.namespace}:begin`, y = `<!-- ${v.namespace}:end -->`, ie = g(e, v.templatesDir, "AGENTS.md"), b = g(t, "AGENTS.md");
|
|
27
|
+
function x(e) {
|
|
28
|
+
let t = e.indexOf(re), n = e.indexOf(y);
|
|
29
|
+
return t === -1 || n === -1 ? null : e.slice(t, n + y.length);
|
|
31
30
|
}
|
|
32
|
-
function
|
|
33
|
-
if (!a(
|
|
34
|
-
let e =
|
|
31
|
+
function S() {
|
|
32
|
+
if (!a(b)) return "absent";
|
|
33
|
+
let e = x(c(ie, "utf-8"));
|
|
35
34
|
if (!e) return "absent";
|
|
36
|
-
let t = c(
|
|
37
|
-
return r === t ? "unchanged" : (m(
|
|
35
|
+
let t = c(b, "utf-8"), n = x(t), r = n ? t.replace(n, e) : `${t.trimEnd()}\n\n${e}\n`;
|
|
36
|
+
return r === t ? "unchanged" : (m(b, r), n ? "updated" : "added");
|
|
38
37
|
}
|
|
39
38
|
//#endregion
|
|
40
39
|
//#region src/cli/skills.ts
|
|
41
|
-
var C = Object.keys(
|
|
42
|
-
function
|
|
40
|
+
var C = Object.keys(v.harnesses).filter((e) => e in v.harnesses), ae = [...C, "all"];
|
|
41
|
+
function w(e) {
|
|
43
42
|
return e === "all" ? C : [e];
|
|
44
43
|
}
|
|
45
|
-
function
|
|
46
|
-
return `./${
|
|
44
|
+
function T(e) {
|
|
45
|
+
return `./${h(t, e)}`;
|
|
47
46
|
}
|
|
48
|
-
var
|
|
49
|
-
if (typeof
|
|
50
|
-
var
|
|
51
|
-
function
|
|
52
|
-
return e ===
|
|
47
|
+
var E = v.namespace, D = ".imf-web-ui-skill-version.json", O = g(e, v.skillsSource), k = g(t, v.skillsDir), A = g(t, v.linkDir), j = class extends Error {}, M = JSON.parse(c(g(e, "package.json"), "utf-8"));
|
|
48
|
+
if (typeof M != "object" || !M || !("version" in M) || typeof M.version != "string") throw Error(`${g(e, "package.json")} has no string "version" field`);
|
|
49
|
+
var oe = M.version;
|
|
50
|
+
function N(e) {
|
|
51
|
+
return e === E || e.startsWith(`${E}-`);
|
|
53
52
|
}
|
|
54
|
-
function
|
|
55
|
-
return a(
|
|
53
|
+
function P() {
|
|
54
|
+
return a(O) ? l(O, { withFileTypes: !0 }).filter((e) => e.isDirectory() && N(e.name)).map((e) => e.name).sort() : [];
|
|
56
55
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
if (!a(t)) return null;
|
|
56
|
+
function se(e) {
|
|
57
|
+
if (!a(e)) return null;
|
|
60
58
|
try {
|
|
61
|
-
let
|
|
62
|
-
if (typeof
|
|
63
|
-
let n = "harnesses" in
|
|
59
|
+
let t = JSON.parse(c(e, "utf-8"));
|
|
60
|
+
if (typeof t != "object" || !t) return null;
|
|
61
|
+
let n = "harnesses" in t && Array.isArray(t.harnesses) ? t.harnesses : null;
|
|
64
62
|
return n ? C.filter((e) => n.includes(e)) : null;
|
|
65
63
|
} catch {
|
|
66
64
|
return null;
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
|
-
function
|
|
67
|
+
function ce(e) {
|
|
70
68
|
for (let t of e) {
|
|
71
|
-
let e =
|
|
69
|
+
let e = se(g(k, t, D));
|
|
72
70
|
if (e && e.length > 0) return e;
|
|
73
71
|
}
|
|
74
72
|
return null;
|
|
75
73
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
74
|
+
function F(e) {
|
|
75
|
+
return e.some((e) => a(g(k, e)));
|
|
76
|
+
}
|
|
77
|
+
function I(e) {
|
|
78
|
+
return R() ? F(e) : e.some((e) => o(g(A, e), { throwIfNoEntry: !1 })?.isSymbolicLink() === !0);
|
|
79
|
+
}
|
|
80
|
+
function L(e, t, n) {
|
|
81
|
+
if (a(e)) for (let r of l(e).filter(N)) t.has(r) || (f(g(e, r), {
|
|
78
82
|
recursive: !0,
|
|
79
83
|
force: !0
|
|
80
|
-
}),
|
|
84
|
+
}), _.log.info(`Removed skill: ${r} (${n})`));
|
|
81
85
|
}
|
|
82
|
-
function
|
|
83
|
-
return a(
|
|
86
|
+
function R() {
|
|
87
|
+
return a(A) && a(k) && d(A) === d(k);
|
|
84
88
|
}
|
|
85
|
-
function
|
|
86
|
-
let t =
|
|
89
|
+
function le(e) {
|
|
90
|
+
let t = g(A, e), n = h(A, g(k, e)), r = o(t, { throwIfNoEntry: !1 });
|
|
87
91
|
r?.isSymbolicLink() && u(t) === n || (r && f(t, {
|
|
88
92
|
recursive: !0,
|
|
89
93
|
force: !0
|
|
90
|
-
}), s(
|
|
94
|
+
}), s(A, { recursive: !0 }), p(n, t));
|
|
91
95
|
}
|
|
92
|
-
function
|
|
96
|
+
function ue(e, t) {
|
|
93
97
|
try {
|
|
94
|
-
s(
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
let e = _(A, n);
|
|
98
|
+
s(k, { recursive: !0 });
|
|
99
|
+
for (let t of e) {
|
|
100
|
+
let e = g(k, t);
|
|
98
101
|
f(e, {
|
|
99
102
|
recursive: !0,
|
|
100
103
|
force: !0
|
|
101
|
-
}), i(
|
|
102
|
-
let a = _(e, O);
|
|
103
|
-
m(a, JSON.stringify({
|
|
104
|
-
version: P,
|
|
105
|
-
harnesses: t
|
|
106
|
-
}, null, 2) + "\n"), r.push(a);
|
|
104
|
+
}), i(g(O, t), e, { recursive: !0 });
|
|
107
105
|
}
|
|
108
|
-
if (
|
|
106
|
+
if (L(k, new Set(e), "no longer in this bundle"), _.log.success(`Skills -> ${T(k)}/${E}*`), R()) return;
|
|
109
107
|
if (t.includes("claude")) {
|
|
110
|
-
for (let t of e)
|
|
111
|
-
|
|
112
|
-
} else
|
|
108
|
+
for (let t of e) le(t);
|
|
109
|
+
L(A, new Set(e), "no longer in this bundle"), _.log.success(`Claude Code -> ${T(A)}/${E}* (symlinks -> ${T(k)})`);
|
|
110
|
+
} else L(A, /* @__PURE__ */ new Set(), "claude not selected");
|
|
113
111
|
} catch (e) {
|
|
114
|
-
throw e instanceof Error ? new
|
|
112
|
+
throw e instanceof Error ? new j(e.message) : e;
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
115
|
//#endregion
|
|
118
116
|
//#region src/cli/hooks.ts
|
|
119
|
-
var
|
|
120
|
-
function
|
|
121
|
-
if (typeof e != "object" || !e || Array.isArray(e)) throw new
|
|
117
|
+
var z = g(e, v.templatesDir), B = g(z, "hooks"), V = g(t, v.hooksDir), H = class extends Error {};
|
|
118
|
+
function de(e, t) {
|
|
119
|
+
if (typeof e != "object" || !e || Array.isArray(e)) throw new H(`${t} is not a JSON object.`);
|
|
122
120
|
let n = { ...e };
|
|
123
121
|
if ("hooks" in n) {
|
|
124
|
-
if (typeof n.hooks != "object" || n.hooks === null || Array.isArray(n.hooks)) throw new
|
|
125
|
-
for (let [e, r] of Object.entries(n.hooks)) if (!Array.isArray(r)) throw new
|
|
122
|
+
if (typeof n.hooks != "object" || n.hooks === null || Array.isArray(n.hooks)) throw new H(`${t}: "hooks" is not a JSON object.`);
|
|
123
|
+
for (let [e, r] of Object.entries(n.hooks)) if (!Array.isArray(r)) throw new H(`${t}: "hooks.${e}" is not a JSON array.`);
|
|
126
124
|
}
|
|
127
125
|
return n;
|
|
128
126
|
}
|
|
129
|
-
function
|
|
127
|
+
function U(e) {
|
|
130
128
|
let t;
|
|
131
129
|
try {
|
|
132
130
|
t = JSON.parse(c(e, "utf-8"));
|
|
133
131
|
} catch {
|
|
134
|
-
throw new
|
|
132
|
+
throw new H(`${e} is not valid JSON.`);
|
|
135
133
|
}
|
|
136
|
-
return
|
|
134
|
+
return de(t, e);
|
|
137
135
|
}
|
|
138
|
-
function
|
|
139
|
-
let e = l(
|
|
140
|
-
s(
|
|
136
|
+
function fe() {
|
|
137
|
+
let e = l(B).filter((e) => e.endsWith(".sh"));
|
|
138
|
+
s(V, { recursive: !0 });
|
|
141
139
|
for (let t of e) {
|
|
142
|
-
let e =
|
|
143
|
-
i(
|
|
140
|
+
let e = g(V, t);
|
|
141
|
+
i(g(B, t), e, { force: !0 }), r(e, 493);
|
|
144
142
|
}
|
|
145
|
-
let t = l(
|
|
146
|
-
for (let e of t) f(
|
|
143
|
+
let t = l(V).filter((t) => t.endsWith(".sh") && !e.includes(t));
|
|
144
|
+
for (let e of t) f(g(V, e), { force: !0 });
|
|
147
145
|
return {
|
|
148
146
|
installed: e,
|
|
149
147
|
removed: t
|
|
150
148
|
};
|
|
151
149
|
}
|
|
152
|
-
var
|
|
153
|
-
function
|
|
150
|
+
var W = `${v.hooksDir}/`;
|
|
151
|
+
function G(e) {
|
|
154
152
|
if (typeof e != "string") return null;
|
|
155
|
-
let t = e.lastIndexOf(
|
|
153
|
+
let t = e.lastIndexOf(W);
|
|
156
154
|
if (t === -1) return null;
|
|
157
|
-
let n = /^([\w.-]+)["']?$/.exec(e.slice(t +
|
|
155
|
+
let n = /^([\w.-]+)["']?$/.exec(e.slice(t + W.length));
|
|
158
156
|
return n ? n[1] : null;
|
|
159
157
|
}
|
|
160
|
-
function
|
|
158
|
+
function K(e) {
|
|
161
159
|
return typeof e == "object" && !!e && "hooks" in e && Array.isArray(e.hooks);
|
|
162
160
|
}
|
|
163
|
-
function
|
|
164
|
-
return
|
|
161
|
+
function q(e) {
|
|
162
|
+
return K(e) ? e.hooks : null;
|
|
165
163
|
}
|
|
166
|
-
function
|
|
164
|
+
function J(e) {
|
|
167
165
|
return typeof e == "object" && e && "command" in e ? e.command : void 0;
|
|
168
166
|
}
|
|
169
|
-
function
|
|
170
|
-
let t =
|
|
171
|
-
return t ? t.map((e) =>
|
|
167
|
+
function Y(e) {
|
|
168
|
+
let t = q(e);
|
|
169
|
+
return t ? t.map((e) => G(J(e))).filter((e) => e !== null) : [];
|
|
172
170
|
}
|
|
173
|
-
function
|
|
171
|
+
function pe(e, t, n) {
|
|
174
172
|
e.hooks ??= {};
|
|
175
173
|
let r = 0, i = 0, a = [];
|
|
176
174
|
for (let [r, o] of Object.entries(e.hooks)) {
|
|
177
175
|
let s = 0, c = o.map((e) => {
|
|
178
|
-
if (!
|
|
176
|
+
if (!K(e)) return e;
|
|
179
177
|
let t = e.hooks, r = t.filter((e) => {
|
|
180
|
-
let t =
|
|
178
|
+
let t = G(J(e));
|
|
181
179
|
return t === null || n.includes(t);
|
|
182
180
|
});
|
|
183
181
|
return s += t.length - r.length, r.length === t.length ? e : {
|
|
@@ -185,7 +183,7 @@ function ue(e, t, n) {
|
|
|
185
183
|
hooks: r
|
|
186
184
|
};
|
|
187
185
|
}).filter((e) => {
|
|
188
|
-
let t =
|
|
186
|
+
let t = q(e);
|
|
189
187
|
return t === null || t.length > 0;
|
|
190
188
|
});
|
|
191
189
|
s !== 0 && (i += s, c.length === 0 && !(r in t) ? a.push(r) : e.hooks[r] = c);
|
|
@@ -194,8 +192,8 @@ function ue(e, t, n) {
|
|
|
194
192
|
for (let [n, i] of Object.entries(t)) {
|
|
195
193
|
e.hooks[n] ??= [];
|
|
196
194
|
for (let t of i) {
|
|
197
|
-
let i =
|
|
198
|
-
e.hooks[n].some((e) =>
|
|
195
|
+
let i = Y(t);
|
|
196
|
+
e.hooks[n].some((e) => Y(e).some((e) => i.includes(e))) || (e.hooks[n].push(t), r++);
|
|
199
197
|
}
|
|
200
198
|
}
|
|
201
199
|
return {
|
|
@@ -203,99 +201,115 @@ function ue(e, t, n) {
|
|
|
203
201
|
removed: i
|
|
204
202
|
};
|
|
205
203
|
}
|
|
206
|
-
function
|
|
204
|
+
function me(e) {
|
|
205
|
+
let n = g(t, v.harnesses[e].hookSettings);
|
|
206
|
+
if (!a(n)) return !1;
|
|
207
|
+
try {
|
|
208
|
+
return Object.values(U(n).hooks ?? {}).flat().some((e) => Y(e).length > 0);
|
|
209
|
+
} catch {
|
|
210
|
+
return !1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function he(e) {
|
|
207
214
|
let r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map();
|
|
208
215
|
for (let n of C) {
|
|
209
|
-
let o =
|
|
210
|
-
if (a(o) && r.set(n,
|
|
211
|
-
let e =
|
|
212
|
-
i.set(n,
|
|
216
|
+
let o = g(t, v.harnesses[n].hookSettings);
|
|
217
|
+
if (a(o) && r.set(n, U(o)), e.includes(n)) {
|
|
218
|
+
let e = g(z, v.harnesses[n].hookTemplate);
|
|
219
|
+
i.set(n, U(e).hooks ?? {});
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
let { installed: o, removed: c } =
|
|
216
|
-
|
|
222
|
+
let { installed: o, removed: c } = fe();
|
|
223
|
+
_.log.success(`Agent hooks -> ${T(V)} (${String(o.length)} scripts)`), c.length > 0 && _.log.info(`Removed stale hook script(s): ${c.join(", ")} (no longer shipped)`);
|
|
217
224
|
for (let a of C) {
|
|
218
225
|
let c = e.includes(a);
|
|
219
226
|
if (!c && !r.has(a)) continue;
|
|
220
|
-
let l =
|
|
227
|
+
let l = g(t, v.harnesses[a].hookSettings), u = i.get(a) ?? {}, d = r.get(a) ?? {}, f = pe(d, u, c ? o : []);
|
|
221
228
|
if (f.added === 0 && f.removed === 0) {
|
|
222
|
-
c &&
|
|
229
|
+
c && _.log.info(`Hook registrations already present in ${T(l)}`);
|
|
223
230
|
continue;
|
|
224
231
|
}
|
|
225
|
-
s(
|
|
232
|
+
s(ee(l), { recursive: !0 }), m(l, JSON.stringify(d, null, 2) + "\n"), n([l]);
|
|
226
233
|
let p = [f.added > 0 && `registered ${String(f.added)}`, f.removed > 0 && `removed ${String(f.removed)} stale`].filter(Boolean);
|
|
227
|
-
|
|
234
|
+
_.log.success(`${p.join(", ")} hook registration(s) in ${T(l)}`);
|
|
228
235
|
}
|
|
229
236
|
}
|
|
230
237
|
//#endregion
|
|
231
238
|
//#region src/cli/index.ts
|
|
239
|
+
function X(e) {
|
|
240
|
+
let t = ce(e);
|
|
241
|
+
if (t) return t;
|
|
242
|
+
if (!F(e)) return null;
|
|
243
|
+
let n = C.filter((t) => me(t) || t === "claude" && I(e));
|
|
244
|
+
return n.length > 0 ? n : ["codex"];
|
|
245
|
+
}
|
|
232
246
|
function Z() {
|
|
233
|
-
return new
|
|
247
|
+
return new ne("--harness <harness>", "claude, codex, or all").choices(ae);
|
|
234
248
|
}
|
|
235
|
-
async function
|
|
236
|
-
if (t) return
|
|
237
|
-
let r =
|
|
238
|
-
if (r && !n) return
|
|
249
|
+
async function ge(e, t, n) {
|
|
250
|
+
if (t) return _.log.info(`Harness from --harness flag: ${t.join(", ")}`), t;
|
|
251
|
+
let r = X(e);
|
|
252
|
+
if (r && !n) return _.log.info(`Refreshing the existing install: ${r.join(", ")} — pass --reconfigure to choose differently.`), r;
|
|
239
253
|
if (!process.stdin.isTTY) return console.error("Pass --harness=claude|codex|all — no terminal to prompt in."), process.exitCode = 1, null;
|
|
240
|
-
let i = await
|
|
254
|
+
let i = await _.multiselect({
|
|
241
255
|
message: "Install into which harness (or harnesses)?",
|
|
242
256
|
options: C.map((e) => ({
|
|
243
257
|
value: e,
|
|
244
|
-
label:
|
|
258
|
+
label: v.harnesses[e].label
|
|
245
259
|
})),
|
|
246
260
|
required: !0
|
|
247
261
|
});
|
|
248
|
-
return
|
|
262
|
+
return _.isCancel(i) ? (_.cancel("Cancelled — nothing installed."), null) : i;
|
|
249
263
|
}
|
|
250
|
-
async function
|
|
251
|
-
let t = e.harness ?
|
|
252
|
-
if (
|
|
253
|
-
|
|
264
|
+
async function _e(e) {
|
|
265
|
+
let t = e.harness ? w(e.harness) : null, n = P();
|
|
266
|
+
if (_.intro(`@imfusion/web-ui install skills — ${String(n.length)} skills`), n.length === 0) {
|
|
267
|
+
_.log.error("No skills found. Reinstall @imfusion/web-ui and try again."), _.outro("Nothing installed."), process.exitCode = 1;
|
|
254
268
|
return;
|
|
255
269
|
}
|
|
256
|
-
let r = await
|
|
270
|
+
let r = await ge(n, t, e.reconfigure === !0);
|
|
257
271
|
if (!r) return;
|
|
258
|
-
|
|
272
|
+
_.log.message(`Skills in this bundle:\n${n.map((e) => ` - ${e}`).join("\n")}`);
|
|
259
273
|
try {
|
|
260
|
-
|
|
274
|
+
ue(n, r);
|
|
261
275
|
} catch (e) {
|
|
262
|
-
if (e instanceof
|
|
263
|
-
|
|
276
|
+
if (e instanceof j) {
|
|
277
|
+
_.log.error(e.message), _.outro("Install failed."), process.exitCode = 1;
|
|
264
278
|
return;
|
|
265
279
|
}
|
|
266
280
|
throw e;
|
|
267
281
|
}
|
|
268
|
-
let i =
|
|
269
|
-
(i === "updated" || i === "added") &&
|
|
282
|
+
let i = S();
|
|
283
|
+
(i === "updated" || i === "added") && _.log.success(`Refreshed the ${v.namespace} block in ./AGENTS.md`), _.outro(`Done — ${String(n.length)} skills installed.`);
|
|
270
284
|
}
|
|
271
|
-
function
|
|
272
|
-
let t = e.harness ?
|
|
273
|
-
if (!t && !
|
|
274
|
-
console.error("No harness
|
|
285
|
+
function ve(e) {
|
|
286
|
+
let t = e.harness ? w(e.harness) : null, n = P(), r = X(n);
|
|
287
|
+
if (!t && !r) {
|
|
288
|
+
console.error("No harness detected. Pass --harness=claude|codex|all."), process.exitCode = 1;
|
|
275
289
|
return;
|
|
276
290
|
}
|
|
277
|
-
let
|
|
278
|
-
if (
|
|
279
|
-
for (let e of
|
|
291
|
+
let i = t ?? r ?? [], a = i.filter((e) => e === "claude" ? !I(n) : !F(n));
|
|
292
|
+
if (a.length > 0) {
|
|
293
|
+
for (let e of a) console.error(`No skills installed for ${e}. Run \`npx web-ui install skills --harness=${e}\` first.`);
|
|
280
294
|
process.exitCode = 1;
|
|
281
295
|
} else {
|
|
282
|
-
|
|
296
|
+
_.intro("@imfusion/web-ui install hooks");
|
|
283
297
|
try {
|
|
284
|
-
|
|
298
|
+
he(i);
|
|
285
299
|
} catch (e) {
|
|
286
|
-
if (e instanceof
|
|
300
|
+
if (e instanceof H) {
|
|
287
301
|
console.error(e.message), process.exitCode = 1;
|
|
288
302
|
return;
|
|
289
303
|
}
|
|
290
304
|
throw e;
|
|
291
305
|
}
|
|
292
|
-
|
|
306
|
+
_.outro("Done — agent hooks installed.");
|
|
293
307
|
}
|
|
294
308
|
}
|
|
295
|
-
var Q = new
|
|
296
|
-
Q.name("web-ui").description("Command-line tools for @imfusion/web-ui").version(
|
|
309
|
+
var Q = new te();
|
|
310
|
+
Q.name("web-ui").description("Command-line tools for @imfusion/web-ui").version(oe);
|
|
297
311
|
var $ = Q.command("install").description("Install @imfusion/web-ui agent skills or hooks into a project");
|
|
298
|
-
$.command("skills").description("Install skills for the chosen harness").addOption(Z()).option("--reconfigure", "Re-open the harness selection prompt on an existing install").action(
|
|
312
|
+
$.command("skills").description("Install skills for the chosen harness").addOption(Z()).option("--reconfigure", "Re-open the harness selection prompt on an existing install").action(_e), $.command("hooks").description("Install agent lifecycle hooks (requires skills already installed for the harness)").addOption(Z()).action(ve), $.action(() => {
|
|
299
313
|
$.help();
|
|
300
314
|
}), Q.action(() => {
|
|
301
315
|
Q.help();
|
|
@@ -40,8 +40,8 @@ npx web-ui install hooks --harness=all
|
|
|
40
40
|
| `codex` | Codex, and any agent that reads `.agents/skills/` |
|
|
41
41
|
| `all` | Both |
|
|
42
42
|
|
|
43
|
-
Without `--harness`, `install skills` prompts
|
|
44
|
-
|
|
43
|
+
Without `--harness`, `install skills` prompts on a first install. Later runs read the harness from the installed skill links
|
|
44
|
+
and hook registrations. `install hooks` needs the skills installed first. Leave out the second command to install only the skills.
|
|
45
45
|
|
|
46
46
|
`npx web-ui --help` lists the commands, and `--help` on any subcommand lists its options.
|
|
47
47
|
|
|
@@ -79,7 +79,7 @@ The hooks put the agent on the right skill at the right moment and ask it to ver
|
|
|
79
79
|
|
|
80
80
|
| Event | What the hook does |
|
|
81
81
|
| --- | --- |
|
|
82
|
-
| `SessionStart` | Points the agent at the router once per session. |
|
|
82
|
+
| `SessionStart` | Points the agent at the router once per session, and tells it when the installed skills differ from the package. |
|
|
83
83
|
| `SubagentStart` | Gives the same pointer to spawned agents. |
|
|
84
84
|
| `UserPromptSubmit` | Names the companion skills relevant to the prompt. |
|
|
85
85
|
| `Stop` | Asks for project verification after a turn edits source files. |
|
|
@@ -97,7 +97,7 @@ npx web-ui install skills
|
|
|
97
97
|
npx web-ui install hooks
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Both
|
|
100
|
+
Both read the harness from the installed files. Leave out the second command if the project skips hooks. Together they bring the installed
|
|
101
101
|
tooling in line with the package version and remove anything the package no longer ships.
|
|
102
102
|
|
|
103
103
|
## Reference
|
package/package.json
CHANGED
|
@@ -2,4 +2,18 @@
|
|
|
2
2
|
# SessionStart: inject the web-ui router pointer once per session. Fixed string — the
|
|
3
3
|
# hook-injection eval asserts on it verbatim.
|
|
4
4
|
echo "This project depends on @imfusion/web-ui. Start at the imf-web-ui skill: it decides whether guidance is needed and routes to the right companion. Repo docs carry only what is unique to this repo."
|
|
5
|
+
|
|
6
|
+
# Installed skills that differ from the package's copy are stale. A symlinked checkout
|
|
7
|
+
# carries work-in-progress skills, so it never counts.
|
|
8
|
+
ROOT=$(cd "$(dirname "$0")/../../.." && pwd) || exit 0
|
|
9
|
+
PACKAGE="$ROOT/node_modules/@imfusion/web-ui"
|
|
10
|
+
[ -d "$PACKAGE/src/llms/skills" ] && [ ! -L "$PACKAGE" ] || exit 0
|
|
11
|
+
for dir in "$PACKAGE"/src/llms/skills/imf-web-ui* "$ROOT"/.agents/skills/imf-web-ui*; do
|
|
12
|
+
[ -d "$dir" ] || continue
|
|
13
|
+
name=$(basename "$dir")
|
|
14
|
+
if ! diff -rq "$PACKAGE/src/llms/skills/$name" "$ROOT/.agents/skills/$name" >/dev/null 2>&1; then
|
|
15
|
+
echo "The installed imf-web-ui skills differ from the @imfusion/web-ui package in node_modules. Tell the user and offer the imf-web-ui-update skill, or \`npx web-ui install skills\`."
|
|
16
|
+
break
|
|
17
|
+
fi
|
|
18
|
+
done
|
|
5
19
|
exit 0
|
|
@@ -96,10 +96,10 @@ UI component or a hard-coded color waiting for a token, is evidence for that top
|
|
|
96
96
|
## Agent tooling checks
|
|
97
97
|
|
|
98
98
|
For `agent-tooling`, compare the declared version in `package.json`, the installed
|
|
99
|
-
`node_modules/@imfusion/web-ui/package.json`,
|
|
100
|
-
scripts, and both host registrations with the shipped topic. Version drift, extra hooks,
|
|
101
|
-
registrations are findings; point version drift to `imf-web-ui-update`.
|
|
102
|
-
text; do not run installers or hooks.
|
|
99
|
+
`node_modules/@imfusion/web-ui/package.json`, the installed skills against `node_modules/@imfusion/web-ui/src/llms/skills`,
|
|
100
|
+
the `AGENTS.md` fence, hook scripts, and both host registrations with the shipped topic. Version drift, extra hooks, skills
|
|
101
|
+
that differ from the package copy, and missing registrations are findings; point version drift to `imf-web-ui-update`.
|
|
102
|
+
Registry availability is unverified. Read files as text; do not run installers or hooks.
|
|
103
103
|
|
|
104
104
|
## Safety
|
|
105
105
|
|
|
@@ -12,12 +12,13 @@ npx web-ui install skills --harness=claude
|
|
|
12
12
|
|
|
13
13
|
It installs or refreshes the `imf-web-ui-*` skills, updates the managed `AGENTS.md` fence, and removes skills that the
|
|
14
14
|
package no longer ships. The skills always live in `.agents/skills/`. `--harness` selects `claude`, which also links each
|
|
15
|
-
skill into `.claude/skills/`, `codex`, or `all`; without it, the command prompts
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
skill into `.claude/skills/`, `codex`, or `all`; without it, the command prompts on a first install. Pass `--reconfigure` to
|
|
16
|
+
choose again. On a project that already has an install, re-running `install skills` or `install hooks` without `--harness`
|
|
17
|
+
reads the harness back from the installed files: Claude skill links and installer-owned hook registrations, with Codex
|
|
18
|
+
assumed when skills leave no trace.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
the
|
|
20
|
+
Refresh the skills with the installer; do not hand-edit them. Keep `.agents/skills/imf-web-ui*` out of the project's
|
|
21
|
+
formatter: a reformatted skill no longer matches the package copy, and the session-start check reports it as stale.
|
|
21
22
|
|
|
22
23
|
## The hooks
|
|
23
24
|
|
|
@@ -36,7 +37,8 @@ harness that is not selected.
|
|
|
36
37
|
|
|
37
38
|
The shipped events are:
|
|
38
39
|
|
|
39
|
-
- `SessionStart`: point the agent at the Web UI router once per session
|
|
40
|
+
- `SessionStart`: point the agent at the Web UI router once per session, and tell it when the installed skills differ from
|
|
41
|
+
the copy in `node_modules/@imfusion/web-ui`.
|
|
40
42
|
- `SubagentStart`: provide the same pointer to spawned agents.
|
|
41
43
|
- `UserPromptSubmit`: name the companion skills relevant to the prompt.
|
|
42
44
|
- `Stop`: ask for project verification after a turn edits source files.
|
|
@@ -44,9 +46,6 @@ The shipped events are:
|
|
|
44
46
|
Read existing registrations first. If the project already covers an event, do not stack a second hook; adapt the existing
|
|
45
47
|
registration and keep the missing behavior.
|
|
46
48
|
|
|
47
|
-
`baseline-staleness.sh` is a pre-commit check, not an agent hook. It compares installed skill markers with the installed
|
|
48
|
-
package version.
|
|
49
|
-
|
|
50
49
|
## Codex trust
|
|
51
50
|
|
|
52
51
|
Codex runs project hooks only after the project is trusted and each script is approved through `/hooks`. Revisit that review
|
|
@@ -28,6 +28,6 @@ The `staged` block and the scripts under `scripts/verify/` own the step lists. H
|
|
|
28
28
|
|
|
29
29
|
## Staleness at commit time
|
|
30
30
|
|
|
31
|
-
Pre-commit also runs advisory
|
|
32
|
-
|
|
33
|
-
`npx web-ui install
|
|
31
|
+
Pre-commit also runs an advisory check for documentation drift. If a staged change makes a doc stale, update the doc in the
|
|
32
|
+
same commit. Outdated installed skills surface at session start instead, as the `agent-tooling` topic describes; refresh them
|
|
33
|
+
with `npx web-ui install skills` (also `npx web-ui install hooks` if the project uses hooks).
|
|
@@ -79,8 +79,9 @@ npx web-ui install skills
|
|
|
79
79
|
npx web-ui install hooks
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Both commands
|
|
83
|
-
|
|
82
|
+
Both commands read the harness back from the installed files and print it. Codex leaves no trace without hooks, so a Claude
|
|
83
|
+
and Codex install without hooks reads as Claude only; when the printed harness is wrong, pass the project's harness, e.g.
|
|
84
|
+
`--harness=all`. Stop on install failure. Do not use `--force` or `--legacy-peer-deps`.
|
|
84
85
|
|
|
85
86
|
The installer owns the vendored skills, its `AGENTS.md` fence, and hook scripts. Run the installer commands after the package
|
|
86
87
|
update even when the existing registrations look complete: they refresh scripts, prune retired registrations, and merge
|
|
@@ -88,8 +89,7 @@ installer-owned entries idempotently into the registration file for the chosen h
|
|
|
88
89
|
`.codex/hooks.json`, or both). Stop on malformed settings or installer conflicts. Read existing host registrations first and
|
|
89
90
|
do not stack an event the project already covers.
|
|
90
91
|
|
|
91
|
-
After the installer command, inspect the installed skill diffs and briefly report any guidance changes
|
|
92
|
-
changes alone do not count.
|
|
92
|
+
After the installer command, inspect the installed skill diffs and briefly report any guidance changes.
|
|
93
93
|
|
|
94
94
|
Then re-read, in full, every changed skill and reference the remaining work uses, starting with this update skill when it
|
|
95
95
|
changed. A diff shows what moved, not the step order you continue under, so it does not replace the re-read. Continue from
|
|
@@ -117,7 +117,7 @@ Run the consumer's documented full verification. Read its scripts first; prefer
|
|
|
117
117
|
non-watch format, lint, typecheck, test, and build commands that exist.
|
|
118
118
|
|
|
119
119
|
Verify that the installed package, manifest, and lockfile match the recorded target version. A mismatch is a base-update
|
|
120
|
-
conflict. Check
|
|
120
|
+
conflict. Check the `AGENTS.md` fence, hook registrations, and the final update-owned path list.
|
|
121
121
|
|
|
122
122
|
Report before the first commit:
|
|
123
123
|
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env sh
|
|
2
|
-
# Advisory baseline-staleness check, meant to be called from the repo's pre-commit hook.
|
|
3
|
-
# Compares the installed imf-web-ui skill markers against the installed package version
|
|
4
|
-
# and warns on drift. Always exits 0 — staleness never blocks a commit.
|
|
5
|
-
PKG=node_modules/@imfusion/web-ui/package.json
|
|
6
|
-
[ -f "$PKG" ] || exit 0
|
|
7
|
-
# A symlinked checkout reports its own dev version.
|
|
8
|
-
[ -L node_modules/@imfusion/web-ui ] && exit 0
|
|
9
|
-
CURRENT=$(node -p "require('./$PKG').version" 2>/dev/null) || exit 0
|
|
10
|
-
|
|
11
|
-
# .claude/skills may be a symlink to .agents/skills; skip markers already read.
|
|
12
|
-
seen=""
|
|
13
|
-
for marker in .agents/skills/imf-web-ui*/.imf-web-ui-skill-version.json .claude/skills/imf-web-ui*/.imf-web-ui-skill-version.json; do
|
|
14
|
-
[ -f "$marker" ] || continue
|
|
15
|
-
real=$(cd "$(dirname "$marker")" && pwd -P)/$(basename "$marker")
|
|
16
|
-
case " $seen " in
|
|
17
|
-
*" $real "*) continue ;;
|
|
18
|
-
esac
|
|
19
|
-
seen="$seen $real"
|
|
20
|
-
INSTALLED=$(node -p "require('./$marker').version" 2>/dev/null) || continue
|
|
21
|
-
if [ "$INSTALLED" != "$CURRENT" ]; then
|
|
22
|
-
echo "imf-web-ui skills are stale ($INSTALLED installed, package is $CURRENT) — run /imf-web-ui-update, or: npx web-ui install skills && npx web-ui install hooks"
|
|
23
|
-
break
|
|
24
|
-
fi
|
|
25
|
-
done
|
|
26
|
-
exit 0
|