mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
Follows bitlogin's layering: the engine stays headless and the widget is a consumer of the published API, never a dependency of it. Building it was also a test of that API — an element needing something unexposed would have been a finding, the way the commerce example was. Viewer elements: governance-veil (blur on restrict, disclosure on hide, warning on warn), governance-report, governance-status. Moderator elements: governance-capabilities, governance-action, governance-admin-panel. Deliberate behaviors: - hidden content leaves the accessibility tree rather than being visually covered, so a screen reader does not read out what the viewer must not see - a hidden decision renders a disclosure, not nothing: content that silently vanishes is indistinguishable from a bug and leaves no route to appeal - refused moderator actions stay visible and name the missing capability. A hidden button teaches nothing about the authority model, and the command layer enforces the same check independently — the UI is an explanation, never the gate - no element computes policy; wording maps from stable reason identifiers and is overridable per application Found while building it: GovernanceAdminStore emitted only when reduced denial state changed, so revoking a role that denied nobody announced nothing, leaving capability-gated UI and the decision cache stale. The root tsconfig deliberately omits the DOM lib — that absence is what keeps core, nostr, and runtime from reaching for browser globals — so the widget carries its own DOM-aware config rather than relaxing the guarantee globally. 687 tests; lint, typecheck, and the declaration build pass from a cold checkout.
117 lines
4.0 KiB
Markdown
117 lines
4.0 KiB
Markdown
# @nostr-governance/widget
|
|
|
|
Drop-in custom elements for governance UI. Framework-agnostic, no build step
|
|
required for consumers, and built strictly on the published API.
|
|
|
|
## The rule these elements follow
|
|
|
|
**They render decisions and issue commands. They compute no policy.**
|
|
|
|
If an element here ever needed to compare a count to a threshold, that would
|
|
mean the policy definition is missing a profile — not that the widget needs a
|
|
number. Keeping that line is the whole reason the engine was extracted.
|
|
|
|
## Elements
|
|
|
|
### Viewer-facing
|
|
|
|
| Element | Purpose |
|
|
| --- | --- |
|
|
| `<governance-veil>` | Wraps content and applies the decision: blur on `restrict`, disclosure on `hide`, warning on `warn` |
|
|
| `<governance-report>` | Report dialog. Requires no capability — anyone may report |
|
|
| `<governance-status>` | Readout of effect, reasons, and evidence |
|
|
|
|
### Moderator-facing
|
|
|
|
| Element | Purpose |
|
|
| --- | --- |
|
|
| `<governance-capabilities>` | What the signed-in account may actually do |
|
|
| `<governance-action>` | A capability-gated button that explains refusals |
|
|
| `<governance-admin-panel>` | Effective state, contributor attribution, and controls |
|
|
|
|
## Usage
|
|
|
|
```html
|
|
<script type="module">
|
|
import { createGovernanceRuntime, createCommands } from "@nostr-governance/runtime";
|
|
import { defineGovernanceElements } from "@nostr-governance/widget";
|
|
|
|
defineGovernanceElements();
|
|
|
|
const runtime = createGovernanceRuntime({
|
|
applicationId: "my-app",
|
|
namespace: "myapp",
|
|
root: ROOT_PUBKEY,
|
|
transport: myRelayAdapter,
|
|
policy: myPolicy,
|
|
now: () => Math.floor(Date.now() / 1000),
|
|
});
|
|
|
|
await runtime.loadAdministrativeState();
|
|
|
|
const veil = document.querySelector("governance-veil");
|
|
veil.runtime = runtime;
|
|
veil.target = { type: "event", id: eventId, author: authorPubkey };
|
|
</script>
|
|
|
|
<governance-veil profile="feed">
|
|
<img src="thumbnail.jpg" alt="" />
|
|
</governance-veil>
|
|
```
|
|
|
|
Elements receive a runtime; they never construct one. A widget that owned relay
|
|
connections would make it impossible to share one governance runtime across a
|
|
page.
|
|
|
|
## Events
|
|
|
|
All events are `composed` so they cross shadow boundaries.
|
|
|
|
| Event | Fired when |
|
|
| --- | --- |
|
|
| `governance:revealed` | The viewer chose to show hidden content |
|
|
| `governance:reported` / `governance:report-failed` | A report was published or refused |
|
|
| `governance:action-completed` / `:action-refused` / `:action-failed` | An action resolved |
|
|
| `governance:account-restricted` | The admin panel restricted an account |
|
|
|
|
## Wording
|
|
|
|
The engine emits stable reason identifiers, never prose, so applications phrase
|
|
them for their own audience:
|
|
|
|
```js
|
|
veil.reasonText = {
|
|
"admin-user-deny": "This seller is suspended",
|
|
"trusted-report-threshold": "Flagged by several buyers you follow",
|
|
};
|
|
```
|
|
|
|
## Design decisions worth knowing
|
|
|
|
**Hidden content is removed from the accessibility tree**, not merely covered.
|
|
A screen reader must not read out something the viewer was not meant to see.
|
|
|
|
**A hidden decision renders a disclosure, not nothing.** Content that silently
|
|
vanishes is indistinguishable from a bug and leaves no route to appeal. Where
|
|
the profile permits an override, a "Show anyway" control appears; where it does
|
|
not, the reason still does.
|
|
|
|
**Refused actions stay visible and explain themselves.** A moderator who cannot
|
|
see that they lack a capability will assume the tool is broken, and a hidden
|
|
button teaches nothing about the authority model. The command layer enforces
|
|
the same check independently — the UI is an explanation, never the gate.
|
|
|
|
**Styling is yours.** Elements expose `part` attributes and inherit `color` and
|
|
`font` from the host. Override the CSS custom properties (`--gov-fg`,
|
|
`--gov-surface`, `--gov-warn`, `--gov-danger`, `--gov-radius`) or target parts:
|
|
|
|
```css
|
|
governance-veil::part(veil) { border: 1px solid red; }
|
|
```
|
|
|
|
## Environment
|
|
|
|
`defineGovernanceElements()` is safe to call repeatedly and safe to import in
|
|
Node — registration is a no-op without a DOM, so a server-rendering host can
|
|
import the helpers without guarding.
|