@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
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/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
import { ArrowSquareOutIcon, QuestionIcon, resolveIcon } from "./icons.tsx";
|
|
4
|
+
import { Checkbox } from "./checkbox.tsx";
|
|
5
|
+
import { Label } from "./label.tsx";
|
|
6
|
+
import { Switch } from "./switch.tsx";
|
|
7
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from "./tooltip.tsx";
|
|
8
|
+
import { permissionMeta, type PermissionKind } from "../../permissions.ts";
|
|
9
|
+
|
|
10
|
+
const ICON_SIZE = "w-[22px] h-[22px]";
|
|
11
|
+
|
|
12
|
+
// Pass the raw string verbatim. Coercion happens server-side via the
|
|
13
|
+
// grantSchema (z.coerce.number()...), so an empty input stays empty and
|
|
14
|
+
// gets rejected at sanitizeGrants time, not silently coerced to 0 here.
|
|
15
|
+
// type="number" + min/max stay for browser-level steppers and validation
|
|
16
|
+
// hints; the value still flows as a string.
|
|
17
|
+
function PortInput({ value, onChange }: { value: string | number; onChange: (v: string) => void }) {
|
|
18
|
+
return (
|
|
19
|
+
<span className="bg-surface-light shadow-sm rounded-lg px-1.5 py-0.5 inline-flex ml-1.5">
|
|
20
|
+
<input
|
|
21
|
+
type="number"
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={(e) => onChange(e.currentTarget.value)}
|
|
24
|
+
min={1}
|
|
25
|
+
max={65535}
|
|
26
|
+
className="bg-transparent border-none outline-none text-sm font-mono font-bold text-text-body w-[50px] text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
|
|
27
|
+
/>
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTitle(kind: PermissionKind, name: string, label?: string): string {
|
|
33
|
+
if (!label) return name;
|
|
34
|
+
// host.port renders the port number in a separate PortInput pill, so the
|
|
35
|
+
// title shouldn't repeat it.
|
|
36
|
+
if (kind === "host.port") return name;
|
|
37
|
+
return `${name} ${label}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PermissionRow({
|
|
41
|
+
kind,
|
|
42
|
+
reason,
|
|
43
|
+
required,
|
|
44
|
+
checked,
|
|
45
|
+
onCheckedChange,
|
|
46
|
+
label,
|
|
47
|
+
port,
|
|
48
|
+
onPortChange,
|
|
49
|
+
control,
|
|
50
|
+
url,
|
|
51
|
+
choices,
|
|
52
|
+
selectedChoices,
|
|
53
|
+
onSelectedChoicesChange,
|
|
54
|
+
scopes,
|
|
55
|
+
networks,
|
|
56
|
+
}: {
|
|
57
|
+
kind: PermissionKind;
|
|
58
|
+
reason: string;
|
|
59
|
+
/** Omit to hide the required/optional badge (e.g. when the parent
|
|
60
|
+
* optional section is toggled off and requirements don't apply). */
|
|
61
|
+
required?: boolean;
|
|
62
|
+
checked: boolean;
|
|
63
|
+
onCheckedChange: (v: boolean) => void;
|
|
64
|
+
label?: string;
|
|
65
|
+
port?: string | number;
|
|
66
|
+
onPortChange?: (v: string) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Per-kind control rendered under the reason. One slot rather than a
|
|
69
|
+
* prop per kind, so a kind with a richer choice than a text field
|
|
70
|
+
* (host.silo's picker) doesn't teach this component - or the docs site
|
|
71
|
+
* that also renders it - what a Silo is.
|
|
72
|
+
*/
|
|
73
|
+
control?: ComponentChildren;
|
|
74
|
+
url?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Sub-capabilities the admin can toggle independently. Each entry
|
|
77
|
+
* carries a name, an optional per-choice reason shown next to the
|
|
78
|
+
* checkbox, and an optional `required` flag that locks the checkbox
|
|
79
|
+
* on (and hides it from the Select-all deselect behavior).
|
|
80
|
+
*/
|
|
81
|
+
choices?: Array<{ name: string; reason?: string; required?: boolean }>;
|
|
82
|
+
/** Controlled list of which choice names are currently selected. */
|
|
83
|
+
selectedChoices?: string[];
|
|
84
|
+
onSelectedChoicesChange?: (next: string[]) => void;
|
|
85
|
+
scopes?: string[];
|
|
86
|
+
networks?: string[];
|
|
87
|
+
}) {
|
|
88
|
+
const meta = permissionMeta(kind);
|
|
89
|
+
const danger = meta.danger === true;
|
|
90
|
+
const title = buildTitle(kind, meta.name, label);
|
|
91
|
+
const Icon = resolveIcon(meta.icon);
|
|
92
|
+
|
|
93
|
+
const [portValue, setPortValue] = useState<string | number>(port ?? "");
|
|
94
|
+
|
|
95
|
+
const handlePortChange = onPortChange ?? setPortValue;
|
|
96
|
+
const currentPort = onPortChange ? (port ?? "") : portValue;
|
|
97
|
+
|
|
98
|
+
// Uncontrolled fallback: callers that pass `choices` without a matching
|
|
99
|
+
// `selectedChoices` + `onSelectedChoicesChange` pair (e.g. the docs-site
|
|
100
|
+
// PermissionPreview) still get a working toggle via internal state.
|
|
101
|
+
const [internalSelected, setInternalSelected] = useState<string[]>([]);
|
|
102
|
+
const isControlled = selectedChoices !== undefined && onSelectedChoicesChange !== undefined;
|
|
103
|
+
const effectiveSelected = isControlled ? (selectedChoices ?? []) : internalSelected;
|
|
104
|
+
const selectedSet = new Set(effectiveSelected);
|
|
105
|
+
const allNames = choices?.map((c) => c.name) ?? [];
|
|
106
|
+
const requiredSet = new Set(choices?.filter((c) => c.required === true).map((c) => c.name) ?? []);
|
|
107
|
+
const allChoicesSelected =
|
|
108
|
+
choices != null && choices.length > 0 && allNames.every((n) => selectedSet.has(n));
|
|
109
|
+
|
|
110
|
+
const emit = (next: string[]) => {
|
|
111
|
+
if (isControlled) onSelectedChoicesChange!(next);
|
|
112
|
+
else setInternalSelected(next);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSelectAll = (v: boolean) => {
|
|
116
|
+
if (!choices) return;
|
|
117
|
+
// Deselect-all keeps required choices checked - they're locked and
|
|
118
|
+
// can't be turned off at the row level.
|
|
119
|
+
emit(v ? [...allNames] : allNames.filter((n) => requiredSet.has(n)));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleSelect = (choice: string, v: boolean) => {
|
|
123
|
+
if (!choices) return;
|
|
124
|
+
if (requiredSet.has(choice)) return; // locked, ignore attempts to toggle
|
|
125
|
+
const next = new Set(selectedSet);
|
|
126
|
+
if (v) next.add(choice);
|
|
127
|
+
else next.delete(choice);
|
|
128
|
+
emit(allNames.filter((n) => next.has(n)));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="bg-card rounded-2xl shadow p-3.5 px-4 flex items-start gap-3">
|
|
133
|
+
<span className={danger ? "text-danger" : "text-perm"}>
|
|
134
|
+
<Icon className={ICON_SIZE} />
|
|
135
|
+
</span>
|
|
136
|
+
<div className="flex-1 min-w-0">
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
<h3 className={`text-sm font-bold ${danger ? "text-danger-deep" : "text-foreground"}`}>
|
|
139
|
+
{title}
|
|
140
|
+
{port != null && <PortInput value={currentPort} onChange={handlePortChange} />}
|
|
141
|
+
</h3>
|
|
142
|
+
{required !== undefined && (
|
|
143
|
+
<span
|
|
144
|
+
className={`text-[10px] font-bold px-[7px] py-0.5 rounded uppercase tracking-wide ${
|
|
145
|
+
danger ? "text-danger bg-danger-bg" : "text-muted-foreground bg-surface-light"
|
|
146
|
+
}`}
|
|
147
|
+
>
|
|
148
|
+
{required ? "required" : "optional"}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
<Tooltip>
|
|
152
|
+
<TooltipTrigger className="opacity-50 hover:opacity-100 transition-opacity">
|
|
153
|
+
<QuestionIcon
|
|
154
|
+
className={`w-[15px] h-[15px] ${danger ? "text-danger" : "text-perm"}`}
|
|
155
|
+
/>
|
|
156
|
+
</TooltipTrigger>
|
|
157
|
+
<TooltipContent side="top" className="max-w-xs">
|
|
158
|
+
{meta.helpText}
|
|
159
|
+
</TooltipContent>
|
|
160
|
+
</Tooltip>
|
|
161
|
+
</div>
|
|
162
|
+
<p className="text-xs mt-1 leading-snug text-foreground">{reason}</p>
|
|
163
|
+
{control}
|
|
164
|
+
{scopes && scopes.length > 0 && (
|
|
165
|
+
<div className="flex gap-1.5 mt-1.5 flex-wrap">
|
|
166
|
+
{scopes.map((scope) => (
|
|
167
|
+
<span
|
|
168
|
+
key={scope}
|
|
169
|
+
className="text-[10px] font-mono font-semibold text-muted-foreground bg-surface-light px-2 py-0.5 rounded"
|
|
170
|
+
>
|
|
171
|
+
{scope}
|
|
172
|
+
</span>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
{networks && networks.length > 0 && (
|
|
177
|
+
<div className="flex gap-1.5 mt-1.5 flex-wrap">
|
|
178
|
+
{networks.map((net) => (
|
|
179
|
+
<span
|
|
180
|
+
key={net}
|
|
181
|
+
className="text-[10px] font-mono font-semibold text-muted-foreground bg-surface-light px-2 py-0.5 rounded"
|
|
182
|
+
>
|
|
183
|
+
{net}
|
|
184
|
+
</span>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
{url && (
|
|
189
|
+
<a
|
|
190
|
+
href={url}
|
|
191
|
+
target="_blank"
|
|
192
|
+
rel="noopener noreferrer"
|
|
193
|
+
className="text-[11px] mt-1 font-mono text-muted-foreground truncate block hover:underline group/url"
|
|
194
|
+
>
|
|
195
|
+
{url}
|
|
196
|
+
<ArrowSquareOutIcon className="w-3 h-3 inline ml-1 opacity-0 group-hover/url:opacity-100 transition-opacity" />
|
|
197
|
+
</a>
|
|
198
|
+
)}
|
|
199
|
+
{choices && (
|
|
200
|
+
<div className="flex flex-col gap-2 mt-3 pt-3 border-t border-border/50">
|
|
201
|
+
{choices.map((choice) => {
|
|
202
|
+
const isRequired = choice.required === true;
|
|
203
|
+
return (
|
|
204
|
+
<div key={choice.name} className="flex items-start gap-2">
|
|
205
|
+
<Checkbox
|
|
206
|
+
checked={isRequired || selectedSet.has(choice.name)}
|
|
207
|
+
disabled={isRequired}
|
|
208
|
+
onCheckedChange={(details: { checked: boolean | "indeterminate" }) =>
|
|
209
|
+
handleSelect(choice.name, details.checked === true)
|
|
210
|
+
}
|
|
211
|
+
id={`perm-${choice.name}`}
|
|
212
|
+
className="mt-0.5"
|
|
213
|
+
/>
|
|
214
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
215
|
+
<Label
|
|
216
|
+
htmlFor={`perm-${choice.name}`}
|
|
217
|
+
className={`text-xs ${isRequired ? "text-muted-foreground" : ""}`}
|
|
218
|
+
>
|
|
219
|
+
{choice.name}
|
|
220
|
+
{isRequired && (
|
|
221
|
+
<span className="ml-1.5 text-[10px] uppercase tracking-wide opacity-70">
|
|
222
|
+
required
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</Label>
|
|
226
|
+
{choice.reason && (
|
|
227
|
+
<p className="text-[11px] leading-snug text-muted-foreground">
|
|
228
|
+
{choice.reason}
|
|
229
|
+
</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
{choices.some((c) => c.required !== true) && (
|
|
236
|
+
<div className="flex items-center gap-2">
|
|
237
|
+
<Checkbox
|
|
238
|
+
checked={allChoicesSelected}
|
|
239
|
+
onCheckedChange={(details: { checked: boolean | "indeterminate" }) =>
|
|
240
|
+
handleSelectAll(details.checked === true)
|
|
241
|
+
}
|
|
242
|
+
id="perm-select-all"
|
|
243
|
+
/>
|
|
244
|
+
<Label htmlFor="perm-select-all" className="text-xs text-muted-foreground">
|
|
245
|
+
Select all
|
|
246
|
+
</Label>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<Switch
|
|
253
|
+
checked={checked}
|
|
254
|
+
onCheckedChange={(d: { checked: boolean }) => onCheckedChange(d.checked === true)}
|
|
255
|
+
label={title}
|
|
256
|
+
labelClassName="sr-only"
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { PermissionRow };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import { ShieldIcon } from "./icons.tsx";
|
|
3
|
+
import { cn } from "../utils.ts";
|
|
4
|
+
|
|
5
|
+
function SectionLabel({
|
|
6
|
+
icon,
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
}: {
|
|
10
|
+
icon?: ComponentChildren;
|
|
11
|
+
children: ComponentChildren;
|
|
12
|
+
className?: string;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
"text-[11px] font-bold text-muted-foreground uppercase tracking-wide flex items-center gap-1.5",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{icon}
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function PermissionSection({
|
|
28
|
+
danger,
|
|
29
|
+
label,
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
danger?: boolean;
|
|
33
|
+
label?: ComponentChildren;
|
|
34
|
+
children: ComponentChildren;
|
|
35
|
+
}) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={`${danger ? "bg-danger-bg rounded-3xl shadow-clay-danger" : "bg-perm-bg rounded-3xl shadow-clay-amber"} p-4 flex flex-col gap-3`}
|
|
39
|
+
>
|
|
40
|
+
<SectionLabel
|
|
41
|
+
icon={<ShieldIcon className={`w-4 h-4 ${danger ? "text-danger" : "text-perm"}`} />}
|
|
42
|
+
className={danger ? "text-danger" : "text-perm pl-0.5"}
|
|
43
|
+
>
|
|
44
|
+
{label ?? (danger ? "Dangerous permissions" : "Permissions")}
|
|
45
|
+
</SectionLabel>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { PermissionSection, SectionLabel };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Pie / donut chart. A stroke-dasharray ring: one <circle> per segment, drawn
|
|
2
|
+
// as an arc by its dash length + accumulated offset, so there's no arc-path
|
|
3
|
+
// math and no dependency. Colors default to the clay `--chart-*` palette, so it
|
|
4
|
+
// themes with the surface (and flips in dark mode) — SVG references the CSS var
|
|
5
|
+
// directly, no canvas, no redraw hook.
|
|
6
|
+
//
|
|
7
|
+
// Sizing: pass `size` for a fixed diameter, or omit it to fill the parent — it
|
|
8
|
+
// measures the box and draws the largest square that fits (the center label is
|
|
9
|
+
// font-scaled to the hole, so it grows with the donut). Pass `thickness` near
|
|
10
|
+
// half the diameter for a solid pie. Opt into a `legend` and a hover `tooltip`
|
|
11
|
+
// (dims the other slices, floats above the cursor); hovering a legend row
|
|
12
|
+
// highlights its slice too.
|
|
13
|
+
|
|
14
|
+
import type { ComponentChildren } from "preact";
|
|
15
|
+
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
16
|
+
import { cn } from "../utils.ts";
|
|
17
|
+
|
|
18
|
+
export interface PieSegment {
|
|
19
|
+
label: string;
|
|
20
|
+
value: number;
|
|
21
|
+
/** Any CSS color; defaults to the `--chart-N` palette, cycled by index. */
|
|
22
|
+
color?: string;
|
|
23
|
+
/** Pre-formatted value for the legend + tooltip (e.g. "1.9 GB"). Falls back to `value`. */
|
|
24
|
+
valueLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PALETTE = [
|
|
28
|
+
"var(--chart-1)",
|
|
29
|
+
"var(--chart-2)",
|
|
30
|
+
"var(--chart-3)",
|
|
31
|
+
"var(--chart-4)",
|
|
32
|
+
"var(--chart-5)",
|
|
33
|
+
"var(--chart-6)",
|
|
34
|
+
"var(--chart-7)",
|
|
35
|
+
"var(--chart-8)",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function colorOf(seg: PieSegment, i: number): string {
|
|
39
|
+
return seg.color ?? PALETTE[i % PALETTE.length]!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PieChartProps {
|
|
43
|
+
data: PieSegment[];
|
|
44
|
+
/** Outer diameter in px. Omit to fill (and measure) the parent, staying square. */
|
|
45
|
+
size?: number;
|
|
46
|
+
/** Ring width in px. Default 20% of the diameter. Pass ~half the diameter for a solid pie. */
|
|
47
|
+
thickness?: number;
|
|
48
|
+
/** Big value in the hole (donut only). */
|
|
49
|
+
centerLabel?: ComponentChildren;
|
|
50
|
+
/** Caption under the center label. */
|
|
51
|
+
centerCaption?: ComponentChildren;
|
|
52
|
+
/** Ring shown behind the segments — the unfilled remainder reads as this. */
|
|
53
|
+
trackColor?: string;
|
|
54
|
+
/** Render a legend (dot + label + value) beside the donut. */
|
|
55
|
+
legend?: boolean;
|
|
56
|
+
/** Hover a slice → dim the others and float a tooltip above the cursor. */
|
|
57
|
+
tooltip?: boolean;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function PieChart({
|
|
62
|
+
data,
|
|
63
|
+
size,
|
|
64
|
+
thickness,
|
|
65
|
+
centerLabel,
|
|
66
|
+
centerCaption,
|
|
67
|
+
trackColor = "var(--muted)",
|
|
68
|
+
legend = false,
|
|
69
|
+
tooltip = false,
|
|
70
|
+
className,
|
|
71
|
+
}: PieChartProps) {
|
|
72
|
+
// Hover keyed by the segment's label, not its index: the loader swaps `data`
|
|
73
|
+
// out on a refetch (and may re-sort it), so an index would point at the wrong
|
|
74
|
+
// slice — or out of range — while the cursor rests. A label follows the slice.
|
|
75
|
+
const [hoveredLabel, setHoveredLabel] = useState<string | null>(null);
|
|
76
|
+
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
|
|
77
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
78
|
+
const [box, setBox] = useState<{ w: number; h: number } | null>(null);
|
|
79
|
+
const responsive = size == null;
|
|
80
|
+
|
|
81
|
+
// Measure the fill container. useLayoutEffect fires before paint, so the first
|
|
82
|
+
// real frame already has a size — no empty flash.
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
if (!responsive) return;
|
|
85
|
+
const el = rootRef.current;
|
|
86
|
+
if (!el) return;
|
|
87
|
+
const read = () => setBox({ w: el.clientWidth, h: el.clientHeight });
|
|
88
|
+
read();
|
|
89
|
+
const ro = new ResizeObserver(read);
|
|
90
|
+
ro.observe(el);
|
|
91
|
+
return () => ro.disconnect();
|
|
92
|
+
}, [responsive]);
|
|
93
|
+
|
|
94
|
+
// Donut diameter: the fixed `size`, or the largest square that fits the
|
|
95
|
+
// measured parent — leaving half the width for the legend when there is one.
|
|
96
|
+
const s =
|
|
97
|
+
size ?? (box ? Math.max(0, Math.floor(Math.min(box.h, legend ? box.w * 0.5 : box.w))) : 0);
|
|
98
|
+
|
|
99
|
+
const w = thickness ?? s * 0.2;
|
|
100
|
+
const r = (s - w) / 2; // centerline radius, so the stroke stays inside the box
|
|
101
|
+
const hole = s - 2 * w; // inner diameter — the center label has to fit inside this
|
|
102
|
+
const circumference = 2 * Math.PI * r;
|
|
103
|
+
const c = s / 2;
|
|
104
|
+
const total = data.reduce((sum, d) => sum + Math.max(0, d.value), 0);
|
|
105
|
+
const hoverable = legend || tooltip;
|
|
106
|
+
const hovered = hoveredLabel != null ? data.findIndex((d) => d.label === hoveredLabel) : -1;
|
|
107
|
+
const active = hovered >= 0 ? data[hovered] : undefined;
|
|
108
|
+
|
|
109
|
+
let consumed = 0;
|
|
110
|
+
const donut = (
|
|
111
|
+
<div
|
|
112
|
+
class="relative shrink-0"
|
|
113
|
+
style={{ width: s, height: s }}
|
|
114
|
+
onPointerMove={tooltip ? (e) => setPos({ x: e.offsetX, y: e.offsetY }) : undefined}
|
|
115
|
+
onPointerLeave={
|
|
116
|
+
hoverable
|
|
117
|
+
? () => {
|
|
118
|
+
setHoveredLabel(null);
|
|
119
|
+
setPos(null);
|
|
120
|
+
}
|
|
121
|
+
: undefined
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
|
|
125
|
+
{/* rotate -90° around the center so segments start at 12 o'clock */}
|
|
126
|
+
<g transform={`rotate(-90 ${c} ${c})`}>
|
|
127
|
+
<circle
|
|
128
|
+
cx={c}
|
|
129
|
+
cy={c}
|
|
130
|
+
r={r}
|
|
131
|
+
fill="none"
|
|
132
|
+
stroke={trackColor}
|
|
133
|
+
stroke-width={w}
|
|
134
|
+
pointer-events="none"
|
|
135
|
+
/>
|
|
136
|
+
{total > 0 &&
|
|
137
|
+
data.map((d, i) => {
|
|
138
|
+
const len = (Math.max(0, d.value) / total) * circumference;
|
|
139
|
+
const dash = (
|
|
140
|
+
<circle
|
|
141
|
+
key={i}
|
|
142
|
+
cx={c}
|
|
143
|
+
cy={c}
|
|
144
|
+
r={r}
|
|
145
|
+
fill="none"
|
|
146
|
+
stroke={colorOf(d, i)}
|
|
147
|
+
stroke-width={w}
|
|
148
|
+
stroke-dasharray={`${len} ${circumference - len}`}
|
|
149
|
+
stroke-dashoffset={-consumed}
|
|
150
|
+
opacity={hovered >= 0 && hovered !== i ? 0.3 : 1}
|
|
151
|
+
style={{ transition: "opacity 120ms", cursor: hoverable ? "pointer" : undefined }}
|
|
152
|
+
onPointerEnter={hoverable ? () => setHoveredLabel(d.label) : undefined}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
consumed += len;
|
|
156
|
+
return dash;
|
|
157
|
+
})}
|
|
158
|
+
</g>
|
|
159
|
+
</svg>
|
|
160
|
+
{(centerLabel != null || centerCaption != null) && (
|
|
161
|
+
// Confined to the hole (inset by the ring width) and font-scaled to it,
|
|
162
|
+
// so the label always clears the ring instead of crowding it.
|
|
163
|
+
<div
|
|
164
|
+
class="absolute flex flex-col items-center justify-center gap-1 text-center"
|
|
165
|
+
style={{ inset: w, pointerEvents: "none" }}
|
|
166
|
+
>
|
|
167
|
+
{centerLabel != null && (
|
|
168
|
+
<div
|
|
169
|
+
class="font-semibold leading-none tabular-nums"
|
|
170
|
+
style={{ fontSize: Math.round(hole * 0.24) }}
|
|
171
|
+
>
|
|
172
|
+
{centerLabel}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
{centerCaption != null && (
|
|
176
|
+
<div
|
|
177
|
+
class="leading-none text-muted-foreground"
|
|
178
|
+
style={{ fontSize: Math.max(9, Math.round(hole * 0.13)) }}
|
|
179
|
+
>
|
|
180
|
+
{centerCaption}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
{tooltip && active && pos && (
|
|
186
|
+
// Centered above the cursor (outer div positions, inner is the pill) with
|
|
187
|
+
// a caret pointing down at it — symmetric, so it rarely clips on an edge.
|
|
188
|
+
<div
|
|
189
|
+
class="pointer-events-none absolute z-10"
|
|
190
|
+
style={{ left: pos.x, top: pos.y, transform: "translate(-50%, calc(-100% - 10px))" }}
|
|
191
|
+
>
|
|
192
|
+
<div class="relative flex items-center gap-1.5 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1.5 text-xs text-background">
|
|
193
|
+
<span
|
|
194
|
+
class="size-2 shrink-0 rounded-full"
|
|
195
|
+
style={{ background: colorOf(active, hovered) }}
|
|
196
|
+
/>
|
|
197
|
+
<span class="font-medium">{active.label}</span>
|
|
198
|
+
<span class="tabular-nums text-background/70">{active.valueLabel ?? active.value}</span>
|
|
199
|
+
{total > 0 && (
|
|
200
|
+
<span class="tabular-nums text-background/50">
|
|
201
|
+
{Math.round((Math.max(0, active.value) / total) * 100)}%
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
<span class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 rounded-[2px] bg-foreground" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const rootClass = cn(
|
|
212
|
+
responsive
|
|
213
|
+
? "flex h-full w-full items-center justify-center"
|
|
214
|
+
: legend
|
|
215
|
+
? "flex items-center"
|
|
216
|
+
: "inline-block",
|
|
217
|
+
legend && "gap-4",
|
|
218
|
+
className,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div ref={responsive ? rootRef : undefined} class={rootClass} data-slot="pie-chart">
|
|
223
|
+
{s > 0 && donut}
|
|
224
|
+
{legend && s > 0 && (
|
|
225
|
+
<ul class="flex min-w-0 flex-col gap-1 text-xs">
|
|
226
|
+
{data.map((d, i) => (
|
|
227
|
+
<li
|
|
228
|
+
key={i}
|
|
229
|
+
class="flex items-center gap-1.5"
|
|
230
|
+
style={{ opacity: hovered >= 0 && hovered !== i ? 0.4 : 1 }}
|
|
231
|
+
onPointerEnter={() => setHoveredLabel(d.label)}
|
|
232
|
+
onPointerLeave={() => setHoveredLabel(null)}
|
|
233
|
+
>
|
|
234
|
+
<span class="size-2 shrink-0 rounded-full" style={{ background: colorOf(d, i) }} />
|
|
235
|
+
<span class="truncate">{d.label}</span>
|
|
236
|
+
<span class="ml-auto shrink-0 tabular-nums text-muted-foreground">
|
|
237
|
+
{d.valueLabel ?? d.value}
|
|
238
|
+
</span>
|
|
239
|
+
</li>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import { GearIcon } from "./icons.tsx";
|
|
3
|
+
import { SectionLabel } from "./permission-section.tsx";
|
|
4
|
+
import { Switch } from "./switch.tsx";
|
|
5
|
+
|
|
6
|
+
// Visual chrome for an `r.tab(...)` block in the install/edit form.
|
|
7
|
+
// Both Home's TabsForm and the docs site's SchemaPreviews render
|
|
8
|
+
// through this so the two can't drift.
|
|
9
|
+
export function TabBlock({ name, children }: { name: string; children: ComponentChildren }) {
|
|
10
|
+
return (
|
|
11
|
+
<section className="bg-trust-bg rounded-3xl shadow-clay-blue p-4 flex flex-col gap-3">
|
|
12
|
+
<SectionLabel
|
|
13
|
+
icon={<GearIcon className="w-4 h-4 text-trust" />}
|
|
14
|
+
className="text-trust pl-0.5"
|
|
15
|
+
>
|
|
16
|
+
{name}
|
|
17
|
+
</SectionLabel>
|
|
18
|
+
{children}
|
|
19
|
+
</section>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Visual chrome for an `r.section(...)` block. Pass `optional` to render
|
|
24
|
+
// the on/off toggle; controlled via `on` + `onChange`. Children are
|
|
25
|
+
// always rendered regardless of `on` - the toggle only flips the section's
|
|
26
|
+
// declared state, leaves keep showing so the user can still see what's
|
|
27
|
+
// inside.
|
|
28
|
+
export function SectionBlock({
|
|
29
|
+
name,
|
|
30
|
+
optional,
|
|
31
|
+
on,
|
|
32
|
+
onChange,
|
|
33
|
+
disabled,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
name: string;
|
|
37
|
+
optional?: boolean;
|
|
38
|
+
on?: boolean;
|
|
39
|
+
onChange?: (next: boolean) => void;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
children: ComponentChildren;
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<fieldset className="bg-card rounded-2xl shadow p-4 flex flex-col gap-3">
|
|
45
|
+
<legend className="flex items-center gap-2 text-sm font-bold text-foreground px-1">
|
|
46
|
+
<span>{name}</span>
|
|
47
|
+
{optional && (
|
|
48
|
+
<Switch
|
|
49
|
+
checked={on === true}
|
|
50
|
+
onCheckedChange={(d: { checked: boolean }) => onChange?.(d.checked === true)}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
label={name}
|
|
53
|
+
labelClassName="sr-only"
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
</legend>
|
|
57
|
+
{children}
|
|
58
|
+
</fieldset>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn, type ElementProps } from "../utils.ts";
|
|
2
|
+
|
|
3
|
+
function Separator({
|
|
4
|
+
className,
|
|
5
|
+
orientation = "horizontal",
|
|
6
|
+
children,
|
|
7
|
+
...props
|
|
8
|
+
}: ElementProps<"div"> & {
|
|
9
|
+
orientation?: "horizontal" | "vertical";
|
|
10
|
+
}) {
|
|
11
|
+
if (children) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
role="separator"
|
|
16
|
+
className={cn("flex items-center gap-4", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<div className="h-px flex-1 bg-gradient-to-r from-transparent to-border" />
|
|
20
|
+
<span className="text-sm font-bold uppercase tracking-wider text-muted-foreground">
|
|
21
|
+
{children}
|
|
22
|
+
</span>
|
|
23
|
+
<div className="h-px flex-1 bg-gradient-to-l from-transparent to-border" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-slot="separator"
|
|
31
|
+
role="separator"
|
|
32
|
+
data-orientation={orientation}
|
|
33
|
+
className={cn(
|
|
34
|
+
"shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=horizontal]:bg-gradient-to-r data-[orientation=horizontal]:from-transparent data-[orientation=horizontal]:via-border data-[orientation=horizontal]:to-transparent data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch data-[orientation=vertical]:bg-border",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { Separator };
|