From 01e455c8af932f4222ddc7001525ce705f34aaf5 Mon Sep 17 00:00:00 2001 From: m5r Date: Wed, 12 Aug 2026 22:12:18 +0200 Subject: [PATCH] milestone 23 s2: react aria primitives and their cluster --- licenses/clsx-mit.txt | 9 + licenses/dependency-identity.txt | 18 ++ licenses/inventory.zon | 37 ++- licenses/licenses.zig | 2 + licenses/react-aria-apache-2.0.txt | 25 ++ specs/milestone-23.md | 115 +++++++++ src/licenses_drift_test.zig | 112 ++++++-- web/package-lock.json | 134 +++++++++- web/package.json | 1 + .../blocklists/BlocklistsPage.test.tsx | 30 +++ .../features/blocklists/BlocklistsPage.tsx | 180 +++++++++---- web/src/features/clients/ClientEditDialog.tsx | 136 +++++----- web/src/features/clients/ClientsPage.test.tsx | 25 +- web/src/features/clients/ClientsPage.tsx | 172 +++++++++--- web/src/features/clients/PrefixesEditor.tsx | 117 +++++++-- web/src/features/local/LocalDnsPage.test.tsx | 89 ++++++- web/src/features/local/LocalDnsPage.tsx | 86 ++---- web/src/features/local/RecordsTab.tsx | 218 +++++++++++----- web/src/features/local/ZonesTab.tsx | 185 ++++++++++--- web/src/features/rules/RulesPage.test.tsx | 73 +++++- web/src/features/rules/RulesPage.tsx | 244 +++++++++++------- web/src/features/upstreams/UpstreamForm.tsx | 79 ++++-- .../features/upstreams/UpstreamsPage.test.tsx | 28 +- web/src/features/upstreams/UpstreamsPage.tsx | 130 ++++++++-- web/src/ui/ConfirmDialog.tsx | 113 ++++++++ web/src/ui/Dialog.tsx | 68 +++++ web/src/ui/Select.tsx | 140 ++++++++++ web/src/ui/Tabs.tsx | 104 ++++++++ web/src/ui/useCrudForm.test.tsx | 77 +++++- web/src/ui/useCrudForm.ts | 42 ++- web/vite.config.ts | 10 +- web/vitest.setup.ts | 49 ++++ 32 files changed, 2316 insertions(+), 532 deletions(-) create mode 100644 licenses/clsx-mit.txt create mode 100644 licenses/react-aria-apache-2.0.txt create mode 100644 web/src/ui/ConfirmDialog.tsx create mode 100644 web/src/ui/Dialog.tsx create mode 100644 web/src/ui/Select.tsx create mode 100644 web/src/ui/Tabs.tsx create mode 100644 web/vitest.setup.ts diff --git a/licenses/clsx-mit.txt b/licenses/clsx-mit.txt new file mode 100644 index 0000000..fa6089f --- /dev/null +++ b/licenses/clsx-mit.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/dependency-identity.txt b/licenses/dependency-identity.txt index 5286423..3b98541 100644 --- a/licenses/dependency-identity.txt +++ b/licenses/dependency-identity.txt @@ -39,7 +39,12 @@ mbedtls url=https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/mbedtls-3.6.7. sqlite url=https://sqlite.org/2026/sqlite-amalgamation-3530400.zip hash=N-V-__8AAGVtrgCcOcmjrOJnagmnRyMrcKaOo09KbU-vu8w8 [npm runtime closure] +@internationalized/date 3.12.3 Apache-2.0 +@internationalized/number 3.6.7 Apache-2.0 +@internationalized/string 3.2.10 Apache-2.0 +@react-types/shared 3.36.1 Apache-2.0 @stylexjs/stylex 0.19.0 MIT +@swc/helpers 0.5.23 Apache-2.0 @tanstack/history 1.162.0 MIT @tanstack/query-core 5.101.4 MIT @tanstack/react-query 5.101.4 MIT @@ -47,6 +52,9 @@ sqlite url=https://sqlite.org/2026/sqlite-amalgamation-3530400.zip hash=N-V-__8A @tanstack/react-store 0.9.3 MIT @tanstack/router-core 1.171.15 MIT @tanstack/store 0.9.3 MIT +aria-hidden 1.2.6 MIT +client-only 0.0.1 MIT +clsx 2.1.1 MIT cookie-es 3.1.1 MIT css-mediaquery 0.1.2 BSD invariant 2.2.4 MIT @@ -54,11 +62,15 @@ isbot 5.2.1 Unlicense js-tokens 4.0.0 MIT loose-envify 1.4.0 MIT react 19.2.8 MIT +react-aria 3.51.0 Apache-2.0 +react-aria-components 1.20.0 Apache-2.0 react-dom 19.2.8 MIT +react-stately 3.49.0 Apache-2.0 scheduler 0.27.0 MIT seroval 1.5.6 MIT seroval-plugins 1.5.6 MIT styleq 0.2.1 MIT +tslib 2.8.1 0BSD use-sync-external-store 1.6.0 MIT [npm build-time generators whose output ships] @@ -70,6 +82,8 @@ vite 8.1.5 MIT alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce [npm packages bundled into web/dist] +@internationalized/string +@stylexjs/stylex @tanstack/history @tanstack/query-core @tanstack/react-query @@ -77,7 +91,11 @@ alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695 @tanstack/react-store @tanstack/router-core @tanstack/store +clsx react +react-aria +react-aria-components react-dom +react-stately scheduler use-sync-external-store diff --git a/licenses/inventory.zon b/licenses/inventory.zon index ab7f018..d5b17f0 100644 --- a/licenses/inventory.zon +++ b/licenses/inventory.zon @@ -45,11 +45,26 @@ // sourcemap build of a component calling stylex.props shows none of them in any // `sources` list. // -// StyleX itself is inventoried below although nothing imports it yet, so no -// chunk carries it today. The licence is carried now because the same sourcemap -// build settles what the page conversion will land, and it is not the obvious -// answer: only @stylexjs/stylex appears as a bundled package, and styleq -// arrives inside it. The published lib/es/stylex.mjs vendors styleq's source +// Seven more joined that list with React Aria, and the sourcemap build is again +// what settled which: @internationalized/date, @internationalized/number, +// @react-types/shared, @swc/helpers, aria-hidden, client-only and tslib are in +// the runtime closure but contribute no module to any chunk. The date and number +// packages serve the calendar and number-field components ruling 4 keeps out of +// scope; @react-types/shared is types only and emits no runtime code; +// client-only is a build-time marker for React Server Components; @swc/helpers +// and tslib are downlevelling helpers the published ESM builds never reach for; +// and aria-hidden sits behind a react-aria path the four adopted components do +// not take. Their licences differ and are recorded one by one in +// src/licenses_drift_test.zig rather than lumped together: @internationalized/ +// date, @internationalized/number, @react-types/shared and @swc/helpers are +// Apache-2.0, tslib is 0BSD, aria-hidden and client-only are MIT. Keeping all +// seven out of the bundle is a fact worth re-checking rather than assuming, so +// the guard fails if any of them turns up in a `sources` list. +// +// StyleX now ships: the converted pages call stylex.props, and a sourcemap build +// puts @stylexjs/stylex in the bundle. What that build settles is not the +// obvious answer, though: only @stylexjs/stylex appears as a bundled package, +// and styleq arrives inside it. The published lib/es/stylex.mjs vendors styleq's source // instead of importing it, so styleq's bytes ship while styleq never appears in // a `sources` list — no guard would raise it, which is why it is written down // here. @@ -109,6 +124,18 @@ .note = "Bundled into the admin UI JavaScript. Grouped because all five carry the identical MIT text and copyright line.", .file = "tanstack-mit.txt", }, + .{ + .component = "React Aria (react-aria-components, react-aria, react-stately, @internationalized/string)", + .version = "react-aria-components 1.20.0, react-aria 3.51.0, react-stately 3.49.0, @internationalized/string 3.2.10", + .note = "The dialog, alert dialog, tab and select behaviour of the admin UI, bundled into the JavaScript embedded in the binary. The first Apache-2.0 npm dependency this project has taken, and the first non-MIT one: Mokhtar Mial accepted Apache-2.0 inbound for nxdns on 2026-08-12, which is the decision that let these four ship. Apache-2.0 Section 4 attribution is satisfied by carrying the licence text in THIRD-PARTY-NOTICES, which the file below does; none of the four ships a NOTICE file, so 4(d) adds nothing. All four carry a byte-identical LICENSE. In the tarballs and in the image.", + .file = "react-aria-apache-2.0.txt", + }, + .{ + .component = "clsx", + .version = "clsx 2.1.1", + .note = "The class-name joiner React Aria calls when it merges a render-prop class name with its own. Bundled into the admin UI JavaScript. Separate entry from the other MIT packages: same MIT text, different copyright line.", + .file = "clsx-mit.txt", + }, .{ .component = "StyleX", .version = "@stylexjs/stylex 0.19.0", diff --git a/licenses/licenses.zig b/licenses/licenses.zig index f1e3210..ef50b57 100644 --- a/licenses/licenses.zig +++ b/licenses/licenses.zig @@ -51,6 +51,8 @@ pub const texts: []const Text = &.{ .{ .name = "react-mit.txt", .body = @embedFile("react-mit.txt") }, .{ .name = "tanstack-mit.txt", .body = @embedFile("tanstack-mit.txt") }, .{ .name = "tanstack-store-mit.txt", .body = @embedFile("tanstack-store-mit.txt") }, + .{ .name = "react-aria-apache-2.0.txt", .body = @embedFile("react-aria-apache-2.0.txt") }, + .{ .name = "clsx-mit.txt", .body = @embedFile("clsx-mit.txt") }, .{ .name = "stylex-mit.txt", .body = @embedFile("stylex-mit.txt") }, .{ .name = "styleq-mit.txt", .body = @embedFile("styleq-mit.txt") }, .{ .name = "tailwindcss-mit.txt", .body = @embedFile("tailwindcss-mit.txt") }, diff --git a/licenses/react-aria-apache-2.0.txt b/licenses/react-aria-apache-2.0.txt new file mode 100644 index 0000000..05b8680 --- /dev/null +++ b/licenses/react-aria-apache-2.0.txt @@ -0,0 +1,25 @@ +React Aria: react-aria-components, react-aria, react-stately and +@internationalized/string, bundled into the admin UI JavaScript that is +embedded in every nxdns binary. + +Copyright 2019 Adobe +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +The full text of the Apache License, Version 2.0 is reproduced above, in the +Mbed TLS section of this file. The LICENSE file each of these four packages +ships is that same document: diffing it against the Mbed TLS copy shows one +differing line, the sample copyright inside the "APPENDIX: How to apply the +Apache License to your work" boilerplate, which is not part of the licence +terms. None of the four ships a NOTICE file, so Section 4(d) attaches nothing +further. diff --git a/specs/milestone-23.md b/specs/milestone-23.md index 1577c9d..05581dd 100644 --- a/specs/milestone-23.md +++ b/specs/milestone-23.md @@ -347,6 +347,121 @@ Tailwind dependency pair and its vite plugin line and CSS import. - [ ] `web/src/lib/types.ts` and `web/src/lib/contractSamples.gen.ts` are byte-identical to HEAD. +## Recorded (implementation) + +Written during S1 and the S2 hand-off. Each entry is a place the spec was wrong +or a decision the spec did not have. + +### S1 could not put StyleX in the bundle, and its acceptance says otherwise + +S1 owns no page file, so no production module imports StyleX and rollup emits +none of it. Two S1 boxes — "`npm run build` emits StyleX CSS into the `web/dist` +assets" and "`assert-bundled` passes with `@stylexjs/stylex` in the recorded +set" — are therefore unmeetable inside S1 as scoped, and the recorded bundled +set is unchanged at the S1 commit. Both were proven instead against a temporary +probe component, then deferred to S2, which converts real pages and does change +the bundled set. Ruling 8's "S1 adds `@stylexjs/stylex` ... to the runtime +closure and the bundled set" is half right: the closure moves in S1, the bundled +set in S2. The inventory entry for StyleX is anticipatory at the S1 commit, +which is deliberate — see the styleq entry for why it cannot wait. + +### The probe results, recorded rather than left as a claim + +Against the temporary probe: the built CSS carries `@layer priority1..N` and the +token variables, resolving to `#fafafa` light and `#09090b` dark with +lightningcss `lab()` fallbacks. The dev server needs no wiring of its own — the +plugin injects `` into +`index.html`, so the `DevStyleXInject` component in StyleX's Vite guide is for +frameworks that bypass Vite's HTML transform and is not used here. Editing a +token regenerates the virtual stylesheet and the dev server issues a page +reload. + +### S1 wrote outside its owned files, and outside an anti-requirement + +The licence guard forced it: `licenses/stylex-mit.txt`, `licenses/styleq-mit.txt` +and their registration in `licenses/licenses.zig`, plus the `npm_not_shipped` +array in `src/licenses_drift_test.zig`. The last contradicts the "no edits ... +under `src/`" anti-requirement below. The anti-requirement is amended, not +ignored: `src/licenses_drift_test.zig` is writable for its licence-fact arrays +and, from S2, for the per-package licence change described next. The rest of +`src/` stays closed. + +### Apache-2.0 inbound: a decision, not a deduction + +`react-aria-components` 1.20.0 brings eight Apache-2.0 packages +(react-aria-components, react-aria, react-stately, `@react-types/shared`, three +`@internationalized/*`, `@swc/helpers`) and `tslib` under 0BSD into an EUPL-1.2 +project. The user accepted Apache-2.0 inbound on 2026-08-12 and authorised the +guard change it needs. This is recorded as their decision because nothing in the +licence settles it: EUPL-1.2's appendix does not list Apache-2.0 — it names GPL, +AGPL, OSL, EPL, CeCILL, MPL, LGPL, CC BY-SA, EUPL and LiLiQ — and that appendix +governs Article 5 outbound relicensing, not inbound consumption. Any claim that +the appendix permits this is false and must not be written into the inventory. +`src/licenses_drift_test.zig:122` hardcodes a single expected licence and +becomes per-package in S2; the guard stays strict, an unexpected licence still +fails. + +### Ruling 8's picture of the React Aria closure is wrong + +There are no `@react-aria/*` or `@react-stately/*` scoped packages. RAC 1.20.0 +ships consolidated `react-aria` and `react-stately`. The runtime closure it adds +is the eleven packages named above plus `aria-hidden`, `clsx` and `client-only` +under MIT — not the "dozens" the ruling anticipated. + +### Ruling 11's claim about the tab tests is wrong + +`LocalDnsPage.tsx` already renders `role="tablist"` and `role="tab"` by hand, so +no test query moves from `getByRole("button")` to `getByRole("tab")`. What RAC +adds there is keyboard arrow-key navigation, which the hand-rolled switcher +lacks, and that is what the S2 acceptance box asserts. + +### `formCard` is a layout change, not a port + +`space-y-3` set sibling margins on a block container; the StyleX version is a +flex column with `gap`. StyleX cannot express the `> * + *` selector `space-y` +compiles to, so `gap` is the only mechanism available. Both call sites +(`RecordsTab`, `ZonesTab`) are plain vertical form stacks, where the difference +in child sizing and margin behaviour does not show. Recorded against ruling 7, +which pins layout structure. + +### S2 needed two build-config changes the spec did not anticipate + +Both are in files S1 and S3 own, both were forced, and neither is optional. + +`web/vitest.setup.ts` (new) plus `setupFiles` in `web/vite.config.ts`: jsdom +implements no `CSS` interface at all, and React Aria's collection code calls +`CSS.escape` unguarded when it looks an item up by key +(`react-aria/dist/private/selection/utils.mjs:22`). Every RAC Select and Tabs +test threw `Cannot read properties of undefined (reading 'escape')` before this +landed. The setup file implements CSSOM's *serialize an identifier* algorithm +rather than approximating it, because a wrong escape would break the key lookups +silently instead of loudly. + +The `@` alias passed to `stylex.vite()` in `web/vite.config.ts`: StyleX resolves +the `.stylex.ts` theme file itself and cannot see Vite's `resolve.alias`, so +`import { colors } from "@/ui/tokens.stylex"` in a feature file failed the build +with "Could not resolve the path to the imported file". Relative imports were the +alternative; passing the alias keeps the repo's `@/` convention instead. + +### RAC Select changes the accessible name of a labelled control + +`useSelect` labels the trigger button with `aria-labelledby="