@passioncode-ai/fabric-agent-adapter 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +14 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/hooks/hooks.json +17 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +21 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +110 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +198 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +253 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/themes.json +350 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +133 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +295 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +31 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +437 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +51 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +65 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +21 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-10-11
|
|
4
|
+
|
|
5
|
+
The dashboard kit (`fabric-dashboard/0.1`, contract DEC-0036): a Fabric service's dashboard holds inside a
|
|
6
|
+
host pane beside the agent console from 360 CSS px up, follows the host's colour scheme and theme, and says
|
|
7
|
+
so in its well-known document. The contract pin moves to Fabric Agent Contract `main` `a83bec6` (the
|
|
8
|
+
DEC-0036 merge, contract PR #27). Bundles issued under the DEC-0035, DEC-0033, DEC-0031, DEC-0024, DEC-0021,
|
|
9
|
+
DEC-0020 and DEC-0019 revisions stay valid.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`building-fabric-services/assets/dashboard-kit/`** (FAA-17): `fabric-dashboard.css` (the workbench token
|
|
14
|
+
layer plus `--edge`; root `min-width: 360px`; fluid grids; navigation that wraps, or folds into a More menu
|
|
15
|
+
with the script; `.fd-scroll` boxes with edge shades, a visible scrollbar and keyboard focus; sticky chrome
|
|
16
|
+
kept within min(120 px, a quarter of the height) by letting the header scroll away), `fabric-dashboard.js`
|
|
17
|
+
(applies the `fabric_appearance` cookie, hides the page's own appearance controls while the host owns
|
|
18
|
+
them, re-applies on cookie change and visibility, folds navigation, names scroll boxes, measures the chrome
|
|
19
|
+
for anchors; CSP-clean), seven reference themes in `themes.json` (graphite, fabric, paper, forest, ocean,
|
|
20
|
+
ember, contrast; light and dark; every pair WCAG 2.2 AA, measured by `test/test_dashboard_kit.py`),
|
|
21
|
+
`kit.json` (version, theme ids, file digests and per-version history) and `demo.html`.
|
|
22
|
+
- **`scripts/dashboard_kit.py`** `status | install | build | notice`: `install` never overwrites a copy the
|
|
23
|
+
service changed without `--force`; `build` regenerates the themes from `themes.json`.
|
|
24
|
+
- **`scripts/check_dashboard_layout.mjs`**: renders a page in WebKit (or `--browser chromium|firefox`) at the
|
|
25
|
+
floor minus 40 and 360, 480, 768, 1280 px and reports ED-1…ED-5; `--service <id>` signs in with a login
|
|
26
|
+
code and reads the declared `minWidth`. Without Playwright every rule is `NOT_RUN`. It measures a settled
|
|
27
|
+
page: after the load it waits for the page's own requests to finish, and at each width for two frames,
|
|
28
|
+
250 ms and two more frames, so a header that grows when its data arrives is not measured between its new
|
|
29
|
+
size and the kit's reaction to it (seen once on the brand board at 360 px: a false ED-5).
|
|
30
|
+
- **`check_service.py` rule `dashboard.declared`** (ED-9): PASS for a valid declaration (and names a newer
|
|
31
|
+
kit), FAIL for an invalid one, NOT_RUN for no dashboard or a legacy one.
|
|
32
|
+
- **`dashboard_extension()` / `dashboardExtension()`** in both kits, and `extensions` in `build_well_known` /
|
|
33
|
+
`buildWellKnown` (a dashboard declaration in it is validated). The sample service's dashboard is built on
|
|
34
|
+
the kit and declares it.
|
|
35
|
+
- **The plugin's first hook**: `SessionStart` runs `dashboard_kit.py notice`, which prints one line in a
|
|
36
|
+
repository whose dashboard has no kit or an older one and nothing elsewhere; it writes nothing and always
|
|
37
|
+
exits 0. `AGENTS.md` Lifecycle (LC-09) declares it.
|
|
38
|
+
- **`references/dashboard-kit.md`**: the rules, the markup, the themes, the declaration, the probes and
|
|
39
|
+
"Upgrading an existing dashboard" (only with the operator's go-ahead). Scenario evals
|
|
40
|
+
`dashboard-in-a-narrow-host-pane`, `dashboard-kit-upgrade-notice`, `dashboard-follows-host-appearance`
|
|
41
|
+
and two trigger queries came first.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **Default contract pin `52da526` → `a83bec6b47776af688402c18efec507143061f68`.** `52da526` (DEC-0035) joins
|
|
46
|
+
`SUPPORTED_CONTRACT_COMMITS`. The revisions between add only the `fabric-dashboard/0.1` profile and
|
|
47
|
+
`service-dashboard-extension.schema.json`; the provider manifest and the MCP, A2A and local-runner
|
|
48
|
+
profiles do not change, and every descriptor and well-known document the kits wrote before stays valid.
|
|
49
|
+
- `references/dashboard.md` point 8 sends the page to the kit; `references/protocol.md` names the
|
|
50
|
+
`extensions` dashboard declaration.
|
|
51
|
+
|
|
3
52
|
## 0.8.3 - 2026-10-10
|
|
4
53
|
|
|
5
54
|
The contract pin moves to Fabric Agent Contract `main` `52da526` (DEC-0034 and DEC-0035, the operator
|
package/README.md
CHANGED
|
@@ -33,6 +33,17 @@ complete `scripts/sample_service.py`, and `scripts/check_service.py`, a live pro
|
|
|
33
33
|
python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
The dashboard itself is built from the **dashboard kit** (`assets/dashboard-kit/`, `fabric-dashboard/0.1`,
|
|
37
|
+
contract DEC-0036): one stylesheet and one script that keep the page usable from 360 CSS px inside a host
|
|
38
|
+
pane beside the agent console — navigation that folds into a More menu, wide tables in their own scroll
|
|
39
|
+
box, sticky chrome at most 120 px — with seven WCAG AA themes (light and dark) that follow the host's
|
|
40
|
+
`fabric_appearance` cookie. `scripts/dashboard_kit.py install|status` copies and checks it,
|
|
41
|
+
`scripts/check_dashboard_layout.mjs` measures a rendered page in WebKit at 360, 480, 768 and 1280 px
|
|
42
|
+
(Playwright; `NOT_RUN` without it), and the probe's `dashboard.declared` checks the declaration. The
|
|
43
|
+
plugin's one hook (SessionStart) prints a single line in a repository whose dashboard has no kit or an
|
|
44
|
+
older one, and nothing anywhere else; the [kit reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md)
|
|
45
|
+
has the upgrade steps, taken only with the operator's go-ahead.
|
|
46
|
+
|
|
36
47
|
Dashboard handoffs use the host-generated `open_link` as the primary action. The
|
|
37
48
|
three skills carry the same [consumer procedure](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md)
|
|
38
49
|
and a portable `scripts/check_dashboard_link.py` gate for generated renderers.
|
|
@@ -191,7 +202,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
|
|
|
191
202
|
The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
|
|
192
203
|
|
|
193
204
|
- version: `0.1.0`
|
|
194
|
-
- commit: `
|
|
205
|
+
- commit: `a83bec6b47776af688402c18efec507143061f68`
|
|
195
206
|
|
|
196
207
|
Every other mention of the default contract revision — this section, the skills' metadata, the
|
|
197
208
|
skill card, `adapt_project.py` — must equal it; `python3 test/validate.py` fails a tree
|
|
@@ -218,8 +229,8 @@ FABRIC_CONTRACT_OLD=/path/to/legacy-contract FABRIC_CONTRACT_NEW=/path/to/curren
|
|
|
218
229
|
```
|
|
219
230
|
|
|
220
231
|
Without both environment variables, those tests explicitly skip as `NOT_RUN`.
|
|
221
|
-
This source checkout is 0.
|
|
222
|
-
released); 0.
|
|
232
|
+
This source checkout is 0.9.0. 0.8.3 is the latest published GitHub/npm release (2026-10-10; 0.7.0 was never
|
|
233
|
+
released); 0.9.0 is published by its `v0.9.0` tag. Source delivery does not update installed skills or live sessions.
|
|
223
234
|
|
|
224
235
|
## License
|
|
225
236
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.9.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "One line at session start in a repository whose Fabric dashboard has no dashboard kit or an older one (fabric-dashboard/0.1, DEC-0036). Silent everywhere else; reads only, never writes, always exits 0.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup|resume",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/building-fabric-services/scripts/dashboard_kit.py\" notice",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.9.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "a83bec6b47776af688402c18efec507143061f68"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -70,7 +70,7 @@ Use exactly:
|
|
|
70
70
|
|
|
71
71
|
- contract version `0.1.0`;
|
|
72
72
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
73
|
-
- commit `
|
|
73
|
+
- commit `a83bec6b47776af688402c18efec507143061f68`.
|
|
74
74
|
|
|
75
75
|
This is the default for new bundles. When checking an issued legacy bundle, preserve its
|
|
76
76
|
explicit supported lock and use that exact clean checkout. Never silently upgrade or
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py
CHANGED
|
@@ -16,10 +16,10 @@ from urllib.parse import urlparse
|
|
|
16
16
|
|
|
17
17
|
CONTRACT_VERSION = "0.1.0"
|
|
18
18
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
19
|
-
CONTRACT_COMMIT = "
|
|
19
|
+
CONTRACT_COMMIT = "a83bec6b47776af688402c18efec507143061f68"
|
|
20
20
|
# #region supported-contract-revisions — docs: README.md#contract-pin
|
|
21
21
|
# Only this declaration may name a legacy revision in live files.
|
|
22
|
-
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
22
|
+
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
23
23
|
# #endregion supported-contract-revisions
|
|
24
24
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
25
25
|
MCP_REVISION = "2026-07-28"
|
|
@@ -9,17 +9,17 @@ description: >-
|
|
|
9
9
|
на модели». Covers
|
|
10
10
|
fabric-service/0.1: surfaces, token and one-time login, state, one copy, launchd, descriptor,
|
|
11
11
|
well-known document, events, the usage report (spend per model); online: the https guard, the token-gated well-known document, the
|
|
12
|
-
__Host- cookie, registering it on the operator's computer;
|
|
13
|
-
sample and
|
|
12
|
+
__Host- cookie, registering it on the operator's computer; Python, Node and dashboard
|
|
13
|
+
kits, a TLS sample, live and layout probes. NOT for a one-off script or cron job, a hosted product with no agent
|
|
14
14
|
behind it, the provider manifest (adapting-projects-to-fabric), or building Fabric Dashboards.
|
|
15
15
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
16
16
|
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
17
17
|
metadata:
|
|
18
18
|
author: PassionCode.ai
|
|
19
|
-
version: "0.
|
|
19
|
+
version: "0.9.0"
|
|
20
20
|
contract-version: "0.1.0"
|
|
21
21
|
extension: "fabric-service/0.1"
|
|
22
|
-
extension-commit: "
|
|
22
|
+
extension-commit: "a83bec6b47776af688402c18efec507143061f68"
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
# Building Fabric services
|
|
@@ -198,6 +198,17 @@ while the operator types or a dialog is open, one panel's failure never blanks t
|
|
|
198
198
|
others, and every error is a sentence with the next action. Read
|
|
199
199
|
[the dashboard reference](references/dashboard.md) before building the page.
|
|
200
200
|
|
|
201
|
+
The host shows the page in a pane beside the agent console that the operator can drag wide, so
|
|
202
|
+
the page must hold from **360 CSS px** up: no sideways document scroll, navigation that wraps or
|
|
203
|
+
folds into a More menu (never a tab strip that only scrolls sideways), wide tables and logs in
|
|
204
|
+
their own focusable scroll box, sticky chrome at most 120 px, and the host's colour scheme and
|
|
205
|
+
theme followed (`fabric-dashboard/0.1`, contract DEC-0036). **Build it from the dashboard kit**
|
|
206
|
+
(`assets/dashboard-kit/`: `dashboard_kit.py install <static dir>`, seven AA themes, light and dark)
|
|
207
|
+
and declare it with `extensions=fs.dashboard_extension()`. Read
|
|
208
|
+
[the dashboard kit reference](references/dashboard-kit.md) for the markup, the themes, the
|
|
209
|
+
declaration and how to upgrade a dashboard built before the kit — only with the operator's
|
|
210
|
+
go-ahead, which the plugin's session-start notice reminds you of.
|
|
211
|
+
|
|
201
212
|
The events feed is a **view over the log you already keep** — a jobs table, a JSONL
|
|
202
213
|
journal — not a second store. Each event is one sentence a person reads; set
|
|
203
214
|
`notify: true` only when the operator must decide, or something failed or degrades their
|
|
@@ -224,10 +235,12 @@ python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
|
|
|
224
235
|
It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, well-known
|
|
225
236
|
shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
|
|
226
237
|
events auth and shape, single-use login, state outside code, instance lock, plist,
|
|
227
|
-
launchd pid equals answering pid, and the `interop.*` rules:
|
|
228
|
-
list, tools equal to the manifest, job tools, unknown job, trace
|
|
229
|
-
trace pairs) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass —
|
|
230
|
-
name it in the report.
|
|
238
|
+
launchd pid equals answering pid, `dashboard.declared` (ED-9), and the `interop.*` rules:
|
|
239
|
+
manifest link, capability list, tools equal to the manifest, job tools, unknown job, trace
|
|
240
|
+
propagation, event trace pairs) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass —
|
|
241
|
+
name it in the report. A service with a dashboard also runs the layout probe,
|
|
242
|
+
`node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
|
|
243
|
+
and 1280 px; needs Playwright, else `NOT_RUN`).
|
|
231
244
|
|
|
232
245
|
Then prove the lock: start a second copy by hand against the same data directory; it
|
|
233
246
|
must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-fd-appearance="scheme theme">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:">
|
|
7
|
+
<title>Example agent · dashboard kit demo</title>
|
|
8
|
+
<link rel="stylesheet" href="fabric-dashboard.css">
|
|
9
|
+
<script src="fabric-dashboard.js" defer></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<a class="fd-skip" href="#overview">Skip to content</a>
|
|
13
|
+
<div class="fd-chrome">
|
|
14
|
+
<header class="fd-header">
|
|
15
|
+
<div>
|
|
16
|
+
<p class="fd-title">Example agent</p>
|
|
17
|
+
<span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Ready</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="fd-actions">
|
|
20
|
+
<label class="fd-sr-only" for="theme">Theme</label>
|
|
21
|
+
<select id="theme" class="fd-select" data-fd-theme-picker></select>
|
|
22
|
+
<label class="fd-sr-only" for="scheme">Colour scheme</label>
|
|
23
|
+
<select id="scheme" class="fd-select" data-fd-scheme-picker></select>
|
|
24
|
+
<button class="fd-btn fd-btn--primary" type="button">Run now</button>
|
|
25
|
+
</div>
|
|
26
|
+
</header>
|
|
27
|
+
<nav class="fd-nav" aria-label="Sections">
|
|
28
|
+
<ul class="fd-nav-list">
|
|
29
|
+
<li><a href="#overview" aria-current="page">Overview</a></li>
|
|
30
|
+
<li><a href="#jobs">Jobs</a></li>
|
|
31
|
+
<li><a href="#approvals">Approvals</a></li>
|
|
32
|
+
<li><a href="#calendar">Calendar</a></li>
|
|
33
|
+
<li><a href="#channels">Channels</a></li>
|
|
34
|
+
<li><a href="#budget">Budget</a></li>
|
|
35
|
+
<li><a href="#journal">Journal</a></li>
|
|
36
|
+
<li><a href="#inbox">Inbox</a></li>
|
|
37
|
+
<li><a href="#cases">Cases</a></li>
|
|
38
|
+
<li><a href="#playbooks">Playbooks</a></li>
|
|
39
|
+
<li><a href="#reports">Reports</a></li>
|
|
40
|
+
<li><a href="#log">Log</a></li>
|
|
41
|
+
<li><a href="#settings">Settings</a></li>
|
|
42
|
+
<li><a href="#help">Help</a></li>
|
|
43
|
+
</ul>
|
|
44
|
+
</nav>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<main class="fd-page">
|
|
48
|
+
<section id="overview">
|
|
49
|
+
<h2>Overview</h2>
|
|
50
|
+
<div class="fd-grid fd-grid--tiles">
|
|
51
|
+
<div class="fd-tile"><div class="fd-label">Jobs today</div><div class="fd-value">128</div></div>
|
|
52
|
+
<div class="fd-tile"><div class="fd-label">Waiting for you</div><div class="fd-value">3</div></div>
|
|
53
|
+
<div class="fd-tile"><div class="fd-label">Spend, 7 days</div><div class="fd-value">$4.21</div></div>
|
|
54
|
+
<div class="fd-tile"><div class="fd-label">Last run</div><div class="fd-value"><time>17:02</time></div></div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section id="jobs">
|
|
59
|
+
<h2>Jobs</h2>
|
|
60
|
+
<div class="fd-scroll" data-fd-label="Jobs table">
|
|
61
|
+
<table class="fd-table">
|
|
62
|
+
<caption class="fd-sr-only">Recent jobs</caption>
|
|
63
|
+
<thead><tr><th>Job</th><th>Capability</th><th>State</th><th>Started</th><th class="fd-num">Tokens</th><th class="fd-num">Cost</th><th>Trace</th></tr></thead>
|
|
64
|
+
<tbody>
|
|
65
|
+
<tr><td class="fd-mono">job-7f3a91</td><td>example.summarize</td><td><span class="fd-chip fd-s-info"><span class="fd-dot"></span>Running</span></td><td><time>2026-10-11 17:02</time></td><td class="fd-num">12 480</td><td class="fd-num">$0.031</td><td class="fd-mono">4bf92f3577b34da6a3ce929d0e0e4736</td></tr>
|
|
66
|
+
<tr><td class="fd-mono">job-7f3a90</td><td>example.translate</td><td><span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Done</span></td><td><time>2026-10-11 16:40</time></td><td class="fd-num">8 002</td><td class="fd-num">$0.019</td><td class="fd-mono">00f067aa0ba902b74bf92f3577b34da6</td></tr>
|
|
67
|
+
<tr><td class="fd-mono">job-7f3a8f</td><td>example.review</td><td><span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Needs you</span></td><td><time>2026-10-11 15:12</time></td><td class="fd-num">2 210</td><td class="fd-num">—</td><td class="fd-mono">a3ce929d0e0e47364bf92f3577b34da6</td></tr>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section id="approvals">
|
|
74
|
+
<h2>Approvals</h2>
|
|
75
|
+
<div class="fd-grid">
|
|
76
|
+
<article class="fd-card fd-stack">
|
|
77
|
+
<h3>Publish the weekly report <span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Waiting</span></h3>
|
|
78
|
+
<p class="fd-hint">Before → after and the hash the service checks when it applies the change.</p>
|
|
79
|
+
<pre class="fd-scroll" data-fd-label="Change preview">- status: draft
|
|
80
|
+
+ status: published sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08</pre>
|
|
81
|
+
<div class="fd-row"><button class="fd-btn fd-btn--primary" type="button">Approve</button><button class="fd-btn" type="button">Reject</button></div>
|
|
82
|
+
</article>
|
|
83
|
+
<article class="fd-card">
|
|
84
|
+
<h3>Service</h3>
|
|
85
|
+
<dl class="fd-pairs">
|
|
86
|
+
<div><dt>Build</dt><dd class="fd-mono">3537587</dd></div>
|
|
87
|
+
<div><dt>Started</dt><dd><time>2026-10-11 09:00</time></dd></div>
|
|
88
|
+
<div><dt>Port</dt><dd class="fd-mono">8710</dd></div>
|
|
89
|
+
<div><dt>Instance</dt><dd class="fd-mono">default</dd></div>
|
|
90
|
+
</dl>
|
|
91
|
+
</article>
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
<section id="calendar"><h2>Calendar</h2><p class="fd-empty">Nothing is scheduled this week. <a href="#jobs">See the jobs</a></p></section>
|
|
96
|
+
<section id="channels"><h2>Channels</h2><div class="fd-banner fd-s-danger"><span class="fd-dot fd-s-danger"></span><span>Channel push failed: the provider rejected the key. Settings → Keys</span></div></section>
|
|
97
|
+
<section id="budget"><h2>Budget</h2><p>$4.21 of $100 this month.</p></section>
|
|
98
|
+
<section id="journal"><h2>Journal</h2><p>No decisions recorded today.</p></section>
|
|
99
|
+
<section id="inbox"><h2>Inbox</h2><p class="fd-empty">No comments waiting.</p></section>
|
|
100
|
+
<section id="cases"><h2>Cases</h2><p>Two candidates from yesterday's work log.</p></section>
|
|
101
|
+
<section id="playbooks"><h2>Playbooks</h2><p>All playbooks reviewed this month.</p></section>
|
|
102
|
+
<section id="reports"><h2>Reports</h2><p>The weekly report is due Monday.</p></section>
|
|
103
|
+
<section id="log"><h2>Log</h2><pre class="fd-scroll" data-fd-label="Service log">2026-10-11T17:02:11Z info job.started job-7f3a91 example.summarize trace=4bf92f3577b34da6a3ce929d0e0e4736 span=00f067aa0ba902b7
|
|
104
|
+
2026-10-11T16:40:57Z info job.finished job-7f3a90 example.translate tokens=8002 cost=0.019</pre></section>
|
|
105
|
+
<section id="settings"><h2>Settings</h2><label for="name">Display name</label> <input id="name" class="fd-input" value="Example agent"></section>
|
|
106
|
+
<section id="help"><h2>Help</h2><p>Every error is a sentence and the next action.</p></section>
|
|
107
|
+
</main>
|
|
108
|
+
<footer class="fd-foot">fabric-dashboard-kit demo · fabric-dashboard/0.1</footer>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* fabric-dashboard-kit@1.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
|
+
A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
|
|
3
|
+
reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
|
|
4
|
+
Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
|
|
5
|
+
its own stylesheet, consuming var(--…) only. Themes are generated from themes.json by
|
|
6
|
+
`dashboard_kit.py build`; the block between the markers is overwritten. */
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
/* The floor (ED-1, ED-2) and the sticky chrome budget (ED-5). */
|
|
10
|
+
--fd-floor: 360px;
|
|
11
|
+
--fd-chrome-max: 120px;
|
|
12
|
+
--fd-chrome-h: 0px;
|
|
13
|
+
--fd-col: 320px;
|
|
14
|
+
--fd-page-max: 1440px;
|
|
15
|
+
--fd-gutter: clamp(12px, 3vw, 24px);
|
|
16
|
+
|
|
17
|
+
--r-control: 6px;
|
|
18
|
+
--r-card: 10px;
|
|
19
|
+
--r-pill: 999px;
|
|
20
|
+
--shadow-1: 0 4px 16px -8px rgba(26, 31, 43, 0.18);
|
|
21
|
+
--motion-ease: cubic-bezier(0.2, 0, 0, 1);
|
|
22
|
+
--dur-state: 0.18s;
|
|
23
|
+
--dur-hover: 0.12s;
|
|
24
|
+
--font-ui: -apple-system, "SF Pro", "Segoe UI", system-ui, sans-serif;
|
|
25
|
+
--font-data: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
26
|
+
--t-chip: 11px;
|
|
27
|
+
--t-label: 12px;
|
|
28
|
+
--t-body: 13px;
|
|
29
|
+
--t-card: 15px;
|
|
30
|
+
--t-section: 20px;
|
|
31
|
+
--t-page: 28px;
|
|
32
|
+
--space-1: 4px;
|
|
33
|
+
--space-2: 8px;
|
|
34
|
+
--space-3: 12px;
|
|
35
|
+
--space-4: 16px;
|
|
36
|
+
--space-5: 24px;
|
|
37
|
+
--space-6: 32px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* fabric-dashboard-kit:themes:begin */
|
|
41
|
+
:root, :root[data-theme="graphite"] { color-scheme: light; --bg: #f7f8fa; --panel: #ffffff; --panel-2: #f7f8fa; --ink: #1a1f2b; --muted: #5b6472; --border: #e6e9ef; --border-strong: #d7dce4; --edge: #828b98; --accent: #2f6feb; --accent-weak: #eaf0fe; --accent-ink: #ffffff; --ok: #1a7f37; --ok-weak: #e6f4ea; --warn: #9a6700; --warn-weak: #fff3d6; --danger: #d1242f; --danger-weak: #fde8e9; --info: #2f6feb; --info-weak: #eaf0fe; }
|
|
42
|
+
:root[data-scheme="dark"], :root[data-theme="graphite"][data-scheme="dark"] { color-scheme: dark; --bg: #0f1218; --panel: #161b24; --panel-2: #1b212c; --ink: #e8ecf3; --muted: #8a93a6; --border: #232a36; --border-strong: #2c3441; --edge: #697383; --accent: #4b8bff; --accent-weak: #1b2740; --accent-ink: #0f1218; --ok: #3fb960; --ok-weak: #12281a; --warn: #d9a93f; --warn-weak: #2b2210; --danger: #e5534b; --danger-weak: #2d1517; --info: #4b8bff; --info-weak: #1b2740; }
|
|
43
|
+
:root[data-theme="fabric"] { color-scheme: light; --bg: #f7f5f8; --panel: #ffffff; --panel-2: #f2eef4; --ink: #211725; --muted: #655769; --border: #e9e3ec; --border-strong: #d9d0de; --edge: #8f8296; --accent: #6b2185; --accent-weak: #f4e9f7; --accent-ink: #ffffff; --ok: #246b3b; --ok-weak: #e4f3e8; --warn: #8a4b0b; --warn-weak: #fff0dc; --danger: #a82f44; --danger-weak: #fde8ec; --info: #345da7; --info-weak: #e8efff; }
|
|
44
|
+
:root[data-theme="fabric"][data-scheme="dark"] { color-scheme: dark; --bg: #110c15; --panel: #1b1420; --panel-2: #231a29; --ink: #fff9f0; --muted: #b6aabc; --border: #2e2334; --border-strong: #3a2e41; --edge: #7a6a80; --accent: #d4a3ea; --accent-weak: #2f1d38; --accent-ink: #110c15; --ok: #79d49a; --ok-weak: #12291c; --warn: #f6ba75; --warn-weak: #332314; --danger: #ff969f; --danger-weak: #35191f; --info: #a6bfff; --info-weak: #18243d; }
|
|
45
|
+
:root[data-theme="paper"] { color-scheme: light; --bg: #faf6ee; --panel: #fffdf8; --panel-2: #f3ede1; --ink: #2b241a; --muted: #6b5f4d; --border: #ebe3d4; --border-strong: #ddd2bf; --edge: #8d806d; --accent: #8a4a12; --accent-weak: #f6eadb; --accent-ink: #ffffff; --ok: #2f6b2f; --ok-weak: #e7f1e2; --warn: #8a5a00; --warn-weak: #fbefd2; --danger: #b02a20; --danger-weak: #fbe5e1; --info: #2c5d8a; --info-weak: #e5eef6; }
|
|
46
|
+
:root[data-theme="paper"][data-scheme="dark"] { color-scheme: dark; --bg: #17140f; --panel: #1f1b15; --panel-2: #27221a; --ink: #f3ece0; --muted: #b3a690; --border: #302a21; --border-strong: #3d362b; --edge: #7b7161; --accent: #e3a565; --accent-weak: #33261a; --accent-ink: #17140f; --ok: #86c48a; --ok-weak: #15261a; --warn: #e0b25a; --warn-weak: #2e2412; --danger: #f0877c; --danger-weak: #331a17; --info: #8fb8e0; --info-weak: #172433; }
|
|
47
|
+
:root[data-theme="forest"] { color-scheme: light; --bg: #f4f8f4; --panel: #ffffff; --panel-2: #edf3ee; --ink: #16231a; --muted: #526356; --border: #dfe8e1; --border-strong: #cddbd0; --edge: #77897c; --accent: #1f7a45; --accent-weak: #e3f2e8; --accent-ink: #ffffff; --ok: #1f7a45; --ok-weak: #e3f2e8; --warn: #8a5d00; --warn-weak: #fbf0d4; --danger: #b8312a; --danger-weak: #fbe6e4; --info: #2a6496; --info-weak: #e4eef7; }
|
|
48
|
+
:root[data-theme="forest"][data-scheme="dark"] { color-scheme: dark; --bg: #0d1410; --panel: #131c16; --panel-2: #18231c; --ink: #e4efe7; --muted: #8fa595; --border: #1f2c23; --border-strong: #29382e; --edge: #637568; --accent: #5cc98a; --accent-weak: #173322; --accent-ink: #0d1410; --ok: #5cc98a; --ok-weak: #173322; --warn: #d8ad4a; --warn-weak: #2b2311; --danger: #ec6a5e; --danger-weak: #2f1714; --info: #7fb2e5; --info-weak: #152435; }
|
|
49
|
+
:root[data-theme="ocean"] { color-scheme: light; --bg: #f3f7f9; --panel: #ffffff; --panel-2: #ebf2f5; --ink: #132029; --muted: #4f6370; --border: #dde7ec; --border-strong: #ccdae1; --edge: #738892; --accent: #0b6e8a; --accent-weak: #e0f1f6; --accent-ink: #ffffff; --ok: #1b7a4a; --ok-weak: #e2f3ea; --warn: #8f5f00; --warn-weak: #fcf0d3; --danger: #c0352b; --danger-weak: #fce6e3; --info: #1f5fae; --info-weak: #e3edfa; }
|
|
50
|
+
:root[data-theme="ocean"][data-scheme="dark"] { color-scheme: dark; --bg: #0b1217; --panel: #111a21; --panel-2: #16212a; --ink: #e2edf3; --muted: #8aa2b1; --border: #1c2a34; --border-strong: #263741; --edge: #607480; --accent: #3fc0d8; --accent-weak: #123039; --accent-ink: #0b1217; --ok: #4fc58a; --ok-weak: #11291d; --warn: #dcac45; --warn-weak: #2c2310; --danger: #ef6b60; --danger-weak: #311714; --info: #6fa8f0; --info-weak: #13233a; }
|
|
51
|
+
:root[data-theme="ember"] { color-scheme: light; --bg: #fbf6f2; --panel: #ffffff; --panel-2: #f6ede6; --ink: #2a1b14; --muted: #6e5649; --border: #efe2d8; --border-strong: #e2d1c4; --edge: #958072; --accent: #b04a0c; --accent-weak: #fbebe0; --accent-ink: #ffffff; --ok: #2a7340; --ok-weak: #e4f2e7; --warn: #8a5800; --warn-weak: #fbefd3; --danger: #b42324; --danger-weak: #fbe4e3; --info: #2b5f9e; --info-weak: #e4edf8; }
|
|
52
|
+
:root[data-theme="ember"][data-scheme="dark"] { color-scheme: dark; --bg: #16100c; --panel: #1e1611; --panel-2: #271d16; --ink: #f6ebe3; --muted: #b8a194; --border: #30241c; --border-strong: #3e2f25; --edge: #7e6a5e; --accent: #ff8f4d; --accent-weak: #3a2112; --accent-ink: #16100c; --ok: #78c88e; --ok-weak: #132719; --warn: #e3b04f; --warn-weak: #2e2410; --danger: #f4776f; --danger-weak: #351816; --info: #86b2ef; --info-weak: #162338; }
|
|
53
|
+
:root[data-theme="contrast"] { color-scheme: light; --bg: #ffffff; --panel: #ffffff; --panel-2: #f2f2f2; --ink: #000000; --muted: #2b2b2b; --border: #000000; --border-strong: #000000; --edge: #000000; --accent: #0033cc; --accent-weak: #e6ecff; --accent-ink: #ffffff; --ok: #005c1f; --ok-weak: #e3f5e8; --warn: #6b4200; --warn-weak: #fff1d1; --danger: #b00010; --danger-weak: #ffe6e8; --info: #0033cc; --info-weak: #e6ecff; }
|
|
54
|
+
:root[data-theme="contrast"][data-scheme="dark"] { color-scheme: dark; --bg: #000000; --panel: #000000; --panel-2: #141414; --ink: #ffffff; --muted: #e0e0e0; --border: #ffffff; --border-strong: #ffffff; --edge: #ffffff; --accent: #7fb2ff; --accent-weak: #0d1f3d; --accent-ink: #000000; --ok: #5ee08a; --ok-weak: #0a2614; --warn: #ffd166; --warn-weak: #2e2306; --danger: #ff8a8a; --danger-weak: #3a0d0d; --info: #7fb2ff; --info-weak: #0d1f3d; }
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root:not([data-scheme="light"]) { color-scheme: dark; --bg: #0f1218; --panel: #161b24; --panel-2: #1b212c; --ink: #e8ecf3; --muted: #8a93a6; --border: #232a36; --border-strong: #2c3441; --edge: #697383; --accent: #4b8bff; --accent-weak: #1b2740; --accent-ink: #0f1218; --ok: #3fb960; --ok-weak: #12281a; --warn: #d9a93f; --warn-weak: #2b2210; --danger: #e5534b; --danger-weak: #2d1517; --info: #4b8bff; --info-weak: #1b2740; }
|
|
57
|
+
:root[data-theme="fabric"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #110c15; --panel: #1b1420; --panel-2: #231a29; --ink: #fff9f0; --muted: #b6aabc; --border: #2e2334; --border-strong: #3a2e41; --edge: #7a6a80; --accent: #d4a3ea; --accent-weak: #2f1d38; --accent-ink: #110c15; --ok: #79d49a; --ok-weak: #12291c; --warn: #f6ba75; --warn-weak: #332314; --danger: #ff969f; --danger-weak: #35191f; --info: #a6bfff; --info-weak: #18243d; }
|
|
58
|
+
:root[data-theme="paper"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #17140f; --panel: #1f1b15; --panel-2: #27221a; --ink: #f3ece0; --muted: #b3a690; --border: #302a21; --border-strong: #3d362b; --edge: #7b7161; --accent: #e3a565; --accent-weak: #33261a; --accent-ink: #17140f; --ok: #86c48a; --ok-weak: #15261a; --warn: #e0b25a; --warn-weak: #2e2412; --danger: #f0877c; --danger-weak: #331a17; --info: #8fb8e0; --info-weak: #172433; }
|
|
59
|
+
:root[data-theme="forest"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #0d1410; --panel: #131c16; --panel-2: #18231c; --ink: #e4efe7; --muted: #8fa595; --border: #1f2c23; --border-strong: #29382e; --edge: #637568; --accent: #5cc98a; --accent-weak: #173322; --accent-ink: #0d1410; --ok: #5cc98a; --ok-weak: #173322; --warn: #d8ad4a; --warn-weak: #2b2311; --danger: #ec6a5e; --danger-weak: #2f1714; --info: #7fb2e5; --info-weak: #152435; }
|
|
60
|
+
:root[data-theme="ocean"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #0b1217; --panel: #111a21; --panel-2: #16212a; --ink: #e2edf3; --muted: #8aa2b1; --border: #1c2a34; --border-strong: #263741; --edge: #607480; --accent: #3fc0d8; --accent-weak: #123039; --accent-ink: #0b1217; --ok: #4fc58a; --ok-weak: #11291d; --warn: #dcac45; --warn-weak: #2c2310; --danger: #ef6b60; --danger-weak: #311714; --info: #6fa8f0; --info-weak: #13233a; }
|
|
61
|
+
:root[data-theme="ember"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #16100c; --panel: #1e1611; --panel-2: #271d16; --ink: #f6ebe3; --muted: #b8a194; --border: #30241c; --border-strong: #3e2f25; --edge: #7e6a5e; --accent: #ff8f4d; --accent-weak: #3a2112; --accent-ink: #16100c; --ok: #78c88e; --ok-weak: #132719; --warn: #e3b04f; --warn-weak: #2e2410; --danger: #f4776f; --danger-weak: #351816; --info: #86b2ef; --info-weak: #162338; }
|
|
62
|
+
:root[data-theme="contrast"]:not([data-scheme="light"]) { color-scheme: dark; --bg: #000000; --panel: #000000; --panel-2: #141414; --ink: #ffffff; --muted: #e0e0e0; --border: #ffffff; --border-strong: #ffffff; --edge: #ffffff; --accent: #7fb2ff; --accent-weak: #0d1f3d; --accent-ink: #000000; --ok: #5ee08a; --ok-weak: #0a2614; --warn: #ffd166; --warn-weak: #2e2306; --danger: #ff8a8a; --danger-weak: #3a0d0d; --info: #7fb2ff; --info-weak: #0d1f3d; }
|
|
63
|
+
}
|
|
64
|
+
/* fabric-dashboard-kit:themes:end */
|
|
65
|
+
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
:root { --dur-state: 0s; --dur-hover: 0s; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ---- base: the floor holds (ED-1, ED-2) ---- */
|
|
71
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
72
|
+
html { min-width: var(--fd-floor); background: var(--bg); color: var(--ink); -webkit-text-size-adjust: 100%; }
|
|
73
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 400 var(--t-body)/1.5 var(--font-ui); overflow-wrap: break-word; }
|
|
74
|
+
img, svg, video, canvas { max-width: 100%; height: auto; }
|
|
75
|
+
h1, h2, h3 { margin: 0; text-wrap: balance; }
|
|
76
|
+
h1 { font-size: var(--t-section); font-weight: 700; }
|
|
77
|
+
h2 { font-size: var(--t-section); font-weight: 700; margin: var(--space-6) 0 var(--space-3); }
|
|
78
|
+
h3 { font-size: var(--t-card); font-weight: 600; margin: 0 0 var(--space-3); }
|
|
79
|
+
p { max-width: 65ch; }
|
|
80
|
+
a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--edge); text-underline-offset: 2px; }
|
|
81
|
+
a:hover { text-decoration-color: var(--ink); }
|
|
82
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
83
|
+
/* A jump to an anchor lands below the sticky chrome (ED-5); the script measures --fd-chrome-h. */
|
|
84
|
+
[id] { scroll-margin-top: calc(var(--fd-chrome-h) + var(--space-2)); }
|
|
85
|
+
.fd-mono, .fd-num, time { font-family: var(--font-data); font-variant-numeric: tabular-nums; }
|
|
86
|
+
.fd-muted { color: var(--muted); }
|
|
87
|
+
.fd-hint { color: var(--muted); font-size: var(--t-label); max-width: 65ch; margin: 0 0 var(--space-3); }
|
|
88
|
+
.fd-break { overflow-wrap: anywhere; }
|
|
89
|
+
.fd-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
90
|
+
.fd-skip { position: absolute; top: var(--space-2); left: var(--space-4); z-index: 20; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
91
|
+
.fd-skip:focus { width: auto; height: auto; clip: auto; overflow: visible; background: var(--panel); padding: var(--space-2); }
|
|
92
|
+
|
|
93
|
+
/* ---- page frame ---- */
|
|
94
|
+
.fd-page { max-width: var(--fd-page-max); margin: 0 auto; padding: var(--space-4) var(--fd-gutter) var(--space-6); }
|
|
95
|
+
/* Sticky only once the script runs: without it the navigation wraps and a wrapped bar must not pin
|
|
96
|
+
half the screen. The script keeps the pinned part at or under min(--fd-chrome-max, 25vh) (ED-5):
|
|
97
|
+
when header and navigation together are taller, the header scrolls away (a negative top) and the
|
|
98
|
+
navigation stays pinned. */
|
|
99
|
+
.fd-chrome { background: var(--panel); border-bottom: 1px solid var(--border); }
|
|
100
|
+
html.fd-js .fd-chrome { position: sticky; top: var(--fd-chrome-top, 0px); z-index: 10; }
|
|
101
|
+
.fd-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4);
|
|
102
|
+
padding: var(--space-3) var(--fd-gutter); min-width: 0; }
|
|
103
|
+
.fd-header > * { min-width: 0; }
|
|
104
|
+
.fd-title { font-size: var(--t-card); font-weight: 700; margin: 0; }
|
|
105
|
+
.fd-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
|
|
106
|
+
.fd-foot { color: var(--muted); font-size: var(--t-label); padding: var(--space-4) var(--fd-gutter); border-top: 1px solid var(--border); }
|
|
107
|
+
|
|
108
|
+
/* ---- navigation: always reachable (ED-3) ----
|
|
109
|
+
Without the script the list wraps. With it (.fd-nav--fold) the list stays one line and the
|
|
110
|
+
links that do not fit move into the More menu; nothing is left past a hidden scrollbar. */
|
|
111
|
+
.fd-nav { display: flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--fd-gutter); min-width: 0; }
|
|
112
|
+
.fd-nav-list { display: flex; flex-wrap: wrap; gap: var(--space-1); list-style: none; margin: 0; padding: 0; min-width: 0; }
|
|
113
|
+
.fd-nav--fold .fd-nav-list { flex: 1 1 auto; flex-wrap: nowrap; overflow: hidden; }
|
|
114
|
+
.fd-nav a, .fd-more > summary { display: block; padding: var(--space-1) var(--space-3); border-radius: var(--r-control); color: var(--muted);
|
|
115
|
+
text-decoration: none; white-space: nowrap; cursor: pointer;
|
|
116
|
+
transition: background-color var(--dur-hover) var(--motion-ease), color var(--dur-hover) var(--motion-ease); }
|
|
117
|
+
.fd-nav a:hover, .fd-more > summary:hover { background: var(--panel-2); color: var(--ink); }
|
|
118
|
+
.fd-nav a[aria-current] { background: var(--accent-weak); color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
|
|
119
|
+
.fd-more { position: relative; flex: none; }
|
|
120
|
+
.fd-more[hidden] { display: none; }
|
|
121
|
+
.fd-more > summary { list-style: none; border: 1px solid var(--edge); }
|
|
122
|
+
.fd-more > summary::-webkit-details-marker { display: none; }
|
|
123
|
+
.fd-more > summary::after { content: " \25BE"; }
|
|
124
|
+
.fd-more-list { position: absolute; right: 0; top: calc(100% + var(--space-1)); z-index: 30; list-style: none; margin: 0; padding: var(--space-1);
|
|
125
|
+
min-width: 12rem; max-width: min(20rem, calc(100vw - 2 * var(--fd-gutter))); max-height: 60vh; overflow-y: auto;
|
|
126
|
+
background: var(--panel); border: 1px solid var(--edge); border-radius: var(--r-card); box-shadow: var(--shadow-1); }
|
|
127
|
+
.fd-more-list a { white-space: normal; }
|
|
128
|
+
|
|
129
|
+
/* ---- layout: fluid, no fixed widths ---- */
|
|
130
|
+
.fd-grid { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--fd-col)), 1fr)); }
|
|
131
|
+
.fd-grid--tiles { --fd-col: 160px; }
|
|
132
|
+
.fd-grid--wide { --fd-col: 480px; }
|
|
133
|
+
.fd-stack > * + * { margin-top: var(--space-3); }
|
|
134
|
+
.fd-row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; min-width: 0; }
|
|
135
|
+
.fd-row > * { min-width: 0; }
|
|
136
|
+
/* A label-value list: two columns while they fit, one below that. */
|
|
137
|
+
.fd-pairs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--space-1) var(--space-4); margin: 0; }
|
|
138
|
+
.fd-pairs > div { display: flex; justify-content: space-between; gap: var(--space-3); border-bottom: 1px solid var(--border); padding: var(--space-1) 0; min-width: 0; }
|
|
139
|
+
.fd-pairs dt { color: var(--muted); }
|
|
140
|
+
.fd-pairs dd { margin: 0; text-align: right; overflow-wrap: anywhere; }
|
|
141
|
+
|
|
142
|
+
/* ---- wide content scrolls in its own box (ED-4) ----
|
|
143
|
+
Edge shades show which side has more (the local/scroll background pair), the WebKit scrollbar
|
|
144
|
+
stays visible instead of overlaying, and the script makes the box focusable with a name. */
|
|
145
|
+
.fd-scroll { overflow-x: auto; max-width: 100%; border: 1px solid var(--border); border-radius: var(--r-card);
|
|
146
|
+
background: var(--panel); /* a browser without color-mix() keeps a plain box */
|
|
147
|
+
background:
|
|
148
|
+
linear-gradient(90deg, var(--panel) 30%, transparent) left center / 40px 100% no-repeat local,
|
|
149
|
+
linear-gradient(270deg, var(--panel) 30%, transparent) right center / 40px 100% no-repeat local,
|
|
150
|
+
linear-gradient(90deg, color-mix(in srgb, var(--ink) 18%, transparent), transparent) left center / 14px 100% no-repeat scroll,
|
|
151
|
+
linear-gradient(270deg, color-mix(in srgb, var(--ink) 18%, transparent), transparent) right center / 14px 100% no-repeat scroll,
|
|
152
|
+
var(--panel);
|
|
153
|
+
scrollbar-width: thin; scrollbar-color: var(--edge) transparent; }
|
|
154
|
+
.fd-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
|
|
155
|
+
.fd-scroll::-webkit-scrollbar-thumb { background: var(--edge); border-radius: var(--r-pill); border: 2px solid var(--panel); }
|
|
156
|
+
.fd-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
157
|
+
.fd-scroll:focus-visible { outline-offset: -2px; }
|
|
158
|
+
.fd-scroll[data-fd-overflow] { border-color: var(--edge); }
|
|
159
|
+
pre.fd-scroll, .fd-scroll pre { margin: 0; padding: var(--space-3); font: var(--t-body)/1.5 var(--font-data); white-space: pre; }
|
|
160
|
+
.fd-table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
|
|
161
|
+
/* Inside its own box a table keeps its natural width and the box scrolls; words are not broken. */
|
|
162
|
+
.fd-scroll > .fd-table { width: max-content; min-width: 100%; }
|
|
163
|
+
.fd-scroll .fd-table td, .fd-scroll .fd-table th { overflow-wrap: normal; }
|
|
164
|
+
.fd-table th, .fd-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
165
|
+
.fd-table th { background: var(--panel-2); color: var(--muted); font-size: var(--t-label); font-weight: 600; white-space: nowrap; }
|
|
166
|
+
.fd-table tbody tr:hover { background: var(--panel-2); }
|
|
167
|
+
.fd-table td.fd-num, .fd-table th.fd-num { text-align: right; }
|
|
168
|
+
|
|
169
|
+
/* ---- atoms ---- */
|
|
170
|
+
.fd-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-card); padding: var(--space-4); min-width: 0; }
|
|
171
|
+
.fd-card > h3:first-child { display: flex; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
|
|
172
|
+
.fd-tile { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-card); padding: var(--space-3); min-width: 0; }
|
|
173
|
+
.fd-tile .fd-label { color: var(--muted); font-size: var(--t-label); }
|
|
174
|
+
.fd-tile .fd-value { font: 600 var(--t-section)/1.2 var(--font-data); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
175
|
+
/* Status: the word in --ink, the colour in the dot and the fill; never colour alone. */
|
|
176
|
+
.fd-chip { display: inline-flex; align-items: center; gap: var(--space-1); max-width: 100%; font: var(--t-chip)/1.6 var(--font-data);
|
|
177
|
+
padding: 0 var(--space-2); border: 1px solid var(--border-strong); border-radius: var(--r-pill); color: var(--ink); background: var(--panel);
|
|
178
|
+
white-space: nowrap; }
|
|
179
|
+
.fd-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
|
|
180
|
+
.fd-s-ok { background: var(--ok-weak); } .fd-s-ok .fd-dot, .fd-dot.fd-s-ok { background: var(--ok); }
|
|
181
|
+
.fd-s-warn { background: var(--warn-weak); } .fd-s-warn .fd-dot, .fd-dot.fd-s-warn { background: var(--warn); }
|
|
182
|
+
.fd-s-danger { background: var(--danger-weak); } .fd-s-danger .fd-dot, .fd-dot.fd-s-danger { background: var(--danger); }
|
|
183
|
+
.fd-s-info { background: var(--info-weak); } .fd-s-info .fd-dot, .fd-dot.fd-s-info { background: var(--info); }
|
|
184
|
+
.fd-banner { display: flex; gap: var(--space-2); align-items: flex-start; padding: var(--space-3); border: 1px solid var(--edge); border-radius: var(--r-card); }
|
|
185
|
+
.fd-btn { font: 600 var(--t-body) var(--font-ui); padding: var(--space-2) var(--space-3); border-radius: var(--r-control); cursor: pointer;
|
|
186
|
+
border: 1px solid var(--edge); background: var(--panel); color: var(--ink); max-width: 100%;
|
|
187
|
+
transition: background-color var(--dur-hover) var(--motion-ease), border-color var(--dur-hover) var(--motion-ease); }
|
|
188
|
+
.fd-btn:hover { background: var(--panel-2); }
|
|
189
|
+
.fd-btn--primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
|
|
190
|
+
.fd-btn--primary:hover { background: var(--accent); filter: brightness(1.06); }
|
|
191
|
+
.fd-btn--danger { border-color: var(--danger); }
|
|
192
|
+
.fd-btn:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
193
|
+
.fd-input, .fd-select, .fd-textarea { font: var(--t-body) var(--font-ui); color: var(--ink); background: var(--panel);
|
|
194
|
+
border: 1px solid var(--edge); border-radius: var(--r-control); padding: var(--space-2); max-width: 100%; min-width: 0; }
|
|
195
|
+
.fd-empty { color: var(--muted); padding: var(--space-4); text-align: center; }
|
|
196
|
+
|
|
197
|
+
/* ---- appearance owned by the host (ED-6) ---- */
|
|
198
|
+
html[data-fd-host-appearance] [data-fd-appearance-control] { display: none !important; }
|