basalt-ui 1.29.0 → 1.29.2

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/AGENTS.md CHANGED
@@ -31,7 +31,7 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
31
31
  ## Hard rules
32
32
 
33
33
  **Mantine-free boundary**: `basalt-ui/charts`, `basalt-ui/tokens`, `basalt-ui/guard`,
34
- `basalt-ui/query`, `basalt-ui/router-tanstack`, `basalt-ui/agent`, and `basalt-ui/state` must
34
+ `basalt-ui/router-tanstack`, `basalt-ui/agent`, and `basalt-ui/state` must
35
35
  import zero `@mantine/*`. `@visx/*` is permitted only inside `src/charts/**`. `@visx/*`
36
36
  (`basalt/visx-boundary`) is enforced by an oxlint plugin rule in both the repo and the shipped
37
37
  consumer preset.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [1.29.2](https://github.com/jkrumm/basalt-ui/compare/v1.29.1...v1.29.2) (2026-09-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * distribute unwrap's NonNullable over Eden's union response shape ([d46bc00](https://github.com/jkrumm/basalt-ui/commit/d46bc00ae46fcbe76e0b89516140735b1f3295fb))
7
+
8
+ ## [1.29.1](https://github.com/jkrumm/basalt-ui/compare/v1.29.0...v1.29.1) (2026-09-03)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * collapse unwrap's two overloads into one conditional generic ([80152d7](https://github.com/jkrumm/basalt-ui/commit/80152d7d8030ce3455d8ba4f314e6aa59a88feab))
14
+ * let BasaltDevDock's router prop accept a real TanStack Router ([955cdfc](https://github.com/jkrumm/basalt-ui/commit/955cdfca02d28e04e4665e652f1b0a53eb92cdfb))
15
+ * make chart-missing-aria-label's tag scan brace-depth aware ([0d6c122](https://github.com/jkrumm/basalt-ui/commit/0d6c12239e99dc26b15d202050810c7788317acf))
16
+ * retire basalt/query-dual-import — its premise is unreachable post-C1 ([372bc1f](https://github.com/jkrumm/basalt-ui/commit/372bc1fa9a32008a1cde2f13e0a2f826262cbe39))
17
+
1
18
  # [1.29.0](https://github.com/jkrumm/basalt-ui/compare/v1.28.0...v1.29.0) (2026-09-02)
2
19
 
3
20
 
package/MIGRATING.md CHANGED
@@ -48,6 +48,23 @@ see § The shell scrollport below.
48
48
 
49
49
  What is new, one line each — nothing here renames or removes anything:
50
50
 
51
+ - **`BasaltDevDock`'s `router` prop now accepts a real TanStack `Router` instance with no cast** —
52
+ the prior structural stand-in rejected a class instance under TS weak-type detection (no property
53
+ overlap, no index signature), forcing `router={router as never}`. It is now `router?: object`:
54
+ any non-null object is assignable, and the devtools panel remains the only reader.
55
+ - **`unwrap` collapses to ONE generic signature with a conditional return**, replacing the two
56
+ overloads this same minor introduced. The overload pair resolved `unwrap` to `unknown` whenever it
57
+ was passed as a bare callback (`api.threads.get().then(unwrap)`) — `.then()` reads the declared
58
+ type of the reference rather than applying an overload, and TS cannot propagate generic inference
59
+ across an overload set the way it can for a single signature. `.then(unwrap)` now infers `TData`
60
+ again, alongside `unwrap(await p)` and `unwrap(p)` — nothing about the runtime behaviour changed.
61
+ - **`unwrap` strips `null`/`undefined` from a union-typed Eden response again** — the single
62
+ conditional signature above still inferred `TData` off `Envelope<infer TData>`, and an Eden
63
+ Treaty response is a UNION of envelopes (`{ data: T; error: null } | { data: null; error:
64
+ EdenFetchError }`), not one envelope with a nullable `data`; inferring through the wrapper type
65
+ read `T | null` at every Eden call site. `UnwrapResult` now infers the raw `data` field and wraps
66
+ it in `NonNullable`, which distributes over a union `R` — `unwrap(await api.x.get())` resolves to
67
+ `T` again, with no `| null`. Runtime unchanged.
51
68
  - **`scrollParentOf` / `SCROLLPORT_ATTRIBUTE`** (`basalt-ui`) — resolve which box actually scrolls an
52
69
  element (`null` = the document). The seam behind the shell scrollport change below.
53
70
  - **`PageAside`** (`basalt-ui`) — the right-hand aside REGION a route claims, with the panel filter
@@ -730,6 +747,16 @@ half is deliberately NOT exempted the same way — a local `StatCard` beside an
730
747
  `import { StatCard as Base }` kept the name AND a piece of the original, which is the fork shape this
731
748
  rule most wants to see.
732
749
 
750
+ ### Guards — `basalt/query-dual-import` removed
751
+
752
+ **Retired, not merely relaxed** — its `RETIRED_RULE_IDS` entry (kept recognized, so an existing
753
+ `theme-allow query-dual-import` still reads as a dead waiver rather than a typo) and its
754
+ `PLUGIN_RULE_GRACE` ledger row are both deleted. Its whole premise — a raw `@tanstack/react-query`
755
+ import beside `basalt-ui/query` — went unreachable the moment `./query` dropped (see § Consolidation
756
+ above): a consumer now MUST import `@tanstack/react-query` directly, and the rule warned on every
757
+ one of those imports instead of the drift it was written to catch (90+ false positives on a single
758
+ consumer). `query-fn-unwrap` is unaffected — it is a different rule, still `warn` (grace → 1.30.0).
759
+
733
760
  ### `basalt-ui/data` — a typed facet id, and a required `getItemKey`
734
761
 
735
762
  **`BasaltDataTable`: a mistyped `facets[].columnId` used to render no pill, silently — `if
package/README.md CHANGED
@@ -143,8 +143,8 @@ The canonical stack, once a router and a query client join the example above:
143
143
  ```
144
144
 
145
145
  The order is load-bearing, not stylistic: `BasaltProvider` auto-mounts `ConnectivityProvider`
146
- (`./connectivity`), which aggregates React-Query's own status and so must sit **above** the query
147
- client; `BasaltOverlays` must sit **inside** the theme, since Modals/Spotlight/Notifications
146
+ (C1: absorbed onto the root barrel from the dropped `./connectivity`), which aggregates
147
+ React-Query's own status and so must sit **above** the query client; `BasaltOverlays` must sit **inside** the theme, since Modals/Spotlight/Notifications
148
148
  resolve `--vx-*` tokens through Mantine's context; the router goes **last** so every route
149
149
  component renders with the theme, the data layer and the overlay layer already mounted above it.
150
150
  `agent/rules/basalt-mantine.md` points here rather than restating the tree.
@@ -322,31 +322,31 @@ deleted rather than silently surviving into the next real skew.
322
322
 
323
323
  ## Subpath exports
324
324
 
325
- | Subpath | Mantine? | Purpose |
326
- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
327
- | `.` | coupled | `BasaltProvider`, `createBasaltTheme` / `baseTheme` / `cssVariablesResolver`, `BasaltShell` + sidebar / mobile nav / breadcrumbs / `PageBar`, `Section` / `WidgetHeader`, `NavCountBadge`, `SidebarAccount` + the provider-agnostic account contract (`BasaltAccountProps`/`State`/`Actions`), `ThreadWorkspace` + thread-chat components, dashboard composites (`DeltaBadge`, `StatCard`, `EmptyState`, `SettingsSection`/`SettingsRow`/`DangerZone`) and the query-branch trio `QueryState` / `LoadingState` / `ErrorState`, shell types — the root entry still re-exports the thread-chat components; `./agent-chat` takes the same components standalone, without `BasaltProvider`, the shell, the dashboard composites, or `./connectivity` |
328
- | `./charts` | **free** | `CartesianChart` + visx primitives, kinds, sparklines, hooks, token re-exports |
329
- | `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
330
- | `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
331
- | `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
332
- | `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core; plus the annotation reader `--audit-allows` is built on: `findAllowAnnotations`, `neutralizeAllowAnnotation`, `NEUTRALIZED_ALLOW_TOKEN`, `PLUGIN_RULE_IDS`, `AllowAnnotationSite` |
333
- | `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools`, `toErrorMessage` / `errorStatus` — the only route to those two; the root barrel does not re-export them |
334
- | `./router-tanstack` | **free** | TanStack Router bridge: `defineNav`/`navGroup`/`navTarget`/`flattenNav` + `useNav` (→ `{ sections, mobileNav }`), `useBasaltNav` (active route), `useRouterBreadcrumbs`, and `createSearchStore` + the `field.*` vocabulary |
335
- | `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `inputProps`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
336
- | `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell` |
337
- | `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays` |
338
- | `./data` | coupled | Convenience barrel pulling both peer groups: `BasaltDataTable`, `BasaltVirtualList` (Mantine-rendered) — prefer `./data/table` / `./data/virtual` for per-feature opt-in |
339
- | `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine |
340
- | `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine |
341
- | `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam |
342
- | `./agent-chat` | coupled | Mantine-styled thread-chat components over `./agent`: `ThreadWorkspace`, `ThreadFeed` (`variant`/`renderRow`), `ThreadFeedRow` (inline-expanding Slack row, lazily mounted + kept mounted), `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript` (`groupConsecutive`/`affordances`/`virtualize` — windowed transcripts open at the newest message unless `initialScroll: 'start'`; `virtualize` is enabled by the optional `@tanstack/react-virtual` peer, absent it degrades to an unwindowed, height-bound pane), `threadPartRenderers` — **requires `motion`**, not merely an optional peer; see [Requirements](#requirements) |
343
- | `./state` | **free** | createPersistedState (versioned localStorage) + the store field vocabulary (field.enum/multi/range/number/boolean/string, FieldHandle, lanes) + createLocalStore, the router-free store — Mantine-free state primitives |
344
- | `./connectivity` | coupled | `ConnectivityProvider` (aggregates browser/React-Query/SSE/health-check status), `useConnectivity`, `ConnectivityIndicator` — auto-mounted by `BasaltProvider` |
345
- | `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents` |
346
- | `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack |
347
- | `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with a bundler but no React or Mantine. **With no bundler, use the `basalt-ui tokens:css` CLI instead** and commit the emitted file: importing this subpath needs bare-specifier resolution. See [`docs/FRAMEWORK-FREE.md`](https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md) |
348
- | `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook |
349
- | `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers |
325
+ | Subpath | Mantine? | Purpose |
326
+ | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
327
+ | `.` | coupled | `BasaltProvider`, `createBasaltTheme` / `baseTheme` / `cssVariablesResolver`, `BasaltShell` + sidebar / mobile nav / breadcrumbs / `PageBar` / `PageAside`, `Section` / `WidgetHeader`, `NavCountBadge`, `SidebarAccount` + the provider-agnostic account contract (`BasaltAccountProps`/`State`/`Actions`), `ThreadWorkspace` + thread-chat components, dashboard composites (`DeltaBadge`, `StatCard`, `EmptyState`, `SettingsSection`/`SettingsRow`/`DangerZone`), the query-branch trio `QueryState` / `LoadingState` / `ErrorState`, `createBasaltQueryClient` / `unwrap` / `toErrorMessage` / `errorStatus` / `BasaltQueryDevtools` (C1: absorbed from the dropped `./query`), `ConnectivityProvider` (auto-mounted) / `useConnectivity` / `ConnectivityIndicator` (C1: absorbed from the dropped `./connectivity`), shell types — the root entry still re-exports the thread-chat components; `./agent-chat` takes the same components standalone, without `BasaltProvider`, the shell, or the dashboard composites |
328
+ | `./charts` | **free** | `CartesianChart` + visx primitives, kinds, sparklines, hooks, token re-exports |
329
+ | `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
330
+ | `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
331
+ | `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
332
+ | `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core; plus the annotation reader `--audit-allows` is built on: `findAllowAnnotations`, `neutralizeAllowAnnotation`, `NEUTRALIZED_ALLOW_TOKEN`, `PLUGIN_RULE_IDS`, `AllowAnnotationSite` |
333
+ | `./format` | **free** | `Intl`-backed, Mantine-free, React-free formatting: `money`/`percent`/`integer`/`compact`/`deltaPct`, `duration`/`durationClock`/`clock`/`weekday`/`relativeTime`, `km`/`kcal` |
334
+ | `./router-tanstack` | **free** | TanStack Router bridge: `defineNav`/`navGroup`/`navTarget`/`flattenNav` + `useNav` (→ `{ sections, mobileNav }`), `useBasaltNav` (active route), `useRouterBreadcrumbs`, and `createSearchStore` + the `field.*` vocabulary |
335
+ | `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `inputProps`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
336
+ | `./notifications` | coupled | Mantine notifications: `notify` helpers, typed registry, persisted history, `NotificationBell` |
337
+ | `./commands` | coupled | Typed command bus + overlay controller, `toSpotlightActions`, `ShortcutsHelp`, `BasaltOverlays` |
338
+ | `./data/table` | coupled | `BasaltDataTable` — sortable data table over TanStack Table, rendered with Mantine |
339
+ | `./data/virtual` | coupled | `BasaltVirtualList` — windowed virtual list over TanStack Virtual, rendered with Mantine |
340
+ | `./agent` | **free** | Headless streaming layer (`useAgentStream`, `PartList`) + multi-thread `createThreadsStore` / `useAgentThreadRuns` / outcome seam |
341
+ | `./agent-chat` | coupled | Mantine-styled thread-chat components over `./agent`: `ThreadWorkspace`, `ThreadFeed` (`variant`/`renderRow`), `ThreadFeedRow` (inline-expanding Slack row, lazily mounted + kept mounted), `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript` (`groupConsecutive`/`affordances`/`virtualize` — windowed transcripts open at the newest message unless `initialScroll: 'start'`; `virtualize` is enabled by the optional `@tanstack/react-virtual` peer, absent it degrades to an unwindowed, height-bound pane), `threadPartRenderers` — **requires `motion`**, not merely an optional peer; see [Requirements](#requirements) |
342
+ | `./content` | coupled | `Prose`, `CodeBlock`, `Callout`, `TableOfContents`, `ReadingProgress`, `Markdown`, `MermaidDiagram`, `ArticleLayout`, `ArticleCard` / `ArticleGrid`, `GuideLink` / `GuideDrawer`, `mdxComponents` |
343
+ | `./controls` | coupled | The control tier: `FilterSet` + the `FieldHandle`-bound filters (`RangeFilter`/`CompareFilter`/`SelectFilter`/`MultiSelectFilter`/`NumberFilter`/`SearchFilter`/`ToggleFilter`), `ViewTabs`, `ActionGroup`/`OverflowMenu`/`SyncButton`, plus the aside's `PanelRow`/`SliderControl` row primitives — `pill`/`sheet`/`panel` surfaces, picked by the home, never a prop |
344
+ | `./controls-dates` | coupled | `DateRangePicker` — the `@mantine/dates` implementation of `RangeFilter`'s `customPicker` seam; its own subpath so `./controls` resolves and renders with no `@mantine/dates` installed |
345
+ | `./state` | **free** | createPersistedState (versioned localStorage) + the store field vocabulary (field.enum/multi/range/number/boolean/string, FieldHandle, lanes) + createLocalStore, the router-free store — Mantine-free state primitives |
346
+ | `./styles.css` | — | `@layer basalt` base styles, iOS input safety net, font stack |
347
+ | `./tokens.css` | **free** | Prebuilt `--vx-*` stylesheet — the default `buildPaletteCss()` output as a plain file, for a consumer with a bundler but no React or Mantine. **With no bundler, use the `basalt-ui tokens:css` CLI instead** and commit the emitted file: importing this subpath needs bare-specifier resolution. See [`docs/FRAMEWORK-FREE.md`](https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md) |
348
+ | `./configs/*` | — | Raw toolchain presets — oxlint, oxfmt, tsconfig (base/react-app/node), lefthook |
349
+ | `./llms.txt` | — | Machine-readable surface map — one entry per published subpath with import specifier, description, layer, and optional peers |
350
350
 
351
351
  Named exports only — no default exports.
352
352
 
@@ -496,28 +496,32 @@ It resolves ahead of the `xTicks` COUNT, which is unchanged; omit both and ticks
496
496
  Reach for values on a dense time axis — the tick chooser appends the final key unconditionally, so
497
497
  a count that misses the last index collides two labels at the right edge at every count.
498
498
 
499
- ### `./query` — TanStack Query adapter
499
+ ### Query — `createBasaltQueryClient` / `unwrap` (root barrel)
500
+
501
+ `./query` was dropped in 1.29.0 (C1 consolidation) — its two exports moved onto the root barrel,
502
+ which already requires `@tanstack/react-query` as a peer. TanStack itself
503
+ (`QueryClientProvider`, hooks, `queryOptions`) is imported straight from `@tanstack/react-query`,
504
+ never re-exported by basalt.
500
505
 
501
506
  ```bash
502
507
  bun add @tanstack/react-query @tanstack/react-query-devtools
503
508
  ```
504
509
 
505
510
  ```tsx
506
- import { createBasaltQueryClient, QueryClientProvider, unwrap } from 'basalt-ui/query'
511
+ import { createBasaltQueryClient } from 'basalt-ui'
512
+ import { QueryClientProvider } from '@tanstack/react-query'
507
513
 
508
514
  const [client] = useState(() => createBasaltQueryClient())
509
515
  // queryFn: () => unwrap(api.users.get())
510
516
  ```
511
517
 
512
- `toErrorMessage(err, fallback?)` and `errorStatus(err)` also live here — decode an unknown thrown
513
- value into something renderable, folding an HTTP status into the fallback when the body decodes to
514
- nothing usable (`''`, `'{}'`, `'[object Object]'`, …).
518
+ `toErrorMessage(err, fallback?)` and `errorStatus(err)` also live on the root barrel — decode an
519
+ unknown thrown value into something renderable, folding an HTTP status into the fallback when the
520
+ body decodes to nothing usable (`''`, `'{}'`, `'[object Object]'`, …). `BasaltQueryDevtools` (lazy)
521
+ is there too.
515
522
 
516
523
  #### Rendering a query — `QueryState`
517
524
 
518
- Imported from **`basalt-ui`**, not `basalt-ui/query`: it renders Mantine, and `./query` is a
519
- guard-enforced Mantine-free entry.
520
-
521
525
  ```tsx
522
526
  import { QueryState } from 'basalt-ui'
523
527
 
@@ -634,17 +638,17 @@ export const COMMANDS = defineCommands({
634
638
  runCommand('file:save')
635
639
  ```
636
640
 
637
- ### `./data` — TanStack Table + Virtual
641
+ ### `./data/table` + `./data/virtual` — TanStack Table + Virtual
638
642
 
639
- `./data` is a convenience barrel that pulls in both peer groups. Prefer the fine subpaths below
640
- for per-feature opt-in — each only requires its own peer.
643
+ Two subpaths, one per peer group (C1: there is no bare `./data` barrel) — install only what you
644
+ use.
641
645
 
642
646
  ```bash
643
647
  bun add @tanstack/react-table @tanstack/react-virtual
644
648
  ```
645
649
 
646
650
  ```tsx
647
- import { BasaltDataTable, BasaltVirtualList, createColumnHelper } from 'basalt-ui/data'
651
+ import { BasaltDataTable, createColumnHelper } from 'basalt-ui/data/table'
648
652
 
649
653
  const col = createColumnHelper<Row>()
650
654
  const columns = [
@@ -10,7 +10,7 @@ paths:
10
10
 
11
11
  <!-- basalt:coverage -->
12
12
  <!-- GENERATED from src/surfaces.ts — `bun scripts/check-coverage.ts --write`. Do not hand-edit. -->
13
- <!-- backed by: guard kinds — none · oxlint rules — basalt/agent-no-raw-usechat, basalt/agent-resume-guard, basalt/ai-sdk-major, basalt/forms-field-key, basalt/query-dual-import, basalt/query-fn-unwrap -->
13
+ <!-- backed by: guard kinds — none · oxlint rules — basalt/agent-no-raw-usechat, basalt/agent-resume-guard, basalt/ai-sdk-major, basalt/forms-field-key, basalt/query-fn-unwrap -->
14
14
  <!-- not guarded: — -->
15
15
  <!-- /basalt:coverage -->
16
16
 
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Scaffold written by `basalt-ui init`. This file is yours — `basalt-ui sync` will not overwrite it.
6
6
  */
7
- import { createBasaltQueryClient } from 'basalt-ui/query'
7
+ import { createBasaltQueryClient } from 'basalt-ui'
8
8
 
9
9
  export const queryClient = createBasaltQueryClient({
10
10
  // staleTime: 60_000, // optional: global cache freshness window (ms)
@@ -91,8 +91,16 @@ import { fileURLToPath } from 'node:url'
91
91
  * - `duplicate-notifications-mount` — its only case, `<BasaltNotifications />` mounted beside
92
92
  * `<BasaltOverlays notifications>`, is unreachable now that `BasaltNotifications` no longer
93
93
  * exists as a standalone export; the rule had nothing left to fire on.
94
- */
95
- export const RETIRED_RULE_IDS = new Set(['unframed-chart', 'duplicate-notifications-mount'])
94
+ * - `query-dual-import` — its whole premise (a raw `@tanstack/react-query` import beside
95
+ * `basalt-ui/query`) is unreachable now that C1 (1.29.0) dropped the `./query` subpath: a
96
+ * consumer MUST import `@tanstack/react-query` directly, so the rule warned on every such
97
+ * import instead of the drift it was written to catch.
98
+ */
99
+ export const RETIRED_RULE_IDS = new Set([
100
+ 'unframed-chart',
101
+ 'duplicate-notifications-mount',
102
+ 'query-dual-import',
103
+ ])
96
104
 
97
105
  /**
98
106
  * Rule ids a `theme-allow` may name — this plugin's own rules plus `src/guard`'s kinds. Duplicated
@@ -3354,83 +3362,15 @@ const providerAboveRouter = {
3354
3362
  },
3355
3363
  }
3356
3364
 
3357
- // ── Rule 27 — query-dual-import ─────────────────────────────────────────────────────────────────
3365
+ // Rule 27 — query-dual-import — RETIRED. 1.29.0 dropped every raw TanStack re-export from
3366
+ // `basalt-ui/query` (the subpath itself is gone), so "dual-importing `@tanstack/react-query`
3367
+ // beside `basalt-ui/query`" is no longer a thing a consumer can even do — a consumer MUST import
3368
+ // `@tanstack/react-query` directly now, and this rule warned on every such import (90+ false
3369
+ // positives on argo alone). See `RETIRED_RULE_IDS` below.
3358
3370
 
3359
- /** `@tanstack/react-query` and its subpaths — never `@tanstack/react-query-devtools`. */
3360
- const TANSTACK_QUERY_SOURCE = /^@tanstack\/react-query(?:\/|$)/
3361
-
3362
- /** The `basalt-ui/query` subpath, exactly — the seam the hooks are supposed to arrive through. */
3371
+ /** The `basalt-ui/query` subpath, exactly — kept for `query-fn-unwrap`'s own gate below. */
3363
3372
  const BASALT_QUERY_SOURCE = 'basalt-ui/query'
3364
3373
 
3365
- /** The root barrel — where `QueryState` and the dashboard composites live. */
3366
- const BASALT_ROOT_SOURCE = 'basalt-ui'
3367
-
3368
- const QUERY_DUAL_IMPORT_MESSAGE =
3369
- 'Dual query import — this file imports from @tanstack/react-query AND from basalt-ui/query. ' +
3370
- 'Take the hooks, the client factory and `unwrap` from basalt-ui/query only: two entry points to ' +
3371
- 'one library is how a second QueryClient, a second devtools bundle and a duplicated ' +
3372
- '`toErrorMessage` reach an app (agent/rules/basalt-batteries.md). ' +
3373
- '(basalt/query-dual-import)'
3374
-
3375
- const QUERY_ROOT_IMPORT_MESSAGE =
3376
- 'Raw @tanstack/react-query import in a basalt file — the hooks come through basalt-ui/query, ' +
3377
- 'which re-exports them beside `createBasaltQueryClient`, `unwrap`, `toErrorMessage` and the lazy ' +
3378
- 'devtools (agent/rules/basalt-batteries.md). Import from basalt-ui/query instead. ' +
3379
- '(basalt/query-dual-import)'
3380
-
3381
- /**
3382
- * The `basalt-batteries.md` law "import query hooks from `basalt-ui/query`, never dual-import
3383
- * `@tanstack/react-query`" — F5, and one of the two `./query` surfaces that shipped advisory.
3384
- *
3385
- * TWO forms, one id, because they are one law read at two strengths. The DUAL form (both
3386
- * specifiers present) is the law verbatim. The softer form — a raw `@tanstack/react-query` import
3387
- * in a file that also imports the basalt root barrel — is a heuristic: it says "this file is
3388
- * already a basalt file, so the query seam is right there", and it is the form that actually
3389
- * catches the drift, since a file that has never heard of `basalt-ui/query` cannot dual-import.
3390
- * The softer form is why the whole rule lands at `warn` rather than `error`.
3391
- *
3392
- * Gated on the LITERAL specifier, never on `ownTree` relative resolution: basalt's own
3393
- * `src/query/**` and `src/dashboard/query-state.tsx` import `@tanstack/react-query` because they
3394
- * ARE the seam, and they reach their siblings relatively. A file that writes `basalt-ui/query` is
3395
- * by definition a consumer of the published surface.
3396
- */
3397
- // Ships: warn (grace → 1.30.0)
3398
- const queryDualImport = {
3399
- meta: {
3400
- type: 'suggestion',
3401
- docs: { description: 'Warn on a raw @tanstack/react-query import beside a basalt import.' },
3402
- schema: [],
3403
- },
3404
- create(context) {
3405
- if (isTestFile(context)) return {}
3406
- const raw = []
3407
- let basaltQuery = false
3408
- let basaltRoot = false
3409
-
3410
- return {
3411
- ImportDeclaration(node) {
3412
- const source = node.source?.value
3413
- if (typeof source !== 'string') return
3414
- if (source === BASALT_QUERY_SOURCE) basaltQuery = true
3415
- else if (source === BASALT_ROOT_SOURCE) basaltRoot = true
3416
- // `import type { UseQueryResult } from '@tanstack/react-query'` is erased at compile time
3417
- // — there is no runtime import to route through the seam, and typing against the library's
3418
- // own result type is exactly what `QueryStateLike` documents as legitimate. Same skip
3419
- // `agent-no-raw-usechat` and `ai-sdk-major` take.
3420
- else if (TANSTACK_QUERY_SOURCE.test(source) && node.importKind !== 'type') raw.push(node)
3421
- },
3422
- 'Program:exit'() {
3423
- if (!basaltQuery && !basaltRoot) return
3424
- const message = basaltQuery ? QUERY_DUAL_IMPORT_MESSAGE : QUERY_ROOT_IMPORT_MESSAGE
3425
- for (const node of raw) {
3426
- if (hasThemeAllow(context, node, 'query-dual-import')) continue
3427
- context.report({ node, message })
3428
- }
3429
- },
3430
- }
3431
- },
3432
- }
3433
-
3434
3374
  // ── Rule 28 — query-fn-unwrap ───────────────────────────────────────────────────────────────────
3435
3375
 
3436
3376
  const QUERY_FN_UNWRAP_MESSAGE =
@@ -3904,18 +3844,6 @@ export const PLUGIN_RULE_GRACE = {
3904
3844
  'image-gen and the playground is measured — every one of them mounts a router, so the ' +
3905
3845
  'false-positive question has a real sample.',
3906
3846
  },
3907
- 'query-dual-import': {
3908
- since: '1.28.0',
3909
- promote: '1.30.0',
3910
- why:
3911
- 'F5 — the basalt-batteries.md law with no guard. TWO forms under one id: the dual import ' +
3912
- '(both `@tanstack/react-query` and `basalt-ui/query` in one file) is the law verbatim, and ' +
3913
- 'the softer form — a raw `@tanstack/react-query` import in a file that also imports the ' +
3914
- 'basalt root barrel — is the heuristic that actually catches the drift, since a file that ' +
3915
- 'never names `basalt-ui/query` cannot dual-import. The soft form is what holds the whole ' +
3916
- 'rule at `warn`: "this file is already a basalt file" is a claim about intent. 1.30.0 is ' +
3917
- 'when the consumer count is measured and the soft form is either kept or split off.',
3918
- },
3919
3847
  'forms-field-key': {
3920
3848
  since: '1.28.0',
3921
3849
  promote: '1.30.0',
@@ -4000,7 +3928,6 @@ export default {
4000
3928
  'search-literal-link': searchLiteralLink,
4001
3929
  'use-search-from-literal': useSearchFromLiteral,
4002
3930
  'provider-above-router': providerAboveRouter,
4003
- 'query-dual-import': queryDualImport,
4004
3931
  'query-fn-unwrap': queryFnUnwrap,
4005
3932
  'deprecated-export': deprecatedExport,
4006
3933
  'forms-field-key': formsFieldKey,
@@ -38,7 +38,6 @@ beforeEach(() => {
38
38
  jsPlugins: [PLUGIN_PATH],
39
39
  rules: {
40
40
  'basalt/provider-above-router': 'error',
41
- 'basalt/query-dual-import': 'error',
42
41
  'basalt/query-fn-unwrap': 'error',
43
42
  'basalt/deprecated-export': 'error',
44
43
  'basalt/forms-field-key': 'error',
@@ -3050,85 +3049,9 @@ describe('basalt/provider-above-router', () => {
3050
3049
  })
3051
3050
  })
3052
3051
 
3053
- // ── query-dual-import (F5) ───────────────────────────────────────────────────
3054
-
3055
- describe('basalt/query-dual-import', () => {
3056
- it('flags a file importing from both @tanstack/react-query and basalt-ui/query', () => {
3057
- const { code, rules, output } = run(
3058
- `import { useQuery } from '@tanstack/react-query'\n` +
3059
- `import { unwrap } from 'basalt-ui/query'\n` +
3060
- `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3061
- )
3062
- expect(code).toBe(1)
3063
- expect(rules).toContain('query-dual-import')
3064
- expect(output).toContain('Dual query import')
3065
- })
3066
-
3067
- // The softer half — the one that actually catches the drift, and why the rule is `warn`.
3068
- it('flags a raw @tanstack/react-query import beside a basalt root import', () => {
3069
- const { code, rules, output } = run(
3070
- `import { useQuery } from '@tanstack/react-query'\n` +
3071
- `import { QueryState } from 'basalt-ui'\n` +
3072
- `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3073
- )
3074
- expect(code).toBe(1)
3075
- expect(rules).toContain('query-dual-import')
3076
- expect(output).toContain('Raw @tanstack/react-query import')
3077
- })
3078
-
3079
- it('does NOT flag basalt-ui/query on its own', () => {
3080
- const { code, rules } = run(
3081
- `import { useQuery, unwrap } from 'basalt-ui/query'\n` +
3082
- `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
3083
- )
3084
- expect(code).toBe(0)
3085
- expect(rules).not.toContain('query-dual-import')
3086
- })
3087
-
3088
- // `import type` is erased at compile time — there is no runtime import to route through the
3089
- // seam, and typing against the library's own result type is what `QueryStateLike` documents as
3090
- // legitimate. Same skip `agent-no-raw-usechat` and `ai-sdk-major` take.
3091
- it('does NOT flag a type-only @tanstack/react-query import beside basalt-ui/query', () => {
3092
- const { code, rules } = run(
3093
- `import type { UseQueryResult } from '@tanstack/react-query'\n` +
3094
- `import { unwrap } from 'basalt-ui/query'\n` +
3095
- `export const pick = (q: UseQueryResult<number>) => unwrap(q.data)\n`,
3096
- )
3097
- expect(code).toBe(0)
3098
- expect(rules).not.toContain('query-dual-import')
3099
- })
3100
-
3101
- it('does NOT flag @tanstack/react-query in a file with no basalt import', () => {
3102
- const { code, rules } = run(
3103
- `import { useQuery } from '@tanstack/react-query'\n` +
3104
- `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
3105
- )
3106
- expect(code).toBe(0)
3107
- expect(rules).not.toContain('query-dual-import')
3108
- })
3109
-
3110
- // The devtools package is a different package, not a subpath.
3111
- it('does NOT flag @tanstack/react-query-devtools', () => {
3112
- const { code, rules } = run(
3113
- `import { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n` +
3114
- `import { unwrap } from 'basalt-ui/query'\n` +
3115
- `export const D = () => <ReactQueryDevtools />\n`,
3116
- )
3117
- expect(code).toBe(0)
3118
- expect(rules).not.toContain('query-dual-import')
3119
- })
3120
-
3121
- it('honours a theme-allow naming the rule', () => {
3122
- const { code, rules } = run(
3123
- `// theme-allow query-dual-import — the persister API has no basalt re-export yet\n` +
3124
- `import { useQuery } from '@tanstack/react-query'\n` +
3125
- `import { unwrap } from 'basalt-ui/query'\n` +
3126
- `export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(g()) })\n`,
3127
- )
3128
- expect(code).toBe(0)
3129
- expect(rules).not.toContain('query-dual-import')
3130
- })
3131
- })
3052
+ // `basalt/query-dual-import` is RETIRED (`RETIRED_RULE_IDS`) — its own describe block was removed
3053
+ // along with the rule; see `KNOWN_RULE_IDS` below for the coverage that remains: it is still a
3054
+ // valid (dead) `theme-allow` id, never a typo.
3132
3055
 
3133
3056
  // ── query-fn-unwrap (F5) ─────────────────────────────────────────────────────
3134
3057
 
@@ -42,7 +42,6 @@
42
42
  "basalt/search-literal-link": "error",
43
43
  "basalt/use-search-from-literal": "error",
44
44
  "basalt/provider-above-router": "warn",
45
- "basalt/query-dual-import": "warn",
46
45
  "basalt/query-fn-unwrap": "warn",
47
46
  "basalt/deprecated-export": "warn",
48
47
  "basalt/forms-field-key": "warn",
@@ -3,9 +3,7 @@ import type { BasaltProps } from './common/props.js';
3
3
  import type { ThemeLabControlsProps } from './theme-lab/index.js';
4
4
  /** Which panel is open. `null` renders nothing (the dock is unmounted, not hidden). */
5
5
  export type BasaltDevDockTool = 'router' | 'query' | 'theme';
6
- type DevDockRouter = {
7
- __routerHasBeenSetup?: unknown;
8
- } | Record<string, unknown>;
6
+ type DevDockRouter = object;
9
7
  export type BasaltDevDockProps = BasaltProps & {
10
8
  /** Which panel to show. `null` renders nothing. */
11
9
  tool: BasaltDevDockTool | null;
@@ -1 +1 @@
1
- {"version":3,"file":"dev-dock.d.ts","sourceRoot":"","sources":["../src/dev-dock.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD,uFAAuF;AACvF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAY5D,KAAK,aAAa,GAAG;IAAE,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAuBjF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG;IAC7C,mDAAmD;IACnD,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC9B;oFACgF;IAChF,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;8DAC0D;IAC1D,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,qBAAqB,CAAA;CACjC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,KAAK,GACN,EAAE,kBAAkB,GAAG,SAAS,CA8DhC"}
1
+ {"version":3,"file":"dev-dock.d.ts","sourceRoot":"","sources":["../src/dev-dock.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD,uFAAuF;AACvF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;AAe5D,KAAK,aAAa,GAAG,MAAM,CAAA;AAuB3B,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG;IAC7C,mDAAmD;IACnD,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC9B;oFACgF;IAChF,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;8DAC0D;IAC1D,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,qBAAqB,CAAA;CACjC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,KAAK,GACN,EAAE,kBAAkB,GAAG,SAAS,CA8DhC"}
@@ -28,7 +28,7 @@ export type UseFormSubmitOptions<Values extends Record<string, unknown>> = {
28
28
  onError?: (err: unknown) => void;
29
29
  /**
30
30
  * Turns a thrown value into the message `submitError` carries. Defaults to `toErrorMessage`
31
- * (`basalt-ui/query`), which already unwraps `Error`, `{ message }` and Eden's `{ status, value }`.
31
+ * (root barrel), which already unwraps `Error`, `{ message }` and Eden's `{ status, value }`.
32
32
  * Override it to brand the copy or to branch on `errorStatus(err)`.
33
33
  */
34
34
  mapError?: (err: unknown) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"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"}
@@ -107,8 +107,33 @@ const AXIS_WRAPPER_FILE = /(?:^|\/)Axes\.tsx$/;
107
107
  const RADIUS_PROP_RE = /\bradius=(?:\{[0-9]+\}|"[0-9]+")/g;
108
108
  const MOTION_TRANSITION_NUMERIC = /\btransition\s*=\s*\{\{[^}]*\b(?:duration|stiffness|damping|mass)\s*:\s*-?\d/g;
109
109
  const MOTION_TRANSITION_EASE_ARRAY = /\btransition\s*=\s*\{\{[^}]*\bease\s*:\s*\[/g;
110
- const CHART_ENTRY_POINT_TAG = /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g;
110
+ const CHART_ENTRY_POINT_TAG_START = /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?/g;
111
111
  const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/;
112
+ function scanChartTagClose(codeText, start) {
113
+ let depth = 0;
114
+ let i = start;
115
+ while (i < codeText.length) {
116
+ const ch = codeText[i];
117
+ if (ch === '"' || ch === "'" || ch === "`") {
118
+ const quote = ch;
119
+ i++;
120
+ while (i < codeText.length && codeText[i] !== quote) {
121
+ i += codeText[i] === "\\" ? 2 : 1;
122
+ }
123
+ i++;
124
+ continue;
125
+ }
126
+ if (ch === "{") {
127
+ depth++;
128
+ } else if (ch === "}") {
129
+ depth = Math.max(0, depth - 1);
130
+ } else if (ch === ">" && depth === 0) {
131
+ return i;
132
+ }
133
+ i++;
134
+ }
135
+ return -1;
136
+ }
112
137
  const LOCAL_COMPONENT_DEF = /(?:^|[\n;}])\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Z]\w*)/g;
113
138
  const BASALT_NAMED_IMPORT = /import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]basalt-ui(?:\/[^'"]*)?['"]/g;
114
139
  function localComponentNames(codeText) {
@@ -265,7 +290,6 @@ const PLUGIN_RULE_IDS = /* @__PURE__ */ new Set([
265
290
  // `checkSource`'s reach: provenance here is an import graph and a JSX ancestry, neither of which
266
291
  // a 12-line regex window has. Same reason `bound-control-outside-home` has no text-lane twin.
267
292
  "provider-above-router",
268
- "query-dual-import",
269
293
  "query-fn-unwrap",
270
294
  "deprecated-export",
271
295
  // Same lane: `{...inputProps(…)}` without a sibling `key` is a JSX-ancestry claim (does this
@@ -291,7 +315,11 @@ const PLUGIN_RULE_IDS = /* @__PURE__ */ new Set([
291
315
  // Retired plugin rule — `<BasaltNotifications />` mounted beside `<BasaltOverlays
292
316
  // notifications>` is unreachable now that `BasaltNotifications` no longer exists as a
293
317
  // standalone export. Mirrored by the plugin's own `RETIRED_RULE_IDS`.
294
- "duplicate-notifications-mount"
318
+ "duplicate-notifications-mount",
319
+ // Retired plugin rule — its whole premise (a raw `@tanstack/react-query` import beside
320
+ // `basalt-ui/query`) is unreachable now that C1 (1.29.0) dropped the `./query` subpath.
321
+ // Mirrored by the plugin's own `RETIRED_RULE_IDS`.
322
+ "query-dual-import"
295
323
  ]);
296
324
  const MIN_ALLOW_REASON_LENGTH = 4;
297
325
  function parseAllowAnnotation(rest, scope) {
@@ -778,7 +806,7 @@ const GUARD_RULES = {
778
806
  },
779
807
  "chart-missing-aria-label": {
780
808
  kind: "chart-missing-aria-label",
781
- pattern: CHART_ENTRY_POINT_TAG,
809
+ pattern: CHART_ENTRY_POINT_TAG_START,
782
810
  // handled inline (full-text tag-scoped scan); entry keeps registry complete
783
811
  enabled: (cfg) => cfg.chartMissingAriaLabel,
784
812
  // JSX-tag-shaped (`<MultiLine …>`) — never appears in CSS text.
@@ -1061,11 +1089,14 @@ function checkSource(text, relPath, cfg) {
1061
1089
  const basaltNames = ariaLabelRuns ? basaltImportedNames(codeText) : /* @__PURE__ */ new Set();
1062
1090
  const isShippedTag = (tag) => basaltNames.has(tag) || !localNames.has(tag);
1063
1091
  if (ariaLabelRuns) {
1064
- for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG)) {
1065
- const tagText = m[0];
1092
+ for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG_START)) {
1093
+ const matchStart = m.index ?? 0;
1066
1094
  if (!isShippedTag(m[1])) continue;
1095
+ const closeIdx = scanChartTagClose(codeText, matchStart + m[0].length);
1096
+ if (closeIdx === -1) continue;
1097
+ const tagText = codeText.slice(matchStart, closeIdx + 1);
1067
1098
  if (HAS_ARIA_LABEL_PROP.test(tagText)) continue;
1068
- const startLine = codeText.slice(0, m.index ?? 0).split("\n").length;
1099
+ const startLine = codeText.slice(0, matchStart).split("\n").length;
1069
1100
  const endLine = startLine + (tagText.split("\n").length - 1);
1070
1101
  if (isAllowedInRange(startLine, endLine, "chart-missing-aria-label")) continue;
1071
1102
  findings.push({
@@ -34,11 +34,23 @@ export declare function createBasaltQueryClient(config?: QueryClientConfig): Que
34
34
  * wrapper that leaves `data` unset on a miss — rather than explicitly `null` — is caught the same
35
35
  * way; the two envelope shapes argo's own pre-basalt `unwrap` and basalt's ONLY differed on.
36
36
  *
37
- * **Two overloads, one name** — a Promise of the envelope (basalt's original shape,
38
- * `queryFn: () => unwrap(api.x.get())`), or the ALREADY-RESOLVED envelope itself
39
- * (`unwrap(await api.x.get())`, or passed directly as a `.then(unwrap)` callback — both argo's
40
- * 97 call sites' shapes). Never a generic argument to disambiguate: TS infers the right overload
41
- * from whether the argument IS a `Promise`.
37
+ * **One generic signature, a conditional return** — a Promise of the envelope (basalt's original
38
+ * shape, `queryFn: () => unwrap(api.x.get())`) or the ALREADY-RESOLVED envelope itself
39
+ * (`unwrap(await api.x.get())`), both resolve `TData` off the same conditional. The two-overload
40
+ * form this replaced (1.29.0) resolved `unwrap` to `unknown` whenever it was passed as a bare
41
+ * callback — `.then(unwrap)` never applies an overload, it just reads the declared type of the
42
+ * `unwrap` reference — so `api.threads.get().then(unwrap)` silently widened. A single generic
43
+ * signature has exactly one type to read regardless of how the reference is used.
44
+ *
45
+ * **The conditional infers the raw `data` type and strips `null`/`undefined` from it**, rather
46
+ * than inferring the already-narrowed `Envelope<TData>`'s `TData` directly — Eden Treaty's
47
+ * response type is a UNION of envelope shapes (`{ data: T; error: null } | { data: null; error:
48
+ * EdenFetchError }`), and inferring through `Envelope<infer TData>` reads `TData` off whichever
49
+ * union member the conditional happens to match, landing on `T | null` at every Eden call site. A
50
+ * conditional type distributes over a union input when the checked type is a bare type parameter,
51
+ * so inferring `D` from `{ data: infer D; error: unknown }` and wrapping the result in
52
+ * `NonNullable` runs the check per union member and unions the results back together —
53
+ * `NonNullable<T> | NonNullable<null>` collapses to `T`.
42
54
  *
43
55
  * @example
44
56
  * import { unwrap } from 'basalt-ui'
@@ -48,16 +60,26 @@ export declare function createBasaltQueryClient(config?: QueryClientConfig): Que
48
60
  * // mutation:
49
61
  * mutationFn: (body) => unwrap(api.resource.post({ body }))
50
62
  *
51
- * // resolved-envelope — either shape:
63
+ * // resolved-envelope — either shape, including a bare `.then(unwrap)` callback:
52
64
  * const rows = unwrap(await api.users.get())
53
65
  * const rows2 = await api.users.get().then(unwrap)
54
66
  */
55
- export declare function unwrap<TData>(response: Promise<{
67
+ export declare function unwrap<R extends Envelope | Promise<Envelope>>(response: R): UnwrapResult<R>;
68
+ /** Transport-agnostic `{ data, error }` result shape — Eden Treaty, a raw fetch wrapper, etc. */
69
+ type Envelope<TData = unknown> = {
56
70
  data: TData | null | undefined;
57
71
  error: unknown;
58
- }>): Promise<TData>;
59
- export declare function unwrap<TData>(response: {
60
- data: TData | null | undefined;
72
+ };
73
+ /** `unwrap`'s conditional return: a `Promise<TData>` when `R` is a promised envelope, `TData` when
74
+ * it is an already-resolved one — inferred off the SAME `R` the overload used to split on, and
75
+ * distributing over a union `R` (see the JSDoc above) so an Eden Treaty response's null branch
76
+ * never leaks into the resolved type. */
77
+ type UnwrapResult<R> = R extends Promise<{
78
+ data: infer TData;
79
+ error: unknown;
80
+ }> ? Promise<NonNullable<TData>> : R extends {
81
+ data: infer TData;
61
82
  error: unknown;
62
- }): TData;
83
+ } ? NonNullable<TData> : never;
84
+ export {};
63
85
  //# sourceMappingURL=query-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query-client.d.ts","sourceRoot":"","sources":["../src/query-client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAW9D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,WAAW,CAQ/E;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAC1B,QAAQ,EAAE,OAAO,CAAC;IAAE,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,GACpE,OAAO,CAAC,KAAK,CAAC,CAAA;AACjB,wBAAgB,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE;IAAE,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAA"}
1
+ {"version":3,"file":"query-client.d.ts","sourceRoot":"","sources":["../src/query-client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAW9D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,WAAW,CAQ/E;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAK3F;AAED,iGAAiG;AACjG,KAAK,QAAQ,CAAC,KAAK,GAAG,OAAO,IAAI;IAAE,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAEnF;;;yCAGyC;AACzC,KAAK,YAAY,CAAC,CAAC,IACjB,CAAC,SAAS,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,GACpD,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,GAC3B,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC7C,WAAW,CAAC,KAAK,CAAC,GAClB,KAAK,CAAA"}
@@ -15,7 +15,9 @@ function createBasaltQueryClient(config) {
15
15
  });
16
16
  }
17
17
  function unwrap(response) {
18
- if (response instanceof Promise) return response.then((envelope) => unwrapEnvelope(envelope));
18
+ if (response instanceof Promise) {
19
+ return response.then((envelope) => unwrapEnvelope(envelope));
20
+ }
19
21
  return unwrapEnvelope(response);
20
22
  }
21
23
  function unwrapEnvelope(envelope) {
@@ -32,7 +32,7 @@ export type RuleName = 'tokens' | 'charts' | 'mantine' | 'state' | 'controls' |
32
32
  * const id: PluginRuleId = 'hand-rolled-filter' // ok
33
33
  * // const bad: PluginRuleId = 'hand-rolled-filters' // tsc error
34
34
  */
35
- export declare const PLUGIN_RULE_ID_LIST: readonly ["no-raw-font-size", "raw-size-literal", "card-inset", "chart-in-raw-surface", "hand-rolled-plot", "chart-legend-literal", "shadow-basalt-export", "hand-rolled-shell", "raw-scroll-container", "hand-rolled-filter", "control-outside-home", "bound-control-outside-home", "control-size-literal", "page-bar-budget", "in-body-page-title", "responsive-twin", "search-literal-link", "use-search-from-literal", "provider-above-router", "query-dual-import", "query-fn-unwrap", "deprecated-export", "forms-field-key", "visx-boundary", "visx-tooltip", "token-layer-boundary", "agent-resume-guard", "agent-no-raw-usechat", "ai-sdk-major", "no-import-meta-env"];
35
+ export declare const PLUGIN_RULE_ID_LIST: readonly ["no-raw-font-size", "raw-size-literal", "card-inset", "chart-in-raw-surface", "hand-rolled-plot", "chart-legend-literal", "shadow-basalt-export", "hand-rolled-shell", "raw-scroll-container", "hand-rolled-filter", "control-outside-home", "bound-control-outside-home", "control-size-literal", "page-bar-budget", "in-body-page-title", "responsive-twin", "search-literal-link", "use-search-from-literal", "provider-above-router", "query-fn-unwrap", "deprecated-export", "forms-field-key", "visx-boundary", "visx-tooltip", "token-layer-boundary", "agent-resume-guard", "agent-no-raw-usechat", "ai-sdk-major", "no-import-meta-env"];
36
36
  /** One registered oxlint plugin rule id — the literal union of {@link PLUGIN_RULE_ID_LIST}. */
37
37
  export type PluginRuleId = (typeof PLUGIN_RULE_ID_LIST)[number];
38
38
  /** The 3 shipped skill names (agent/skills/basalt-{name}/SKILL.md, placed into a consumer's
@@ -424,7 +424,7 @@ export declare const SURFACES: {
424
424
  readonly rule: "batteries";
425
425
  readonly skill: readonly ["basalt-app"];
426
426
  readonly guardKinds: readonly [];
427
- readonly pluginRules: readonly ["query-dual-import", "query-fn-unwrap"];
427
+ readonly pluginRules: readonly ["query-fn-unwrap"];
428
428
  readonly globs: {
429
429
  readonly shipped: readonly ["**/*.{ts,tsx}"];
430
430
  readonly repo: readonly ["packages/basalt-ui/src/**"];
@@ -1 +1 @@
1
- {"version":3,"file":"surfaces.d.ts","sourceRoot":"","sources":["../src/surfaces.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAI9C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,CAAA;AAE3F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,kpBA+BtB,CAAA;AAEV,+FAA+F;AAC/F,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,eAAe,GAAG,eAAe,CAAA;AAExE;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,iBAAiB,GAAG,UAAU,GAAG,YAAY,GAAG,cAAc,GAAG,eAAe,CAAA;AAEpG;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAA;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAA;CACzB,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CACrC,CAAA;AAID,KAAK,WAAW,GAAG;IACjB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAA;IACrD,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IAChD;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;QACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;KACjC,CAAA;IACD;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3C,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG;IACvC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,iGAAiG;IACjG,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IACpC,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAA;IACzC;;;;;;;;;OASG;IACH,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAA;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1C,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAA;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,CAAA;CAC9B,CAAA;AAED,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,WAAW,CAAA;AA8BpD;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2c2B,CAAA;AAIhD;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,qBAMiB,CAAA;AAIxC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,sBAMiB,CAAA;AAIzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAAqC,CAAA;AAEnG;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACzC,OAAO,CAET"}
1
+ {"version":3,"file":"surfaces.d.ts","sourceRoot":"","sources":["../src/surfaces.ts"],"names":[],"mappings":"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"}
package/dist/surfaces.js CHANGED
@@ -18,7 +18,6 @@ const PLUGIN_RULE_ID_LIST = [
18
18
  "search-literal-link",
19
19
  "use-search-from-literal",
20
20
  "provider-above-router",
21
- "query-dual-import",
22
21
  "query-fn-unwrap",
23
22
  "deprecated-export",
24
23
  "forms-field-key",
@@ -456,10 +455,12 @@ const SURFACES = {
456
455
  rule: "batteries",
457
456
  skill: ["basalt-app"],
458
457
  guardKinds: [],
459
- // query-dual-import / query-fn-unwrap: reassigned here from the now-dropped `./query` surface
460
- // (C1 consolidation — createBasaltQueryClient/unwrap/BasaltQueryDevtools are root barrel
461
- // exports now, but their doctrine stays `batteries` — "use the shipped thing" — not `mantine`).
462
- pluginRules: ["query-dual-import", "query-fn-unwrap"],
458
+ // query-fn-unwrap: reassigned here from the now-dropped `./query` surface (C1 consolidation —
459
+ // createBasaltQueryClient/unwrap/BasaltQueryDevtools are root barrel exports now, but their
460
+ // doctrine stays `batteries` — "use the shipped thing" — not `mantine`). `query-dual-import`
461
+ // was retired alongside it — its premise (a raw `@tanstack/react-query` import beside
462
+ // `basalt-ui/query`) is unreachable now that the `./query` subpath is gone.
463
+ pluginRules: ["query-fn-unwrap"],
463
464
  globs: {
464
465
  // Shipped is a catch-all (not just src/**+app/**) so consumer code under components/, lib/,
465
466
  // features/, etc. is also covered. The @visx/*-only-in-charts and Mantine-free charts/tokens
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.29.0
1
+ # basalt-ui v1.29.2
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.29.0
22
+ Version: 1.29.2
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.29.0",
3
+ "version": "1.29.2",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
package/src/dev-dock.tsx CHANGED
@@ -42,11 +42,14 @@ const TOOL_TITLE: Record<BasaltDevDockTool, string> = {
42
42
  theme: 'Theme Lab',
43
43
  }
44
44
 
45
- // Minimal structural shape of a TanStack `Router` instance — avoids a hard, eager type
46
- // dependency on `@tanstack/react-router` for a consumer who never renders the `'router'` tool.
47
- // `TanStackRouterDevtoolsPanel` itself takes the real (much wider) `AnyRouter` type; this
48
- // structural subset is only what THIS component threads through as a prop.
49
- type DevDockRouter = { __routerHasBeenSetup?: unknown } | Record<string, unknown>
45
+ // Any non-null object — avoids a hard, eager type dependency on `@tanstack/react-router` for a
46
+ // consumer who never renders the `'router'` tool, while still accepting a REAL `Router` class
47
+ // instance with no cast. A narrower structural type (matching a `Router` field, or falling back
48
+ // to `Record<string, unknown>`) used to reject that instance under TS weak-type detection: a
49
+ // class instance declares no index signature, so it has no excess-property overlap with either
50
+ // branch. `TanStackRouterDevtoolsPanel` itself takes the real (much wider) `AnyRouter` type; the
51
+ // devtools panel is the only reader of this prop, so `object` is exactly as wide as it needs to be.
52
+ type DevDockRouter = object
50
53
 
51
54
  const LazyRouterDevtoolsPanel = lazy(() =>
52
55
  import('@tanstack/react-router-devtools').then((m) => ({
@@ -135,8 +138,8 @@ export function BasaltDevDock({
135
138
  {tool === 'router' && router !== undefined && (
136
139
  <LazyRouterDevtoolsPanel
137
140
  // The devtools panel's own `router` prop takes the real `AnyRouter` type;
138
- // `DevDockRouter` is deliberately a narrower structural stand-in so this module
139
- // never eagerly imports the router's types.
141
+ // `DevDockRouter` is deliberately the widest structural stand-in (`object`) so this
142
+ // module never eagerly imports the router's types.
140
143
  // oxlint-disable-next-line typescript/no-explicit-any -- see above
141
144
  router={router as any}
142
145
  isOpen
@@ -82,7 +82,7 @@ export type UseFormSubmitOptions<Values extends Record<string, unknown>> = {
82
82
  onError?: (err: unknown) => void
83
83
  /**
84
84
  * Turns a thrown value into the message `submitError` carries. Defaults to `toErrorMessage`
85
- * (`basalt-ui/query`), which already unwraps `Error`, `{ message }` and Eden's `{ status, value }`.
85
+ * (root barrel), which already unwraps `Error`, `{ message }` and Eden's `{ status, value }`.
86
86
  * Override it to brand the copy or to branch on `errorStatus(err)`.
87
87
  */
88
88
  mapError?: (err: unknown) => string
@@ -154,8 +154,8 @@ const INLINE_FONT_SIZE =
154
154
  // A `Card` / `Paper` opening tag carrying `withBorder`. Card depth is `--vx-shadow-card`, whose 1px
155
155
  // ring lives INSIDE the shadow value; `withBorder` therefore adds a SECOND, real `border` property
156
156
  // on top of it (the theme's `styles.root` pins bg/shadow/radius but never clears `border`), and the
157
- // card reads heavy/boxed. Bounded full-text tag scan, same shape as CHART_ENTRY_POINT_TAG below, so
158
- // a multi-line-formatted tag still resolves to one match.
157
+ // card reads heavy/boxed. Bounded full-text tag scan, same shape as CHART_ENTRY_POINT_TAG_START below,
158
+ // so a multi-line-formatted tag still resolves to one match.
159
159
  //
160
160
  // Two deliberate non-matches:
161
161
  // • `<Card.Section withBorder>` — a section DIVIDER, not card depth. Excluded by the `(?![\w.])`
@@ -409,18 +409,49 @@ const MOTION_TRANSITION_NUMERIC =
409
409
  /\btransition\s*=\s*\{\{[^}]*\b(?:duration|stiffness|damping|mass)\s*:\s*-?\d/g
410
410
  const MOTION_TRANSITION_EASE_ARRAY = /\btransition\s*=\s*\{\{[^}]*\bease\s*:\s*\[/g
411
411
 
412
- // A chart entry-point JSX tag (the 7 kinds + 2 sparklines) — full opening/self-closing tag,
413
- // scanned for a missing `ariaLabel` prop (an accessible text alternative for the SVG graphic).
414
- // Bounded, full-text scan so a multi-line-formatted tag still resolves to one match. The scan must
415
- // survive two `>` decoys inside an opening tag: an explicit JSX generic argument
416
- // (`<MultiLine<Point>`) — consumed by the optional `<[^<>]*>` group — and arrow functions in prop
417
- // expressions (`getX={(d) => d.date}`) — consumed atomically by the `=>` alternative so their `>`
418
- // never terminates the tag early. A bare `>` comparison inside a prop expression still ends the
419
- // match (accepted limitation of the bounded scan).
420
- const CHART_ENTRY_POINT_TAG =
421
- /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
412
+ // A chart entry-point JSX tag (the 7 kinds + 2 sparklines) — matches the opening `<Name` plus an
413
+ // optional explicit JSX generic argument (`<MultiLine<Point>`); the rest of the tag (up to its
414
+ // closing `>`) is resolved by `scanChartTagClose` below, brace-depth aware, so it survives every
415
+ // `>`-shaped decoy inside a prop expression — arrow functions (`=>`), comparisons (`>=`, `<=`),
416
+ // shifts (`>>`) — without needing to enumerate them: a `>` only closes the tag outside every `{…}`.
417
+ const CHART_ENTRY_POINT_TAG_START =
418
+ /<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?/g
422
419
  const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/
423
420
 
421
+ /**
422
+ * Scans forward from `start` (immediately after a chart tag's name/generic-args) for the `>` that
423
+ * closes the JSX tag. Curly-brace depth aware: a `>` only closes the tag at depth 0, so anything
424
+ * living inside a `{…}` prop expression — `=>`, `>=`, `<=`, `>>`, a nested JSX child's own `>` —
425
+ * never ends the tag early. String/template literals are skipped verbatim so a quoted `>`
426
+ * cannot perturb the depth count either. Returns the index of the closing `>`, or -1 if the tag
427
+ * never closes (malformed/truncated source — the caller treats that as no match).
428
+ */
429
+ function scanChartTagClose(codeText: string, start: number): number {
430
+ let depth = 0
431
+ let i = start
432
+ while (i < codeText.length) {
433
+ const ch = codeText[i]
434
+ if (ch === '"' || ch === "'" || ch === '`') {
435
+ const quote = ch
436
+ i++
437
+ while (i < codeText.length && codeText[i] !== quote) {
438
+ i += codeText[i] === '\\' ? 2 : 1
439
+ }
440
+ i++
441
+ continue
442
+ }
443
+ if (ch === '{') {
444
+ depth++
445
+ } else if (ch === '}') {
446
+ depth = Math.max(0, depth - 1)
447
+ } else if (ch === '>' && depth === 0) {
448
+ return i
449
+ }
450
+ i++
451
+ }
452
+ return -1
453
+ }
454
+
424
455
  // ── Tag provenance, shared by chart-missing-aria-label ────────────────────────────────────────────
425
456
  //
426
457
  // `chart-missing-aria-label` keys on a JSX tag NAME, which is the whole of its signal — so a
@@ -472,13 +503,13 @@ function basaltImportedNames(codeText: string): ReadonlySet<string> {
472
503
  const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g
473
504
 
474
505
  // A raw form-control's own opening tag — bounded full-text scan (same shape as CARD_SURFACE_TAG /
475
- // CHART_ENTRY_POINT_TAG), used ONLY by sub-16-input-font to search the tag's own inline `style`
506
+ // CHART_ENTRY_POINT_TAG_START), used ONLY by sub-16-input-font to search the tag's own inline `style`
476
507
  // for a sub-floor fontSize.
477
508
  const RAW_FORM_CONTROL_TAG = /<(?:input|select|textarea)\b(?:=>|[^>])*?>/g
478
509
 
479
510
  // ── Control-home patterns (docs/CONTROLS-SPEC.md §6 — the text lane of laws C1/C8) ──────────────
480
511
  //
481
- // Bounded full-text tag scans, the CARD_SURFACE_TAG / CHART_ENTRY_POINT_TAG shape: a `<Title>` and a
512
+ // Bounded full-text tag scans, the CARD_SURFACE_TAG / CHART_ENTRY_POINT_TAG_START shape: a `<Title>` and a
482
513
  // `<Select>` are both routinely formatted across lines, so a per-line regex would see neither.
483
514
 
484
515
  /** A `<Title …>` opening tag, however it is wrapped. `order` is judged separately, on the tag text. */
@@ -877,7 +908,6 @@ export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
877
908
  // `checkSource`'s reach: provenance here is an import graph and a JSX ancestry, neither of which
878
909
  // a 12-line regex window has. Same reason `bound-control-outside-home` has no text-lane twin.
879
910
  'provider-above-router',
880
- 'query-dual-import',
881
911
  'query-fn-unwrap',
882
912
  'deprecated-export',
883
913
  // Same lane: `{...inputProps(…)}` without a sibling `key` is a JSX-ancestry claim (does this
@@ -904,6 +934,10 @@ export const PLUGIN_RULE_IDS: ReadonlySet<string> = new Set([
904
934
  // notifications>` is unreachable now that `BasaltNotifications` no longer exists as a
905
935
  // standalone export. Mirrored by the plugin's own `RETIRED_RULE_IDS`.
906
936
  'duplicate-notifications-mount',
937
+ // Retired plugin rule — its whole premise (a raw `@tanstack/react-query` import beside
938
+ // `basalt-ui/query`) is unreachable now that C1 (1.29.0) dropped the `./query` subpath.
939
+ // Mirrored by the plugin's own `RETIRED_RULE_IDS`.
940
+ 'query-dual-import',
907
941
  ])
908
942
 
909
943
  /** The shortest string accepted as a written reason — enough to exclude a stray separator. */
@@ -1901,7 +1935,7 @@ export const GUARD_RULES = {
1901
1935
  },
1902
1936
  'chart-missing-aria-label': {
1903
1937
  kind: 'chart-missing-aria-label',
1904
- pattern: CHART_ENTRY_POINT_TAG, // handled inline (full-text tag-scoped scan); entry keeps registry complete
1938
+ pattern: CHART_ENTRY_POINT_TAG_START, // handled inline (full-text tag-scoped scan); entry keeps registry complete
1905
1939
  enabled: (cfg: GuardConfig) => cfg.chartMissingAriaLabel,
1906
1940
  // JSX-tag-shaped (`<MultiLine …>`) — never appears in CSS text.
1907
1941
  appliesTo: (relPath) => !relPath.endsWith('.css'),
@@ -2349,12 +2383,15 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
2349
2383
  // that is missing the prop. The allow-comment is honored anywhere in the tag's span, so a
2350
2384
  // `theme-allow` that used to sit on the closing line keeps working.
2351
2385
  if (ariaLabelRuns) {
2352
- for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG)) {
2353
- const tagText = m[0]
2386
+ for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG_START)) {
2387
+ const matchStart = m.index ?? 0
2354
2388
  // A tag this file defines itself is not the shipped kind — it does not take `ariaLabel`.
2355
2389
  if (!isShippedTag(m[1] as string)) continue
2390
+ const closeIdx = scanChartTagClose(codeText, matchStart + m[0].length)
2391
+ if (closeIdx === -1) continue
2392
+ const tagText = codeText.slice(matchStart, closeIdx + 1)
2356
2393
  if (HAS_ARIA_LABEL_PROP.test(tagText)) continue
2357
- const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
2394
+ const startLine = codeText.slice(0, matchStart).split('\n').length
2358
2395
  const endLine = startLine + (tagText.split('\n').length - 1)
2359
2396
  if (isAllowedInRange(startLine, endLine, 'chart-missing-aria-label')) continue
2360
2397
  findings.push({
@@ -55,11 +55,23 @@ export function createBasaltQueryClient(config?: QueryClientConfig): QueryClient
55
55
  * wrapper that leaves `data` unset on a miss — rather than explicitly `null` — is caught the same
56
56
  * way; the two envelope shapes argo's own pre-basalt `unwrap` and basalt's ONLY differed on.
57
57
  *
58
- * **Two overloads, one name** — a Promise of the envelope (basalt's original shape,
59
- * `queryFn: () => unwrap(api.x.get())`), or the ALREADY-RESOLVED envelope itself
60
- * (`unwrap(await api.x.get())`, or passed directly as a `.then(unwrap)` callback — both argo's
61
- * 97 call sites' shapes). Never a generic argument to disambiguate: TS infers the right overload
62
- * from whether the argument IS a `Promise`.
58
+ * **One generic signature, a conditional return** — a Promise of the envelope (basalt's original
59
+ * shape, `queryFn: () => unwrap(api.x.get())`) or the ALREADY-RESOLVED envelope itself
60
+ * (`unwrap(await api.x.get())`), both resolve `TData` off the same conditional. The two-overload
61
+ * form this replaced (1.29.0) resolved `unwrap` to `unknown` whenever it was passed as a bare
62
+ * callback — `.then(unwrap)` never applies an overload, it just reads the declared type of the
63
+ * `unwrap` reference — so `api.threads.get().then(unwrap)` silently widened. A single generic
64
+ * signature has exactly one type to read regardless of how the reference is used.
65
+ *
66
+ * **The conditional infers the raw `data` type and strips `null`/`undefined` from it**, rather
67
+ * than inferring the already-narrowed `Envelope<TData>`'s `TData` directly — Eden Treaty's
68
+ * response type is a UNION of envelope shapes (`{ data: T; error: null } | { data: null; error:
69
+ * EdenFetchError }`), and inferring through `Envelope<infer TData>` reads `TData` off whichever
70
+ * union member the conditional happens to match, landing on `T | null` at every Eden call site. A
71
+ * conditional type distributes over a union input when the checked type is a bare type parameter,
72
+ * so inferring `D` from `{ data: infer D; error: unknown }` and wrapping the result in
73
+ * `NonNullable` runs the check per union member and unions the results back together —
74
+ * `NonNullable<T> | NonNullable<null>` collapses to `T`.
63
75
  *
64
76
  * @example
65
77
  * import { unwrap } from 'basalt-ui'
@@ -69,27 +81,32 @@ export function createBasaltQueryClient(config?: QueryClientConfig): QueryClient
69
81
  * // mutation:
70
82
  * mutationFn: (body) => unwrap(api.resource.post({ body }))
71
83
  *
72
- * // resolved-envelope — either shape:
84
+ * // resolved-envelope — either shape, including a bare `.then(unwrap)` callback:
73
85
  * const rows = unwrap(await api.users.get())
74
86
  * const rows2 = await api.users.get().then(unwrap)
75
87
  */
76
- export function unwrap<TData>(
77
- response: Promise<{ data: TData | null | undefined; error: unknown }>,
78
- ): Promise<TData>
79
- export function unwrap<TData>(response: { data: TData | null | undefined; error: unknown }): TData
80
- export function unwrap<TData>(
81
- response:
82
- | Promise<{ data: TData | null | undefined; error: unknown }>
83
- | { data: TData | null | undefined; error: unknown },
84
- ): TData | Promise<TData> {
85
- if (response instanceof Promise) return response.then((envelope) => unwrapEnvelope(envelope))
86
- return unwrapEnvelope(response)
88
+ export function unwrap<R extends Envelope | Promise<Envelope>>(response: R): UnwrapResult<R> {
89
+ if (response instanceof Promise) {
90
+ return response.then((envelope) => unwrapEnvelope(envelope)) as unknown as UnwrapResult<R>
91
+ }
92
+ return unwrapEnvelope(response as Envelope) as unknown as UnwrapResult<R>
87
93
  }
88
94
 
89
- function unwrapEnvelope<TData>(envelope: {
90
- data: TData | null | undefined
91
- error: unknown
92
- }): TData {
95
+ /** Transport-agnostic `{ data, error }` result shape — Eden Treaty, a raw fetch wrapper, etc. */
96
+ type Envelope<TData = unknown> = { data: TData | null | undefined; error: unknown }
97
+
98
+ /** `unwrap`'s conditional return: a `Promise<TData>` when `R` is a promised envelope, `TData` when
99
+ * it is an already-resolved one — inferred off the SAME `R` the overload used to split on, and
100
+ * distributing over a union `R` (see the JSDoc above) so an Eden Treaty response's null branch
101
+ * never leaks into the resolved type. */
102
+ type UnwrapResult<R> =
103
+ R extends Promise<{ data: infer TData; error: unknown }>
104
+ ? Promise<NonNullable<TData>>
105
+ : R extends { data: infer TData; error: unknown }
106
+ ? NonNullable<TData>
107
+ : never
108
+
109
+ function unwrapEnvelope<TData>(envelope: Envelope<TData>): TData {
93
110
  const { data, error } = envelope
94
111
  if (error) throw error
95
112
  if (data === null || data === undefined)
package/src/surfaces.ts CHANGED
@@ -56,7 +56,6 @@ export const PLUGIN_RULE_ID_LIST = [
56
56
  'search-literal-link',
57
57
  'use-search-from-literal',
58
58
  'provider-above-router',
59
- 'query-dual-import',
60
59
  'query-fn-unwrap',
61
60
  'deprecated-export',
62
61
  'forms-field-key',
@@ -672,10 +671,12 @@ export const SURFACES = {
672
671
  rule: 'batteries',
673
672
  skill: ['basalt-app'],
674
673
  guardKinds: [],
675
- // query-dual-import / query-fn-unwrap: reassigned here from the now-dropped `./query` surface
676
- // (C1 consolidation — createBasaltQueryClient/unwrap/BasaltQueryDevtools are root barrel
677
- // exports now, but their doctrine stays `batteries` — "use the shipped thing" — not `mantine`).
678
- pluginRules: ['query-dual-import', 'query-fn-unwrap'],
674
+ // query-fn-unwrap: reassigned here from the now-dropped `./query` surface (C1 consolidation —
675
+ // createBasaltQueryClient/unwrap/BasaltQueryDevtools are root barrel exports now, but their
676
+ // doctrine stays `batteries` — "use the shipped thing" — not `mantine`). `query-dual-import`
677
+ // was retired alongside it — its premise (a raw `@tanstack/react-query` import beside
678
+ // `basalt-ui/query`) is unreachable now that the `./query` subpath is gone.
679
+ pluginRules: ['query-fn-unwrap'],
679
680
  globs: {
680
681
  // Shipped is a catch-all (not just src/**+app/**) so consumer code under components/, lib/,
681
682
  // features/, etc. is also covered. The @visx/*-only-in-charts and Mantine-free charts/tokens