// تثبيت المهارة
تثبيت المهارة
المهارات هي كود تابع لأطراف ثالثة من مستودعات GitHub العامة. يفحص SkillHub الأنماط الخبيثة المعروفة، لكنه لا يستطيع ضمان السلامة. راجع الكود المصدري قبل التثبيت.
تثبيت عام (على مستوى المستخدم):
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancementتثبيت في المشروع الحالي:
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancement --projectاستخدمه مع Hermes أو أي runtime آخر. تُكتب المهارة في <directory>/<skill-name>/.
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancement --target-dir /path/to/skillsالمسار المقترح: ~/.claude/skills/progressive-enhancement/
محتوى SKILL.md
---
name: progressive-enhancement
description: Use when a page must paint before its JavaScript runs.
version: 1.0.0
author: TimeLine
license: MIT
platforms: [macos]
metadata:
hermes:
tags: [astro, solid, hydration, no-js, audit, cdp]
---
# Progressive Enhancement
The site is a static archive behind a service worker, so a reader with a cold cache, a blocked
script or a browser that never runs one must still see the archive. Every surface therefore has two
jobs: paint itself from the build, then hydrate for interaction. A surface that renders its content
only after hydration has one job, and it is the wrong one.
The rule is one sentence: the first paint is the server's, and JavaScript only makes it interactive.
## The first paint is the server's
A page reads its content at build time from the content collections and renders it into HTML. That
is what the service worker caches and what a script-free reader sees.
- **Props carry the data; the island never fetches it.** `Source/pages/Workspace/index.astro` reads
the collections and hands `Stages`, `Columns` and `Tiles` to the islands as plain objects. What a
person is doing right now - which stage is selected, which record is open, which tiles were
published - lives in `Source/Script/Workspace/Store.ts`, which every island on the page reads. An
island that fetched its own content would render an empty frame first.
- **`client:visible` over `client:load`.** A `client:load` island competes with the first paint for
the main thread; `client:visible` lets the canvas paint and boots when the island is on screen.
The workspace fills the viewport, so it hydrates immediately in practice - the directive only
changes who wins the first frame.
- **`client:only` never sits on a content surface.** It renders nothing at build time, so the page
ships an empty box and a script fills it. The audit fails on it outright.
- **One visible `<h1>` per page, on every surface.** It is the page's own name and the only heading
a screen reader can outline the document from, so it is the server's to draw. The workspace's is
the drawer's title (`My Files`): the composition draws no heading above the frame, and the title
keeps its 13px semibold face because the level is the document's, not a type step.
- **Interaction may be absent without scripts.** A button that does nothing is acceptable; a row, a
tile or a heading that is missing is not. That distinction is the whole scope of this skill.
## A visible island needs a rendered frame
`client:visible` observes the island's children with an `IntersectionObserver`, and the observation
only delivers while the page is being rendered. Two conditions break it, both of which look like a
broken component rather than a broken environment:
- **A background tab is never rendered.** A target created with `background: true` - the polite way
to open a tab in someone's own browser - receives no rendering updates, so the observer stays
silent and the island never hydrates. Bring the tab to the front (`Page.bringToFront`) before
measuring for this reason. A real reader's tab is in front, so this is an audit-environment rule,
not a product defect.
- **An observer target with no box never intersects.** Astro styles `astro-island` as
`display: contents`, which is why the directive observes children rather than the island itself. A
child that is itself `display: contents`, `display: none` or zero-height keeps the island dormant.
A `client:visible` island that stays static is diagnosed by reading the `ssr` attribute: the runtime
removes it on hydration, so `document.querySelector("astro-island[ssr]")` still matching means the
directive never fired. Report that as `js=static` and say the parity comparison measured two static
renders - a parity that holds between two static pages proves nothing about hydration.
## The audit
It is two passes over every built route, and it is run by hand: launch a headless browser on a
throwaway profile, point it at a copy of `Target/` served on loopback, and load each route twice
over CDP - once with `Emulation.setScriptExecutionDisabled`, once normally.
- **The script-free pass is the measurement.** `Runtime.evaluate` still answers while page script
execution is disabled - the switch stops the document's own scripts, not the debugger's - so the
pass reports what the first paint actually shows, with layout. "Visible" is a layout fact: a node
inside `display: none` or a zero-height wrapper is present in the markup and absent from the page.
- **Parity is the rule that catches the defect.** For every selector a route profile names, the
script-free render must show at least as much as the hydrated one. Content that appears only in
the second pass fails with `content waits for hydration`.
- **The read is a snapshot, not the build directory.** `astro build` empties `Target/` before it
writes, so a run that starts mid-build sees an empty tree and reports every route as serving no
document - a total failure that looks exactly like the defect this exists to catch. The audit
waits for the newest mtime under `Target/` to be older than 1.5s, copies the tree aside, checks
that every copied document is complete and that the route set matches the live one, and serves the
copy. A build that starts after the copy cannot take the pages away from the browser.
- **Every route gets the generic contract, profile or not.** A non-empty `<title>`, at least 40
characters of prose under `<main>`, exactly one visible `<h1>`, no island unless the profile
allows one, no `client:only` island, and server-rendered content inside every island. A route with
no profile is reported as `generic`, so a new page cannot pass by being unknown.
- **A failing route is named, never dropped.** Write the report as one line per route, pass or fail.
A route missing from the report is indistinguishable from a route that passed, and the second is
the claim the audit exists to make.
The browser binary is configuration, never a constant: the first installed candidate will do -
Chrome, Chromium, Brave, a Playwright cache - so a clone on another machine needs no edit.
## One store, two islands
A page that draws a rail and a body mounts two islands, and both read
`Source/Script/Workspace/Store.ts`. A signal inside each component would make them two workspaces
that happen to look alike: press a band in the rail and the seam beside it would still name the old
stage, and neither would be wrong about a value the other never had.
- **State lives in the store; data arrives as props.** The store owns the selected stage, the live
column, the view, the open record, the published tile set and the session slot. The stages, the
columns and the tiles stay with the page, which derives them at build time; a second copy in the
store would be a second source of truth.
- **`Adopt` is what makes one store safe to mount twice.** The first island to render seeds a field
and a later island fills only what is still empty, so the order two islands hydrate in cannot
change what is on screen - and hydration never resets a server-rendered view to empty.
- **One instance per page, one per render on the server.** The module is evaluated once in the
browser, so every island that imports it reads the same value. A build renders every route in one
process, so `store()` hands each render its own instance; a singleton there would carry one page's
selection into the next page's HTML.
- **A session read happens on mount, never during render.** The server's HTML and the first client
render are then the same anonymous slot, so hydration has nothing to disagree with. A failed read
keeps that slot, and the session layer states the failure once.
The check for this is three passes of the built workspace page, each in its own tab - anonymous, a
session element that is not JSON, and a readable session - read per pass:
- two islands on the page, both hydrated (a pass that never hydrates reports it instead of measuring
a static render);
- a press on a rail band that moves the seam beside it, which is the cross-island proof: the rail
island dispatches and the body island renders, so a store that is not shared leaves the seam
naming the stage it opened on;
- a seam that is `locked` and names no stage while there is no identity, and offers the verb once
the session names one;
- exactly one warning for a session element that cannot be read, and none for a page that embeds
none;
- a publish that lands one more filled tile once an identity exists - skipped, not failed, when the
archive holds no drawer rows, because a fresh clone is a normal state.
The session element is spliced into the served HTML rather than injected in the browser: it has to
be in the document before any island script runs, or the read races hydration. The press check is
falsifiable by construction - it requires the seam to name a stage it did not name before the press.
## Adding a route
A new page is audited the moment it builds, and its expectations are written down before the page is
read: the route, how many islands it may mount, a minimum visible count per selector it must show,
and any group of alternatives where one has to hold.
The workspace's profile is the shape: two islands; at least one `.rail`, one `.drawer` and four
`section.drawer-column`; and, as the one alternative, at least one `main ol` or one `main p`. The
alternative is how an empty archive passes: the page shows rows when there is content and a note
when there is none, and both are complete first paints. The island count caps hydration boundaries
and defaults to none, so a page that grows an island without a reason fails. A dynamic route names
its variable segment with `:`, so `/Bookmark/:Id/` covers every built post.
A profile names what the design promises, never the markup a page happens to use:
- **Elements with a meaning** - `main section`, `main ol`, `main article`, `main a[href]`.
- **The parts the composition makes contractual** - `.rail`, `.drawer`, `.drawer-column`,
`.swimlane`, `.spectrometer__cell`, `.status__value`, `.security__mark`, `.provenance__band`.
These are the names `Documentation/Reference/Design-Composition.md` gives its own parts.
- **The empty state a page renders when the archive holds nothing** - `.drawer__empty`.
**A selector is a claim about the DOM, never about the source.** The audit runs
`document.querySelector` inside the page, so it is immune to the minifier - but it is not immune to
a redesign, and the two look the same when a route fails. A profile pinned to `.roster__row` or
`article.sheet` fails the day the surface is redrawn, and a redraw is not a no-script regression:
when six of nine routes fail while each still renders its text with zero islands, the content
contract held and the selector profile went stale. Assert what the design guarantees, and let a
per-route profile name only classes the composition spec makes contractual. A grep over `Target/` is
the weaker check in both directions: `@playform/compress` strips attribute quotes, so `class="foo"`
matches nothing on a page full of `class=foo`. The heading is the clearest case: the rules name
`h1`, never `h1.board__title`, because the class belongs to the component that draws the page while
"one visible heading" is the contract a screen reader depends on.
Expectations are structural selectors, not counts of records: a count that depends on the archive
would make the audit fail on a fresh clone, which is a normal state.
## Proving the audit bites
An audit that cannot fail is decoration. Build two pages that satisfy the structural rules and break
them at render time - one draws a rail band from a script and carries an island with no
server-rendered content, the other has lost its heading and its prose - and require the audit to
fail on both before trusting a clean run.
Both fixtures must fail, and on the rules they were built to break: hydration parity, an empty
island and a lost heading. A fixture that passes means a rule stopped catching the defect it exists
for, and the fixture's report is where the gap shows. Build the fixtures from the current rules
rather than keeping them beside them: a fixture archived next to the rules drifts the moment a rule
changes, and then the control tests the fixture instead of the audit. Run the negative control after
changing the rules.
A build that is still writing is not a verdict: the audit waits for the tree to settle and then
measures a copy, so a run that overlaps `astro build` reports what the finished build renders. A
page that genuinely lost its content fails the generic contract - the `<main>` text floor and the
one-visible-heading rule - before any profile is consulted, so the two failures cannot be confused
for one another.
## Claim → test
| Claim | Test |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Every built route renders without scripts | the two-pass audit reports one `PASS` line per route |
| The workspace island hydrates | the audit's `/Workspace/` line ends in `js=hydrated` |
| A route's content is not hydration-only | the route line carries no `content waits for hydration` failure |
| Every route shows one visible heading | every `PASS` line carries `h1=1` |
| A route mounts an island only where expected | the `/Workspace/` line carries `islands=2` and every other route `islands=0`, except the post's `islands=1` |
| The audit catches the defect it exists for | both negative-control fixtures fail on hydration parity, an empty island and a lost heading |
| The audit reads a settled build | run `pnpm run build` in the background and audit while it runs: one `PASS` line per route |
| The island boots after the canvas paints | the built HTML carries `<astro-island ... client=visible>`; `grep -c 'client=load' Target/Workspace/index.html` is 0 |
| Two islands read one store | the readable-session pass reports the pressed stage in the seam |
| A failed session read is one warning | the audit's malformed pass reports exactly one warning and `state=static` |
| An empty session slot hydrates cleanly | the audit's anonymous pass reports no error, `state=static` and a `locked` seam |
الترخيص
الترخيص المُعلن: MIT
MIT License
Copyright (c) 2026 TimeLine
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.عرض الترخيص في المستودع المصدري — النسخة المنشورة هناك هي المرجع.
// تثبيت المهارة
تثبيت المهارة
المهارات هي كود تابع لأطراف ثالثة من مستودعات GitHub العامة. يفحص SkillHub الأنماط الخبيثة المعروفة، لكنه لا يستطيع ضمان السلامة. راجع الكود المصدري قبل التثبيت.
تثبيت عام (على مستوى المستخدم):
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancementتثبيت في المشروع الحالي:
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancement --projectاستخدمه مع Hermes أو أي runtime آخر. تُكتب المهارة في <directory>/<skill-name>/.
npx skillhub install TimeLine-DELO/TimeLine/progressive-enhancement --target-dir /path/to/skillsالمسار المقترح: ~/.claude/skills/progressive-enhancement/