diff --git a/.changeset/dev-error-visibility.md b/.changeset/dev-error-visibility.md new file mode 100644 index 000000000..8d8d28a11 --- /dev/null +++ b/.changeset/dev-error-visibility.md @@ -0,0 +1,5 @@ +--- +"everything-dev": patch +--- + +Build errors are visible again in `bos dev`. Rsbuild prints `error Build error:` on stderr and the actual detail (`File: …`, `× …`) on stdout; the log pipeline now joins those detail lines onto the header as one error event and treats bundler `×` lines as errors on any stream, so the live tail and shutdown summary show the cause instead of a bare header. CLI commands also stop reporting "Unknown error" for Effect tagged errors (e.g. `ArtifactGenError` from `bos typecheck` / `bos types gen`): the tag, phase and cause chain are now shown. diff --git a/.changeset/dev-hmr-data-uri.md b/.changeset/dev-hmr-data-uri.md new file mode 100644 index 000000000..99320172b --- /dev/null +++ b/.changeset/dev-hmr-data-uri.md @@ -0,0 +1,5 @@ +--- +"every-plugin": patch +--- + +`FixMfDataUriPlugin` now only rewrites `@module-federation/*` imports inside data-URI modules. It used to strip the absolute `node_modules` prefix from every data-URI import, which turned Rsbuild 2's dev HMR client entry (`@rsbuild/core/dist/client/hmr.js`) into a bare specifier its exports map rejects, so `every-plugin dev` failed for the UI and plugin UIs with "Package subpath './dist/client/hmr.js' is not defined by exports". diff --git a/.changeset/soft-frames.md b/.changeset/soft-frames.md new file mode 100644 index 000000000..11e9ea251 --- /dev/null +++ b/.changeset/soft-frames.md @@ -0,0 +1,6 @@ +--- +"ui": minor +"@everything-dev/auth-plugin": minor +--- + +Soft frames visual system: the UI moves from shadcn `base-maia` to square `base-lyra`, with 1px bevel frames — raised buttons that press in, sunken inputs, framed cards, dialogs and menus — in light and dark. Adds `Window`/`WindowTitleBar`/`WindowBody`, an `xl` button size and a `brand` button variant. The landing page is now a centered title, the under-construction GIF and one sign-in button, without the app shell. diff --git a/GLOSSARY.md b/GLOSSARY.md index 72c3c090c..6219e3fda 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -1,12 +1,62 @@ # everything.dev -The open runtime for apps on NEAR — composed from published config and loaded through a shared host, UI, and API runtime. +The open runtime for apps on NEAR — every app is a node, owned by a NEAR account, composed from published config and run by a shared runtime. + +## Nodes + +**Node**: +An app published at `bos:///` — its config is signed by `` and authored with `App(...)`. Every app is a node; "node" names its place in the graph, "app" names what it is. +_Avoid_: runtime (that's the package that runs nodes), tenant, project + +**Root node**: +`bos://dev.everything.near/everything.dev` — the node every other node ultimately extends. +_Avoid_: base runtime, platform, parent app + +**Child node**: +A node addressed under another node's path, `bos:////plugins/` — usually a plugin published from its parent's repo, runnable on its own and composable into any node. +_Avoid_: embedded plugin (a plugin that has not yet published as a node), sub-app + +**Gateway**: +The second segment of a node address; for a top-level node, usually its domain. +_Avoid_: name, slug + +**Owner**: +The NEAR account that signs a node's config — a single-key account or a multisig (Sputnik DAOs are multisigs). Ownership is whatever is true on-chain. +_Avoid_: organization (an organization links to an owner; it never is one), admin + +**Address**: +Where a node is reached: a subdomain of its parent's domain, or its own domain. +_Avoid_: URL, host + +**Hosting**: +What serves a node: the shared host, a sandbox, or a standalone deployment. Independent of Address. +_Avoid_: tier, instance + +**Extends**: +A node inheriting its parent node's config, child-wins merged. +_Avoid_: fork, inherit + +**Composition**: +A node pulling another node's surfaces in as one of its `plugins`; the composed node plays the **plugin** role. References follow the composed node's latest publish unless pinned. +_Avoid_: install, import, dependency + +**Surface**: +One loadable part of a node — host, UI, API, or auth. +_Avoid_: remote (the Module Federation transport word), workspace (the repo directory), slot + +**Contract**: +The API shape a node provides or requires, identified by its published contract types; what makes swapping one node's UI over another node's API checkable. +_Avoid_: interface, schema + +**Node card**: +A node's public self-description — title, extends, surfaces, contracts, skill, MCP endpoint, and source — published with its config. +_Avoid_: manifest (that's the build artifact), metadata ## Language **Runtime**: -A published app configuration (`bos.app.ts` authored, resolved through `extends`) that the host loads as remotes. -_Avoid_: app (ambiguous between the runtime and a tenant), project +The `everything-dev` package and host process that loads a node's surfaces and runs it. `RuntimeConfig` in code is a historical name for a node's resolved config. +_Avoid_: app or node (those are what the runtime runs), base runtime **SlotResolver**: The host's entry-URL seam (`entryUrls` in `everything-dev/ui/slot`) — one pure derivation from a stamped config slot (`EntrySlot`) to the URLs each surface loads: `web` (browser entry — bustered fixed name in development, pin-derived hashed outside it), `ssr` (server container entry; undefined when the slot has no SSR coordinates), and `browserManifest` (the pin-derived hashed mf-manifest, structurally absent for local slots — atomic-deploys 08). Surfaces resolve lazily and throw loudly outside development naming slot + surface; buster arithmetic (integrity for web, ssrIntegrity→containerVersion for the server) lives inside the module, never at call sites. @@ -26,24 +76,24 @@ _Avoid_: sync file, template file (sync no longer carries ui source) ## Organization access +**Organization**: +A group of people who act together; it links to one or more Owners so its members can work on those owners' nodes. +_Avoid_: owner, org account + +**Linked Account**: +An Owner an organization has proven control of — a single-key account the member has signed in with, or a multisig the member belongs to on-chain. An account links to at most one organization. +_Avoid_: DAO link, connected wallet + **Team**: -A named sub-group within an organization that shares access to the organization's feature areas. +A named sub-group within an organization. _Avoid_: team account, team wallet **Active Team**: -The Team currently selected for a user's organization work. +The Team a user is currently acting as. _Avoid_: current team, team account -**Feature Area**: -A product capability that an organization can grant to a Team. -_Avoid_: permission, role - -**Team Workspace**: -The organization view scoped to an Active Team and its granted feature areas. -_Avoid_: organization account - **Gateway Origin**: -The canonical base runtime origin for a network, where every passkey ceremony takes place regardless of which domain the member arrived from. +The root node's origin for a network, where every passkey ceremony takes place regardless of which domain the member arrived from. _Avoid_: base URL, main domain **Device Link**: diff --git a/docs/adr/0027-every-app-is-a-node.md b/docs/adr/0027-every-app-is-a-node.md new file mode 100644 index 000000000..a5a25948d --- /dev/null +++ b/docs/adr/0027-every-app-is-a-node.md @@ -0,0 +1,28 @@ +# ADR 0027: Every app is a node + +Date: 2026-10-06 +Status: Accepted + +Apps already extend each other (`bos:///` → `extends`), but plugins have no identity of their own: `bos plugin publish` rewrites the host app's whole config, and the only way to point at a plugin is a fragment into someone else's config (`bos://acct/gw#plugins.registry`). Most plugins are apps — an API and a UI exposed for composition — so we treat every app as a **node** in one graph, and give plugins a real address nested under the node that publishes them: `bos:////plugins/`, stored at `apps///plugins//bos.config.json`. The authoring surface does not change: a node is still `App(...)`, `gateway` keeps its name, and `RuntimeConfig` stays in code as a historical name; "Runtime" now means the `everything-dev` package and host that run nodes. + +## Decisions + +1. **Address.** A node is `bos:///[/plugins/]…`; the path nests, so a child node can compose its own children. `parseBosUrl` and `getRegistryConfigKey` (`packages/everything-dev/src/fastkv.ts`) already accept the trailing segments. Nested addresses mirror the bundle layout (`bundles////…`, ADR 0015/0020), avoid global plugin names, and let one prefix scan of `apps///` return a node and everything under it. +2. **Ownership is on-chain.** The signing account owns the node (ADR 0001, unchanged) — a single-key account or a multisig; Sputnik DAOs are multisigs and publish through a proposal whose `FunctionCall` executes as the DAO. An organization never owns a node: it **links** to owners through a verified `linkAccount { accountId, kind: "account" | "multisig" }` (the member's signed-in NEAR account, or on-chain membership of the multisig), replacing the unverified `linkDao`. An account links to at most one organization. +3. **Follow by default.** A composed or extended reference follows the target's latest publish — instant updates are the point. `@` pins to the content-addressed version manifest that already exists (`every-plugin/src/version-manifest.ts`). Contract checks gate a follow before the new version is adopted. +4. **Contracts make swaps checkable.** A node declares the contracts it provides (its published contract types, by sha256 and semver) and requires. A UI node requiring `auth@^1` runs over any node providing it — "swap the UI, keep the functionality" becomes a check, not a hope. +5. **Node card.** Each published config carries a node card — title, extends, surfaces, provides/requires, skill URL, MCP endpoint, source (repository, git sha) — also served at `/.well-known/bos.json`, so agents and the registry discover nodes the same way (aligned with NEP-330 source metadata, agent skills, and `.well-known/mcp.json`). +6. **Graduation.** A child node that moves to its own domain publishes at the new top-level address, and its old nested address `extends` the new one, so existing composers keep following. + +## Considered options + +- **Flat `bos:///` for plugins** — host-independent identity, but needs globally unique names per account and redefines `gateway` as a generic name. Rejected; graduation (6) covers the independence case. +- **`modules/{ns}/{name}` + ModuleRecord** (beta-v2 map decision #3) — a second key space and record type beside app configs. Rejected in favour of one config shape at every level of the tree; the record's useful fields land in the node card. +- **Organization-owned nodes** — would make an off-chain auth record the authority over an on-chain write. Rejected; ownership follows the signer. + +## Consequences + +- The registry indexes nested configs and shows "used by" for every node; a plugin that has not published under its own nested address is shown as embedded until it does. +- `bos plugin publish ` must write `apps///plugins//bos.config.json` and the parent references it with `Plugin(id).extends("bos://…/plugins/")`; the CLI and publish track is shared with the citynode "Everything is a node" wayfinder (citynode.app#233–#252). +- Each plugin directory moves from `plugin.dev.ts` to the `bos.app.ts` + `bos.dev.ts` pair, so any node can boot standalone on the universal image (ADR 0021). +- The dev panel can only write a node when the connected wallet is its owner; multisig owners get a proposal flow later. diff --git a/docs/adr/README.md b/docs/adr/README.md index d3305c49b..3b9358764 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,6 +32,7 @@ Format: context → decision → consequences, one file per decision. | [0024](./0024-composition-stays-manifest-based.md) | Composition stays manifest-based; route chunks load lazily | Accepted | | [0025](./0025-auth-identity-value-classes.md) | Auth identity values derive from the runtime config | Accepted | | [0026](./0026-pnpm-workspace-node-runtime-v2.md) | pnpm workspace, node runtime — one toolchain for parent and children (v2) | Accepted | +| [0027](./0027-every-app-is-a-node.md) | Every app is a node — nested plugin addresses, on-chain ownership, follow by default | Accepted | Skipped numbers: 0006 exists; there are no gaps otherwise. 0015–0018 were @@ -42,7 +43,7 @@ low-reference file in each pair moved (see git history). ## Number assignment discipline - Take the next free number (`ls docs/adr/ | sort -n | tail -1`, then +1 — - next free is **0026**). Never reuse a number, even for a rejected ADR. + next free is **0028**). Never reuse a number, even for a rejected ADR. - When citing an ADR in code, plans, changesets, or AGENTS.md, cite the number **with the filename** if the context could be ambiguous. - Superseded ADRs stay in place with a `Superseded by` status link to the diff --git a/packages/every-plugin/src/build/rspack/fix-mf-data-uri-plugin.ts b/packages/every-plugin/src/build/rspack/fix-mf-data-uri-plugin.ts index 34ee7981c..9ba67bc0a 100644 --- a/packages/every-plugin/src/build/rspack/fix-mf-data-uri-plugin.ts +++ b/packages/every-plugin/src/build/rspack/fix-mf-data-uri-plugin.ts @@ -2,6 +2,8 @@ import type { Compiler, RspackPluginInstance } from "@rspack/core"; const MF_DATA_URI_MARKER = "data:text/javascript,"; +const MF_ABSOLUTE_IMPORT_RE = /(["'])\/[^"']*?\/node_modules\/(@module-federation\/[^"']*)/g; + export class FixMfDataUriPlugin implements RspackPluginInstance { name = "FixMfDataUriPlugin"; @@ -9,29 +11,32 @@ export class FixMfDataUriPlugin implements RspackPluginInstance { compiler.hooks.compilation.tap(this.name, (_compilation, { normalModuleFactory }) => { normalModuleFactory.hooks.beforeResolve.tap(this.name, (resolveData) => { if (!resolveData?.request) return; - if (!resolveData.request.includes(MF_DATA_URI_MARKER)) return; - this.reencodeDataUri(resolveData); + resolveData.request = rewriteMfDataUriRequest(resolveData.request); }); }); } +} - private reencodeDataUri(resolveData: { request: string }) { - const { request } = resolveData; - const idx = request.indexOf(MF_DATA_URI_MARKER); - if (idx === -1) return; - - const contentStart = idx + MF_DATA_URI_MARKER.length; - const prefix = request.substring(0, contentStart); - const rawContent = request.substring(contentStart); +/** + * The MF runtime's generated data-URI module imports its runtime pieces by + * absolute node_modules paths (`require.resolve` output), which differ per + * machine. Rewriting them to bare specifiers keeps the module — and therefore + * the module id space — machine-independent; `mfDataUriAliases()` points the + * specifiers back at their on-disk targets for resolution. + * + * Only `@module-federation/*` imports are rewritten: other data-URI entries + * (e.g. Rsbuild's dev HMR client, `@rsbuild/core/dist/client/hmr.js`) import + * subpaths their package's exports map does not expose, so stripping their + * absolute prefix makes them unresolvable. + */ +export function rewriteMfDataUriRequest(request: string): string { + const idx = request.indexOf(MF_DATA_URI_MARKER); + if (idx === -1) return request; - // The MF runtime's generated data-URI module imports its runtime pieces - // by absolute node_modules paths (`require.resolve` output), which differ - // per machine. Rewriting them to bare specifiers keeps the module — and - // therefore the module id space — machine-independent; the composition - // aliases the specifiers to their on-disk targets for resolution. - const decoded = safeDecode(rawContent).replace(/(["'])\/[^"']*?\/node_modules\//g, "$1"); - resolveData.request = prefix + encodeURIComponent(decoded); - } + const contentStart = idx + MF_DATA_URI_MARKER.length; + const prefix = request.substring(0, contentStart); + const decoded = safeDecode(request.substring(contentStart)); + return prefix + encodeURIComponent(decoded.replace(MF_ABSOLUTE_IMPORT_RE, "$1$2")); } function safeDecode(content: string): string { diff --git a/packages/every-plugin/tests/unit/fix-mf-data-uri.test.ts b/packages/every-plugin/tests/unit/fix-mf-data-uri.test.ts new file mode 100644 index 000000000..232c902eb --- /dev/null +++ b/packages/every-plugin/tests/unit/fix-mf-data-uri.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { rewriteMfDataUriRequest } from "../../src/build/rspack/fix-mf-data-uri-plugin"; + +const dataUri = (source: string) => `data:text/javascript,${encodeURIComponent(source)}`; +const decode = (request: string) => + decodeURIComponent(request.slice("data:text/javascript,".length)); + +describe("rewriteMfDataUriRequest", () => { + it("strips the machine-absolute prefix from MF runtime imports", () => { + const request = dataUri( + [ + `import * as runtime from "/Users/a/proj/node_modules/.pnpm/x/node_modules/@module-federation/webpack-bundler-runtime/dist/index.js";`, + `import plugin from '/home/b/node_modules/@module-federation/node/dist/src/runtimePlugin.js';`, + ].join("\n"), + ); + const out = decode(rewriteMfDataUriRequest(request)); + expect(out).toContain(`"@module-federation/webpack-bundler-runtime/dist/index.js"`); + expect(out).toContain(`'@module-federation/node/dist/src/runtimePlugin.js'`); + expect(out).not.toContain("/node_modules/"); + }); + + it("leaves non-MF absolute imports alone so unexported subpaths still resolve", () => { + const hmr = "/Users/a/proj/node_modules/@rsbuild/core/dist/client/hmr.js"; + const overlay = "/Users/a/proj/node_modules/@rsbuild/core/dist/client/overlay.js"; + const request = dataUri( + `import { init } from '${hmr}';\nimport '${overlay}';\ninit('t', {}, "localhost", 3003);`, + ); + const out = decode(rewriteMfDataUriRequest(request)); + expect(out).toContain(`'${hmr}'`); + expect(out).toContain(`'${overlay}'`); + }); + + it("passes through requests that are not data URIs", () => { + expect(rewriteMfDataUriRequest("./src/index.ts")).toBe("./src/index.ts"); + }); +}); diff --git a/packages/everything-dev/src/build.ts b/packages/everything-dev/src/build.ts index 23893afce..10d2bb3d2 100644 --- a/packages/everything-dev/src/build.ts +++ b/packages/everything-dev/src/build.ts @@ -5,6 +5,7 @@ import { resolveLocalDevelopmentPath } from "./config"; import type { WorkspaceDeployResult } from "./contract"; import { syncResolvedSharedDeps } from "./shared-deps"; import type { BosConfig, BosPluginRef, RuntimeConfig } from "./types"; +import { describeError } from "./utils/error"; import { run } from "./utils/run"; import { padRight } from "./utils/string"; import { colors, icons } from "./utils/theme"; @@ -273,7 +274,7 @@ export async function buildWorkspaceTargets(opts: { key: ws.key, kind: ws.kind, success: false, - error: result.reason?.message ?? "Unknown error", + error: describeError(result.reason), }); } } diff --git a/packages/everything-dev/src/commands/db.ts b/packages/everything-dev/src/commands/db.ts index b07f54031..046d168b3 100644 --- a/packages/everything-dev/src/commands/db.ts +++ b/packages/everything-dev/src/commands/db.ts @@ -1,5 +1,6 @@ import { Context, Effect } from "effect"; import { findConfigPath, MISSING_CONFIG_MESSAGE } from "../config"; +import { describeError } from "../utils/error"; import { type BosBuilder, BosDepsTag } from "./shared"; export function registerDb(builder: BosBuilder) { @@ -36,7 +37,7 @@ export function registerDb(builder: BosBuilder) { plugin: input.plugin, source: "remote" as const, section: "", - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -82,7 +83,7 @@ export function registerDb(builder: BosBuilder) { appliedHashCount: 0, expectedTables: [], missingTables: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -112,9 +113,9 @@ export function registerDb(builder: BosBuilder) { } catch (error) { return { status: "error" as const, - message: error instanceof Error ? error.message : "Unknown error", + message: describeError(error), diagnosis: null, - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), diff --git a/packages/everything-dev/src/commands/init.ts b/packages/everything-dev/src/commands/init.ts index 34d498400..16de2970c 100644 --- a/packages/everything-dev/src/commands/init.ts +++ b/packages/everything-dev/src/commands/init.ts @@ -31,6 +31,7 @@ import { timePhase } from "../progress"; import { openResolution } from "../resolution/session"; import { syncResolvedSharedDeps } from "../shared-deps"; import type { BosConfig, BosConfigInput, StarterLevel } from "../types"; +import { describeError } from "../utils/error"; import { saveBosConfig } from "../utils/save-config"; import type { BosBuilder } from "./shared"; @@ -351,7 +352,7 @@ export function registerInit(builder: BosBuilder) { overrides: input.overrides, filesCopied: 0, timings: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), diff --git a/packages/everything-dev/src/commands/keys.ts b/packages/everything-dev/src/commands/keys.ts index d38f135fe..afa5d2d46 100644 --- a/packages/everything-dev/src/commands/keys.ts +++ b/packages/everything-dev/src/commands/keys.ts @@ -23,6 +23,7 @@ import { } from "../near-cli"; import { getNetworkIdForAccount } from "../network"; import type { ResolutionSession } from "../resolution/session"; +import { describeError } from "../utils/error"; import { colors } from "../utils/theme"; import { type BosBuilder, BosDepsTag } from "./shared"; @@ -114,7 +115,7 @@ export function registerKeys(builder: BosBuilder) { contract, allowance: input.allowance, functionNames: PUBLISH_FUNCTION_NAMES, - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -200,7 +201,7 @@ export function registerKeys(builder: BosBuilder) { return { status: "error" as const, siteUrl: input.site ?? "", - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -244,7 +245,7 @@ export function registerKeys(builder: BosBuilder) { status: "error" as const, revokedApiKey: false, removedPublishKey: false, - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), diff --git a/packages/everything-dev/src/commands/ops.ts b/packages/everything-dev/src/commands/ops.ts index 832a0147e..ebd4ef481 100644 --- a/packages/everything-dev/src/commands/ops.ts +++ b/packages/everything-dev/src/commands/ops.ts @@ -12,6 +12,7 @@ import { ownerOfPort } from "../infra/port-ownership"; import { killProcessGroupEscalating, reapGroup } from "../process-kill"; import { isPidAlive, pruneDeadEffect, readRegistry, unregisterPid } from "../process-registry"; import { openResolution } from "../resolution/session"; +import { describeError } from "../utils/error"; import { computeDeployedVersionStatus, type DeployedVersionStatus } from "../version-status"; import { type BosBuilder, type BosDeps, BosDepsTag } from "./shared"; @@ -41,7 +42,7 @@ export function registerOps(builder: BosBuilder) { status: "error" as const, packages: [], envFile: "missing" as const, - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -87,7 +88,7 @@ export function registerOps(builder: BosBuilder) { return { status: "error" as const, entries: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -170,7 +171,7 @@ export function registerOps(builder: BosBuilder) { status: "error" as const, killed: [], skipped: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), diff --git a/packages/everything-dev/src/commands/upgrade.ts b/packages/everything-dev/src/commands/upgrade.ts index 2e00bceb3..ecdf04ec7 100644 --- a/packages/everything-dev/src/commands/upgrade.ts +++ b/packages/everything-dev/src/commands/upgrade.ts @@ -10,6 +10,7 @@ import { findConfigPath, MISSING_CONFIG_MESSAGE, readAuthoredConfigInput } from import { checkFederationCompat } from "../mf"; import { openResolution } from "../resolution/session"; import type { BosConfig } from "../types"; +import { describeError } from "../utils/error"; import { colors } from "../utils/theme"; import type { BosBuilder } from "./shared"; @@ -45,7 +46,7 @@ export function registerUpgrade(builder: BosBuilder) { updated: [], skipped: [], added: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -67,7 +68,7 @@ export function registerUpgrade(builder: BosBuilder) { return { status: "error" as const, packages: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -238,7 +239,7 @@ export function registerUpgrade(builder: BosBuilder) { fetched: [], skipped: [], failed: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), @@ -381,7 +382,7 @@ export function registerUpgrade(builder: BosBuilder) { checked: [], skipped: [], results: [], - error: error instanceof Error ? error.message : "Unknown error", + error: describeError(error), }; } }), diff --git a/packages/everything-dev/src/dev-log-pipeline.ts b/packages/everything-dev/src/dev-log-pipeline.ts index 2a5b433a7..ae5784a0c 100644 --- a/packages/everything-dev/src/dev-log-pipeline.ts +++ b/packages/everything-dev/src/dev-log-pipeline.ts @@ -62,6 +62,13 @@ export function toEffectLogLevel(level: LogLevel): EffectLogLevel { const STACK_FRAME_RE = /^\s+at\s/; +const OPEN_ERROR_HEADER_RE = /^(?:error\b.*|.*\b(?:Build|Compile|Compilation) error)\s*:\s*$/i; + +const ERROR_DETAIL_RE = + /^(?:\s+\S|\s*[×│╭╰├┌└╷╵|]|File:|help:|Caused by|Module not found|Can't resolve|Package subpath)/; + +const MAX_ERROR_BLOCK_LINES = 60; + const countBraces = (line: string): number => { let count = 0; for (const ch of line) { @@ -111,7 +118,13 @@ const feedLine = ( return null; } - const isContinuation = state.openBraces > 0 || STACK_FRAME_RE.test(line) || /^\s*\{/.test(line); + const inErrorBlock = + OPEN_ERROR_HEADER_RE.test(state.pending[0] ?? "") && + state.pending.length < MAX_ERROR_BLOCK_LINES && + ERROR_DETAIL_RE.test(line); + + const isContinuation = + inErrorBlock || state.openBraces > 0 || STACK_FRAME_RE.test(line) || /^\s*\{/.test(line); if (isContinuation) { const closeOnBalance = state.openBraces > 0; @@ -151,6 +164,7 @@ export function normalizeLines(lines: string[]): string[] { } const WARN_RE = /(?:^|\s)WARN\b|\[WARN\]|(?:^|\s)\w*Warning:/i; +const BUNDLER_ERROR_RE = /^(?:error\s{2,}|\s*×\s)/; const DEBUG_RE = /(?:^|\s)DEBUG\b|\[DEBUG\]/i; const EXIT_LINE_RE = @@ -214,7 +228,7 @@ export function classifyEvent(raw: RawLogLine): LogEvent { const category = classifyCategory(text); let level: LogLevel; - let isError = raw.isError === true; + let isError = raw.isError === true || BUNDLER_ERROR_RE.test(text); if (exitMatch) { if (exitMatch[2]) { @@ -223,7 +237,7 @@ export function classifyEvent(raw: RawLogLine): LogEvent { level = exitMatch[1] === "0" ? "info" : "error"; } isError = level === "error"; - } else if (WARN_RE.test(text)) { + } else if (WARN_RE.test(text.split("\n", 1)[0] ?? "")) { level = "warn"; isError = false; } else if (DEBUG_RE.test(text)) { diff --git a/packages/everything-dev/src/dev-program.ts b/packages/everything-dev/src/dev-program.ts index d1ce43ec4..57230c339 100644 --- a/packages/everything-dev/src/dev-program.ts +++ b/packages/everything-dev/src/dev-program.ts @@ -48,6 +48,7 @@ import { syncResolvedSharedDeps } from "./shared-deps"; import { isRegistryStart, resolveStartConfigSource } from "./start-config-source"; import type { BosConfig, SourceMode } from "./types"; import { BosConfigSchema } from "./types"; +import { describeError } from "./utils/error"; import { run } from "./utils/run"; import { ensureFreshDeps, findWorkspaceRoot } from "./workspace"; @@ -440,9 +441,9 @@ export const startBootstrap = ( try: () => resolvePublishedConfig(account, domain, input.registry), catch: (error) => new StartFetchFailed({ - message: `Failed to fetch config for bos://${account}/${domain}: ${ - error instanceof Error ? error.message : "Unknown error" - }\nExpected URL: ${expectedUrl}`, + message: `Failed to fetch config for bos://${account}/${domain}: ${describeError( + error, + )}\nExpected URL: ${expectedUrl}`, }), }); if (remoteConfig) { diff --git a/packages/everything-dev/src/publish.ts b/packages/everything-dev/src/publish.ts index 156705af8..7bca49439 100644 --- a/packages/everything-dev/src/publish.ts +++ b/packages/everything-dev/src/publish.ts @@ -31,6 +31,7 @@ import { openResolution } from "./resolution/session"; import { verifyRollbackSnapshot } from "./rollback"; import { collectDistFiles, uploadBundle, uploadWorkspaceDist } from "./storage-upload"; import type { BosConfig, BosConfigInput, PublishConfig, RuntimeConfig } from "./types"; +import { describeError } from "./utils/error"; import { padRight } from "./utils/string"; import { colors, icons } from "./utils/theme"; import { composeWorkspaceVersionManifest, readBuildReport } from "./version-manifest-deploy"; @@ -268,7 +269,7 @@ export async function preflightPublish(input: PublishToFastKvInput): Promise MAX_CAUSE_DEPTH) return ""; + if (typeof error === "string") return error.trim() || (depth === 0 ? "Unknown error" : ""); + if (error === null || error === undefined || typeof error !== "object") { + return depth === 0 ? (error === undefined ? "Unknown error" : String(error)) : ""; + } + + const record = error as { message?: unknown; _tag?: unknown; phase?: unknown; cause?: unknown }; + const message = typeof record.message === "string" ? record.message.trim() : ""; + const tag = typeof record._tag === "string" ? record._tag : ""; + const phase = typeof record.phase === "string" ? record.phase : ""; + const label = message || (tag ? (phase ? `${tag} (${phase})` : tag) : ""); + const cause = + record.cause !== undefined && record.cause !== error + ? describeError(record.cause, depth + 1) + : ""; + + const parts = label && cause && !label.includes(cause) ? `${label}: ${cause}` : label || cause; + if (parts) return parts; + return depth === 0 ? "Unknown error" : ""; +} diff --git a/packages/everything-dev/tests/unit/describe-error.test.ts b/packages/everything-dev/tests/unit/describe-error.test.ts new file mode 100644 index 000000000..ffc56c77f --- /dev/null +++ b/packages/everything-dev/tests/unit/describe-error.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { ArtifactGenError } from "../../src/code-artifacts"; +import { describeError } from "../../src/utils/error"; + +describe("describeError", () => { + it("surfaces a tagged error's phase and cause instead of 'Unknown error'", () => { + const error = new ArtifactGenError({ + phase: "generate ui manifest (ui)", + cause: new Error( + 'manifest generation failed for ui:\n route "prototype/retro-frames" must live under a mount', + ), + }); + const message = describeError(error); + expect(message).toContain("generate ui manifest (ui)"); + expect(message).toContain('route "prototype/retro-frames" must live under a mount'); + }); + + it("keeps plain Error messages as-is", () => { + expect(describeError(new Error("boom"))).toBe("boom"); + }); + + it("does not repeat a cause already contained in the message", () => { + const inner = new Error("disk full"); + expect(describeError(new Error("write failed: disk full", { cause: inner }))).toBe( + "write failed: disk full", + ); + }); + + it("walks plain-object failures and strings", () => { + expect(describeError({ _tag: "NotFound" })).toBe("NotFound"); + expect(describeError("nope")).toBe("nope"); + }); + + it("falls back to 'Unknown error' only when nothing is describable", () => { + expect(describeError(undefined)).toBe("Unknown error"); + expect(describeError({})).toBe("Unknown error"); + }); + + it("survives self-referencing causes", () => { + const a: { message: string; cause?: unknown } = { message: "" }; + const b = { message: "", cause: a }; + a.cause = b; + expect(describeError(a)).toBe("Unknown error"); + }); +}); diff --git a/packages/everything-dev/tests/unit/dev-log-pipeline.test.ts b/packages/everything-dev/tests/unit/dev-log-pipeline.test.ts index 9f7492956..53285ed6e 100644 --- a/packages/everything-dev/tests/unit/dev-log-pipeline.test.ts +++ b/packages/everything-dev/tests/unit/dev-log-pipeline.test.ts @@ -346,6 +346,51 @@ describe("createLogPipeline broadcast", () => { expect(file[0]).toContain("}"); }); + it("groups an rsbuild build error split across stderr and stdout into one error event", () => { + const { pipeline, display, exported } = makePipeline(); + pipeline.ingest({ source: "ui", line: "start build started..." }); + pipeline.ingest({ source: "ui", line: "error Build error: ", isError: true }); + pipeline.ingest({ + source: "ui", + line: "File: data-uri virtual module (import%20%7B%20init%20%7D%20fr...)", + }); + pipeline.ingest({ + source: "ui", + line: " × Package subpath './dist/client/hmr.js' is not defined by \"exports\" in /x/node_modules/@rsbuild/core/package.json", + }); + pipeline.ingest({ + source: "ui", + line: "Timed out or failed: wait for process readiness: Process failed: ui", + isError: true, + }); + pipeline.ingest({ source: "ui", line: "ready built in 1.4s" }); + pipeline.flush(); + + expect(exported).toHaveLength(2); + expect(exported[0]).toContain("Build error:"); + expect(exported[0]).toContain("File: data-uri virtual module"); + expect(exported[0]).toContain("Package subpath './dist/client/hmr.js'"); + expect(exported[1]).toBe("Timed out or failed: wait for process readiness: Process failed: ui"); + expect(display.every((d) => d.isError)).toBe(true); + }); + + it("surfaces bundler × error lines printed on stdout without a stderr header", () => { + const { pipeline, exported } = makePipeline(); + pipeline.ingest({ source: "auth", line: " × Module not found: Can't resolve './missing'" }); + pipeline.flush(); + + expect(exported).toEqual([" × Module not found: Can't resolve './missing'"]); + }); + + it("does not swallow unrelated lines after an error header that has a message", () => { + const { pipeline, file } = makePipeline(); + pipeline.ingest({ source: "ui", line: "error build failed in 2.04s", isError: true }); + pipeline.ingest({ source: "ui", line: " indented info line" }); + pipeline.flush(); + + expect(file).toEqual(["error build failed in 2.04s", " indented info line"]); + }); + it("separates multi-line blocks per source", () => { const { pipeline, file } = makePipeline(); pipeline.ingest({ source: "api", line: "[INFO] (api) Request context" }); diff --git a/plugins/auth/ui/components.json b/plugins/auth/ui/components.json index 65be6fbba..c34b6d2ee 100644 --- a/plugins/auth/ui/components.json +++ b/plugins/auth/ui/components.json @@ -1,6 +1,6 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-maia", + "style": "base-lyra", "rsc": false, "tsx": true, "tailwind": { diff --git a/plugins/auth/ui/src/components/ui/badge.tsx b/plugins/auth/ui/src/components/ui/badge.tsx index b89828880..254809d92 100644 --- a/plugins/auth/ui/src/components/ui/badge.tsx +++ b/plugins/auth/ui/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "cn"; const badgeVariants = cva( - "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", { variants: { variant: { diff --git a/plugins/auth/ui/src/components/ui/button.tsx b/plugins/auth/ui/src/components/ui/button.tsx index fc49ffdfc..789a1d60b 100644 --- a/plugins/auth/ui/src/components/ui/button.tsx +++ b/plugins/auth/ui/src/components/ui/button.tsx @@ -5,27 +5,31 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "cn"; const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-none border-0 text-sm font-medium whitespace-nowrap transition-bevel outline-none select-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:bg-face disabled:text-muted-foreground disabled:text-emboss aria-disabled:pointer-events-none aria-disabled:bg-face aria-disabled:text-muted-foreground aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", + default: + "bg-primary text-primary-foreground bevel-raised hover:bg-primary/90 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", + brand: + "bg-brand text-brand-foreground bevel-raised hover:bg-brand/90 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", outline: - "border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground", + "bg-face text-foreground bevel-raised hover:bg-accent hover:text-accent-foreground active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-accent aria-expanded:bevel-pressed data-pressed:bevel-pressed", secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + "bg-card text-card-foreground bevel-raised hover:bg-muted active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-muted aria-expanded:bevel-pressed data-pressed:bevel-pressed", ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + "bg-transparent text-foreground bevel-none hover:bg-muted hover:bevel-raised active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-muted aria-expanded:bevel-pressed data-pressed:bg-accent data-pressed:bevel-pressed disabled:bg-transparent aria-disabled:bg-transparent", destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", + "bg-destructive-muted text-destructive-muted-foreground bevel-raised hover:bg-destructive hover:text-destructive-foreground focus-visible:ring-destructive/30 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", + link: "bg-transparent text-primary bevel-none underline-offset-4 hover:underline disabled:bg-transparent aria-disabled:bg-transparent", }, size: { default: "h-11 gap-2 px-5 text-base has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4", - xs: "h-7 gap-1 px-3 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", - sm: "h-9 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + xs: "h-7 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", + sm: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", lg: "h-12 gap-2 px-6 text-base has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg:not([class*='size-'])]:size-5", + xl: "h-14 gap-2.5 px-8 text-lg has-data-[icon=inline-end]:pr-7 has-data-[icon=inline-start]:pl-7 [&_svg:not([class*='size-'])]:size-5", icon: "size-11", "icon-xs": "size-7 [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-9", diff --git a/plugins/auth/ui/src/components/ui/card.tsx b/plugins/auth/ui/src/components/ui/card.tsx index 77dc17cf4..c42cb6ba2 100644 --- a/plugins/auth/ui/src/components/ui/card.tsx +++ b/plugins/auth/ui/src/components/ui/card.tsx @@ -11,7 +11,7 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-2xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-sm text-card-foreground bevel-window [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", className, )} {...props} diff --git a/plugins/auth/ui/src/components/ui/dialog.tsx b/plugins/auth/ui/src/components/ui/dialog.tsx index de1c2cfbe..786cd370c 100644 --- a/plugins/auth/ui/src/components/ui/dialog.tsx +++ b/plugins/auth/ui/src/components/ui/dialog.tsx @@ -47,7 +47,7 @@ function DialogContent({ ) { type={type} data-slot="input" className={cn( - "h-11 w-full min-w-0 rounded-4xl border border-input bg-input/30 px-4 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", + "h-11 w-full min-w-0 rounded-none border-0 bg-background bevel-sunken px-4 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className, )} {...props} diff --git a/plugins/auth/ui/src/components/ui/popover.tsx b/plugins/auth/ui/src/components/ui/popover.tsx index 25685d575..18b986476 100644 --- a/plugins/auth/ui/src/components/ui/popover.tsx +++ b/plugins/auth/ui/src/components/ui/popover.tsx @@ -31,7 +31,7 @@ function PopoverContent({ {children} - + diff --git a/plugins/auth/ui/src/styles.css b/plugins/auth/ui/src/styles.css index 4bb09c4b8..d92fac708 100644 --- a/plugins/auth/ui/src/styles.css +++ b/plugins/auth/ui/src/styles.css @@ -73,6 +73,7 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-face: var(--face); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); @@ -81,3 +82,51 @@ --radius-3xl: calc(var(--radius) * 2.2); --radius-4xl: calc(var(--radius) * 2.6); } + +@utility bevel-raised { + --tw-shadow: var(--bevel-raised); + box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@utility bevel-pressed { + --tw-shadow: var(--bevel-pressed); + box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@utility bevel-sunken { + --tw-shadow: var(--bevel-sunken); + box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@utility bevel-window { + --tw-shadow: var(--bevel-window); + box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@utility bevel-none { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@utility bg-titlebar { + background: var(--titlebar-bg); + color: var(--titlebar-fg); +} + +@utility p-window { + padding: var(--window-inset); +} + +@utility text-emboss { + text-shadow: 1px 1px 0 var(--bevel-light); +} + +@utility transition-bevel { + transition-property: box-shadow, background-color, color; + transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1); + transition-duration: var(--release-duration); + + &:active { + transition-duration: var(--press-duration); + } +} diff --git a/tests/regression/browser/specs/plugin-chunk-laziness.spec.ts b/tests/regression/browser/specs/plugin-chunk-laziness.spec.ts index dbaf667b1..bef08a39a 100644 --- a/tests/regression/browser/specs/plugin-chunk-laziness.spec.ts +++ b/tests/regression/browser/specs/plugin-chunk-laziness.spec.ts @@ -56,8 +56,8 @@ test.describe("plugin route chunk laziness", () => { const beforeNavigation = await authJsResources(); expect(beforeNavigation.length, "compose must register the auth remote").toBeGreaterThan(0); - // Client-navigate into the plugin route via the header's sign-in link. - const signIn = page.getByTestId("public-header-signin"); + // Client-navigate into the plugin route via the landing sign-in button. + const signIn = page.getByTestId("landing-primary"); await expect(signIn).toBeVisible(); await signIn.click(); diff --git a/ui/DESIGN.md b/ui/DESIGN.md index 72aecff46..7444846a8 100644 --- a/ui/DESIGN.md +++ b/ui/DESIGN.md @@ -1,12 +1,40 @@ # everything.dev design system -The everything.dev UI is built on shadcn's `base-maia` style (preset `b3ZN5L2h44`) running on -Base UI, with our own tokens, sizes and page patterns layered on top. The preset is -the starting point; this file is the source of truth. +The everything.dev UI is built on shadcn's `base-lyra` style (square) running on Base UI, +with our own tokens, sizes, frames and page patterns layered on top. This file is the +source of truth. -`bun lint` enforces the mechanical parts through `@shadcn/lint`. The rest is judgment — +`pnpm run lint` enforces the mechanical parts through `@shadcn/lint`. The rest is judgment — read this before building or changing a page. +## Direction: soft frames + +Modern space, type and layout, with the frame borrowed from Windows 95/98-era software +(RollerCoaster Tycoon): raised bevel buttons that press in, window title bars, sunken +panels for inputs and code. "Soft frames" won the retro-frames prototype over louder +Win98 and Tycoon variants: a 1px bevel plus a 1px outline. No desktop metaphor — no +draggable windows, taskbar or desktop icons. + +- Everything is square: `--radius` is `0`, so the whole `rounded-*` scale resolves to 0. + `rounded-full` (avatars, dots) is the only round shape. +- Frames are tokens in `styles.css` (`--bevel-raised`, `--bevel-pressed`, + `--bevel-sunken`, `--bevel-window`, built from `--bevel-light`/`--bevel-shade`, light + and dark) and utilities used **only inside `components/ui/*`**: `bevel-raised`, + `bevel-pressed`, `bevel-sunken`, `bevel-window`, `bevel-none`, `bg-titlebar`, + `transition-bevel`. They compose with `ring-*`, so focus rings still work. +- Raised: buttons, toggles, active tabs. Pressed: `:active`, `aria-expanded`, + `data-pressed`, the active sidebar item. Sunken: inputs, selects, textareas, + checkboxes, tab lists, code panels. Window: cards, dialogs, sheets, popovers, menus. +- `--face` / `bg-face` is the neutral raised surface (outline buttons, disabled + buttons). Disabled buttons sit on the face with embossed muted text, not opacity. +- `Window` + `WindowTitleBar` + `WindowBody` is the titled frame for grouped content and + focused flows (login, node panels). `Card` is the untitled frame. +- Motion: the pressed bevel is the press feedback (no `scale`, no `translate`). Press is + instant, release eases out in 120ms (`transition-bevel`), on named properties only. + Tailwind's `hover:` is already gated to hover-capable pointers. +- Mobile first: windows go full-bleed with margins, dialogs become bottom sheets. +- `plugins/auth/ui` duplicates the primitives and the frame utilities; change both. + ## Principles 1. **One job per screen.** Every page answers one question or completes one task. If a @@ -50,7 +78,7 @@ Geist Mono for code and identifiers. | Use | Classes | |---|---| -| Display (landing hero) | `text-5xl sm:text-6xl font-semibold` | +| Display (landing hero) | `text-4xl sm:text-5xl font-semibold` | | Page title (`h1`) | `text-3xl sm:text-4xl font-semibold` | | Section title (`h2`) | `text-xl font-semibold` | | Card / group title (`h3`) | `text-lg font-medium` | @@ -66,8 +94,8 @@ everywhere. - Page container: `PageContainer` (`narrow` forms · `default` · `wide` dashboards). - Vertical rhythm: `gap-12` between page sections, `gap-6` inside a section, `gap-3` inside a group. Prefer `flex flex-col gap-*` over `space-y-*`. -- Radius comes from the scale (`rounded-lg` … `rounded-4xl`); buttons and inputs are - pill-shaped by default (maia) — don't override. +- Everything is square (lyra, `--radius: 0`). Don't add radius back with arbitrary + values. ## Components @@ -80,27 +108,36 @@ and JSON. | Component | Sizes | Notes | |---|---|---| -| Button | `sm` 36px · `default` 44px · `lg` 48px · `icon*` | One `default`-variant button per view; everything else `outline`, `secondary` or `ghost`. Links rendered as buttons use `render={}` + `nativeButton={false}`. | -| Input / Select / Textarea | `default` 44px | Label above, one line of helper text below via `Field`. | +| Button | `xs` 28px · `sm` 36px · `default` 44px · `lg` 48px · `xl` 56px · `icon*` | Raised bevel, presses in. One `default`-variant button per view; everything else `outline` (face), `secondary` (card), `ghost` (flat until hover) or `destructive` (tinted). `brand` is for the rare hero moment. Links rendered as buttons use `render={}` + `nativeButton={false}`. | +| Input / Select / Textarea | `default` 44px | Sunken on `bg-background`. Label above, one line of helper text below via `Field`. | | Badge | `default`, `secondary`, `outline`, `success`, `warning`, `destructive` | Status only — never as decoration. | -| Card | — | Only for openable / actionable items and grouped forms. | +| Card | — | Window frame, no title bar. Only for openable / actionable items and grouped forms. | +| Window | — | Window frame with a muted title strip (`WindowTitleBar`: icon, title, actions, close). For titled groups and focused flows. | ## Page patterns - **Page:** `PageContainer` → `PageHeader` (title, one-line description, primary action) → sections. +- **Landing (`/`):** no app shell, signed in or not. Centered title (`text-4xl + sm:text-5xl`), the under-construction GIF linking to the source, one `lg` button + (Sign in / Open my node), then the public footer. - **Section:** `SectionHeader` (title + optional action) → content. - **Empty state:** icon, one-line title, one sentence, one action. -- **Step flow:** multi-step tasks (apply, create tenant) show steps as a numbered list; +- **Step flow:** multi-step tasks (create organization, swap a node's UI) show steps as a numbered list; completed steps collapse to a single checked row. - **Destructive actions:** live at the bottom of a page in a "Danger zone" section and always confirm through `ConfirmDialog`. ## Glossary (use exactly) -Home (`/dashboard`) · Explore (`/explore`) · Directory (`/discover`) · My community -(`/dashboard/node`) · Events & profile (`/nodes/$id/content`) · Proposals · Community -settings (`/tenant/$id`) · Organizations · Stake · Start a community (`/apply`) · Settings. +Nouns follow `GLOSSARY.md` (node, owner, extends, composition, surface, contract). +Navigation names: + +My node (signed-in home) · Registry · Organizations (`/settings/organizations`) · +Example (Things) · Settings · Docs (`/about`) · Skill (`/skill`) · Admin. + +Say "node", never "runtime" or "tenant", in UI copy. A composed node is a "plugin" only +when describing its role inside another node. ## Test ids diff --git a/ui/components.json b/ui/components.json index 62a6c4546..846e7dc2a 100644 --- a/ui/components.json +++ b/ui/components.json @@ -1,6 +1,6 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-maia", + "style": "base-lyra", "rsc": false, "tsx": true, "tailwind": { diff --git a/ui/src/assets/under-construction.gif b/ui/src/assets/under-construction.gif new file mode 100644 index 000000000..ac939c8e8 Binary files /dev/null and b/ui/src/assets/under-construction.gif differ diff --git a/ui/src/components/bulletin.test.tsx b/ui/src/components/bulletin.test.tsx index 50d006599..a8bfeb04b 100644 --- a/ui/src/components/bulletin.test.tsx +++ b/ui/src/components/bulletin.test.tsx @@ -30,6 +30,6 @@ describe("Bulletin", () => { runtimeConfig={{ repository: "https://github.com/example/example" }} />, ); - expect(html).toContain("In progress"); + expect(html).toContain('href="https://github.com/example/example"'); }); }); diff --git a/ui/src/components/bulletin.tsx b/ui/src/components/bulletin.tsx index 225e01681..ac060c711 100644 --- a/ui/src/components/bulletin.tsx +++ b/ui/src/components/bulletin.tsx @@ -34,9 +34,9 @@ export function Bulletin({ content, className, runtimeConfig }: BulletinProps) { />
diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 099d9408a..854f40863 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -90,4 +90,5 @@ export { ScrollArea, ScrollBar } from "./ui/scroll-area"; export { Skeleton } from "./ui/skeleton"; export { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; export { Textarea } from "./ui/textarea"; +export { Window, WindowBody, WindowTitleBar } from "./ui/window"; export { UnderConstruction } from "./under-construction"; diff --git a/ui/src/components/ui/badge.tsx b/ui/src/components/ui/badge.tsx index b89828880..254809d92 100644 --- a/ui/src/components/ui/badge.tsx +++ b/ui/src/components/ui/badge.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "cn"; const badgeVariants = cva( - "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", { variants: { variant: { diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx index fc49ffdfc..789a1d60b 100644 --- a/ui/src/components/ui/button.tsx +++ b/ui/src/components/ui/button.tsx @@ -5,27 +5,31 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "cn"; const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-none border-0 text-sm font-medium whitespace-nowrap transition-bevel outline-none select-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:bg-face disabled:text-muted-foreground disabled:text-emboss aria-disabled:pointer-events-none aria-disabled:bg-face aria-disabled:text-muted-foreground aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", + default: + "bg-primary text-primary-foreground bevel-raised hover:bg-primary/90 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", + brand: + "bg-brand text-brand-foreground bevel-raised hover:bg-brand/90 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", outline: - "border-border bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground", + "bg-face text-foreground bevel-raised hover:bg-accent hover:text-accent-foreground active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-accent aria-expanded:bevel-pressed data-pressed:bevel-pressed", secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + "bg-card text-card-foreground bevel-raised hover:bg-muted active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-muted aria-expanded:bevel-pressed data-pressed:bevel-pressed", ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + "bg-transparent text-foreground bevel-none hover:bg-muted hover:bevel-raised active:not-aria-[haspopup]:bevel-pressed aria-expanded:bg-muted aria-expanded:bevel-pressed data-pressed:bg-accent data-pressed:bevel-pressed disabled:bg-transparent aria-disabled:bg-transparent", destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", + "bg-destructive-muted text-destructive-muted-foreground bevel-raised hover:bg-destructive hover:text-destructive-foreground focus-visible:ring-destructive/30 active:not-aria-[haspopup]:bevel-pressed aria-expanded:bevel-pressed data-pressed:bevel-pressed", + link: "bg-transparent text-primary bevel-none underline-offset-4 hover:underline disabled:bg-transparent aria-disabled:bg-transparent", }, size: { default: "h-11 gap-2 px-5 text-base has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4", - xs: "h-7 gap-1 px-3 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", - sm: "h-9 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + xs: "h-7 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", + sm: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", lg: "h-12 gap-2 px-6 text-base has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg:not([class*='size-'])]:size-5", + xl: "h-14 gap-2.5 px-8 text-lg has-data-[icon=inline-end]:pr-7 has-data-[icon=inline-start]:pl-7 [&_svg:not([class*='size-'])]:size-5", icon: "size-11", "icon-xs": "size-7 [&_svg:not([class*='size-'])]:size-3", "icon-sm": "size-9", diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx index 77dc17cf4..c42cb6ba2 100644 --- a/ui/src/components/ui/card.tsx +++ b/ui/src/components/ui/card.tsx @@ -11,7 +11,7 @@ function Card({ data-slot="card" data-size={size} className={cn( - "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-2xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-sm text-card-foreground bevel-window [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", className, )} {...props} diff --git a/ui/src/components/ui/checkbox.tsx b/ui/src/components/ui/checkbox.tsx index 86eba6ebe..be7957d17 100644 --- a/ui/src/components/ui/checkbox.tsx +++ b/ui/src/components/ui/checkbox.tsx @@ -9,7 +9,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { ) { data-slot="input-group" role="group" className={cn( - "group/input-group relative flex h-11 w-full min-w-0 items-center rounded-4xl border border-input bg-input/30 transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-2xl has-data-[align=block-start]:rounded-2xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + "group/input-group relative flex h-11 w-full min-w-0 items-center rounded-none border-0 bg-background bevel-sunken transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-2xl has-data-[align=block-start]:rounded-2xl has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-xl has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className, )} {...props} diff --git a/ui/src/components/ui/input.tsx b/ui/src/components/ui/input.tsx index c6be88f9c..defbaeba4 100644 --- a/ui/src/components/ui/input.tsx +++ b/ui/src/components/ui/input.tsx @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "h-11 w-full min-w-0 rounded-4xl border border-input bg-input/30 px-4 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", + "h-11 w-full min-w-0 rounded-none border-0 bg-background bevel-sunken px-4 py-1 text-base transition-colors outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className, )} {...props} diff --git a/ui/src/components/ui/popover.tsx b/ui/src/components/ui/popover.tsx index 25685d575..18b986476 100644 --- a/ui/src/components/ui/popover.tsx +++ b/ui/src/components/ui/popover.tsx @@ -31,7 +31,7 @@ function PopoverContent({ ) { } const sidebarMenuButtonVariants = cva( - "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding,background-color,color] duration-150 group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", + "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg px-3 py-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding,background-color,color] duration-150 group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:bevel-pressed data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", { variants: { variant: { default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", outline: - "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]", + "bg-face bevel-raised hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", }, size: { default: "h-9 text-sm", diff --git a/ui/src/components/ui/tabs.tsx b/ui/src/components/ui/tabs.tsx index 7ea26d8f0..ae730a500 100644 --- a/ui/src/components/ui/tabs.tsx +++ b/ui/src/components/ui/tabs.tsx @@ -16,11 +16,11 @@ function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive } const tabsListVariants = cva( - "group/tabs-list inline-flex w-fit items-center justify-center rounded-4xl p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl", + "group/tabs-list inline-flex w-fit items-center justify-center rounded-none p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl", { variants: { variant: { - default: "bg-muted", + default: "bg-muted bevel-sunken", line: "gap-5 rounded-none bg-transparent p-0 group-data-horizontal/tabs:h-11 group-data-vertical/tabs:gap-1 group-data-vertical/tabs:rounded-none", }, }, @@ -51,7 +51,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { data-slot="tabs-trigger" className={cn( "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", + "data-active:bg-background data-active:text-foreground data-active:bevel-raised group-data-[variant=line]/tabs-list:data-active:bevel-none dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "group-data-[variant=line]/tabs-list:h-full group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-0 group-data-[variant=line]/tabs-list:px-0.5 group-data-[variant=line]/tabs-list:py-0 group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-vertical/tabs:group-data-[variant=line]/tabs-list:px-3 group-data-vertical/tabs:group-data-[variant=line]/tabs-list:py-2", "group-data-[variant=line]/tabs-list:after:absolute group-data-[variant=line]/tabs-list:after:rounded-full group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=line]/tabs-list:after:opacity-0 group-data-[variant=line]/tabs-list:after:transition-opacity group-data-horizontal/tabs:group-data-[variant=line]/tabs-list:after:inset-x-0 group-data-horizontal/tabs:group-data-[variant=line]/tabs-list:after:bottom-0 group-data-horizontal/tabs:group-data-[variant=line]/tabs-list:after:h-0.5 group-data-vertical/tabs:group-data-[variant=line]/tabs-list:after:inset-y-1 group-data-vertical/tabs:group-data-[variant=line]/tabs-list:after:left-0 group-data-vertical/tabs:group-data-[variant=line]/tabs-list:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className, diff --git a/ui/src/components/ui/textarea.tsx b/ui/src/components/ui/textarea.tsx index bf7bcc2f1..4b05db4c3 100644 --- a/ui/src/components/ui/textarea.tsx +++ b/ui/src/components/ui/textarea.tsx @@ -6,7 +6,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {