diff --git a/.agents/skills/webjs/SKILL.md b/.agents/skills/webjs/SKILL.md index d2d9b071d..0591ba15c 100644 --- a/.agents/skills/webjs/SKILL.md +++ b/.agents/skills/webjs/SKILL.md @@ -80,6 +80,7 @@ The table above routes by the job; this one routes by the topic, for when you al | Server actions, mutations, queries, validation, the `ActionResult` envelope | `references/data-and-actions.md` | | Sessions, login flows, route protection, `forbidden()` / `unauthorized()` | `references/auth-and-sessions.md` | | Tailwind, light-DOM tag-prefix rule, tokens, fixed headers, no-reflow layout | `references/styling.md` | +| Where a repeated markup helper lives (`utils/ui/` vs `lib/`), and fragment vs display-only component | `references/styling.md` | | Client router, prefetch, frames, view transitions, Suspense streaming | `references/client-router-and-streaming.md` | | Optimistic UI for a user-facing mutation | `references/optimistic-ui.md` | | The `@webjsdev/ui` component kit (a `components.json` is present): class helpers, tokens, `add` / `view`, the MCP `ui` tool | `references/ui-kit.md` | @@ -122,8 +123,10 @@ app/ ROUTING ONLY (thin adapters importing from modules/) error.ts loading.ts not-found.ts forbidden.ts unauthorized.ts boundaries (nearest wins) middleware.ts root middleware modules// actions/ (mutations, *.server.ts), queries/ (reads, *.server.ts), - components/, utils/ (pure), types.ts -lib/ lib/*.server.ts server-only infra, lib/utils/ browser-safe helpers + components/ (custom elements), types.ts, + utils/ (pure; returns data, or an html fragment under utils/ui/) +lib/ lib/*.server.ts server-only infra, lib/utils/ browser-safe helpers, + lib/utils/ui.ts app-wide html fragments (lib/ui/ once they grow) components/*.ts shared presentational custom elements (one per file) db/*.server.ts Drizzle: schema, connection public/* static assets, served at /public/ diff --git a/.agents/skills/webjs/references/styling.md b/.agents/skills/webjs/references/styling.md index 18f8e3200..40fff09a6 100644 --- a/.agents/skills/webjs/references/styling.md +++ b/.agents/skills/webjs/references/styling.md @@ -36,7 +36,20 @@ When custom CSS IS unavoidable inside a light-DOM component, the tag-prefix inva ## DRY via a JS helper, not `@apply` -When the same Tailwind bundle repeats across 2+ places, extract it into a helper in `lib/utils/ui.ts` that returns an `html` fragment (SSR-time, no client runtime, output identical to inline classes): +When the same Tailwind bundle repeats across 2+ places, extract it into a helper that returns an `html` fragment (SSR-time, no client runtime, output identical to inline classes). Where the helper LIVES follows the narrowest-owner rule, so pick the tier by who consumes it: + +| Consumers | Home | +|---|---| +| routes across the app (a heading, a lede, a back link) | `lib/utils/ui.ts` | +| one feature (a todo row, a comment card, a board) | `modules//utils/ui/.ts` | + +One file per fragment under `utils/ui/`, because a feature accumulates several and one-per-file keeps them greppable. A fragment promotes from the feature tier to `lib/` only when a second feature genuinely consumes it. + +The app-wide tier grows the same way, on the same judgment `references/module-structure.md` applies to any module. `lib/utils/ui.ts` is where it starts and where it usually stays: small, independent, one-element helpers belong together in one file, however many of them there are (the blog example keeps nine there quite happily). Split to `lib/ui/.ts`, one file per fragment, when a fragment stops being a one-liner, when one composes others, or when the single file is no longer scannable. The framework's own website crossed that line and its four composed page fragments live in `lib/ui/`. + +So `modules//utils/ui/`, `lib/utils/ui.ts`, and `lib/ui/` are one convention at three sizes rather than three conventions, and the `ui` segment is the part carrying the meaning at every one of them: inside `modules//`, `components/` holds custom elements, `utils/ui/` holds functions returning a `TemplateResult`, and the rest of `utils/` holds functions returning data. Drop the segment and a view fragment ends up beside a pure data helper with nothing in the path to tell them apart. + +The example below is the app-wide tier: ```ts import { html } from '@webjsdev/core'; @@ -62,12 +75,44 @@ export default function Post({ params }) { | Repeats | Action | |---|---| | Once | Inline the classes. | -| 2 to 3 times, identical | Extract to `lib/utils/ui.ts`. | +| 2 to 3 times, identical, inside ONE feature | Extract to `modules//utils/ui/.ts`. | +| 2 to 3 times, identical, across features or routes | Extract to `lib/utils/ui.ts`. | | Varies by 1 to 2 props | Extract with a small parameter (`mb: 'sm' \| 'md'`). | | Radically different per call site | Keep inline, do not force-fit. | Avoid `@apply`: it hides which utilities a class uses and creates a second source of truth. A JS helper keeps the bundle visible at the definition site, composes with conditional classes and active states, and runs at SSR time. +### Fragment or display-only component? + +Two questions, in order. + +**First, is this a UNIT or a repeated CLASS BUNDLE?** The helpers this section began with (a heading, a lede, a back link) are the second kind: one element with a class list you did not want to type twice. That is a fragment by definition and never a component; nobody wants `` as a tag in their DOM, and promoting a class bundle to an element is the same over-reach as absorbing a page section into an island. The question below only arises for a genuine unit of markup (a row, a card, a board) that could reasonably be either. + +**Second, for a unit: can the markup carry an extra wrapper element at all?** A component is a tag in the DOM, so choosing one adds a node between the parent and the markup. Usually that is fine and the component is the better choice, since it gets a tag name to target and can grow behaviour later. Two cases make it impossible outright: + +| Case | What happens | +|---|---| +| a `` / `` child | the parser FOSTER-PARENTS the element out of the table (an HTML spec rule, not a WebJs one), so it lands BEFORE the table and its cells are adopted by a `` it no longer owns. The component never renders where you put it | +| output that is not DOM | a `` payload, or HTML a `route.ts` returns, is a STRING, and a component has no way to produce one | + +Three more render fine and are wrong in ways that surface later, so treat them as strong reasons rather than hard blocks. Measured in Chromium, the element survives in all three: + +| Case | What survives, what breaks | +|---|---| +| a `
    ` / `
      ` / `
      ` child | the list renders, but `ul > li`, `:nth-child`, and list markers now see the wrapper instead of the row | +| a `
` child, which the parser foster-parents out per the HTML spec, and string output such as a `` payload) or where it lands wrong (a `
    ` / `
    ` / `