@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,506 @@
|
|
|
1
|
+
import type { ComponentChildren, JSX } from "preact";
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
import { cn, type ElementProps } from "../utils.ts";
|
|
5
|
+
import { buttonVariants } from "./button.tsx";
|
|
6
|
+
import {
|
|
7
|
+
CaretDownIcon,
|
|
8
|
+
CaretLeftIcon,
|
|
9
|
+
CaretRightIcon,
|
|
10
|
+
CaretUpDownIcon,
|
|
11
|
+
CaretUpIcon,
|
|
12
|
+
} from "./icons.tsx";
|
|
13
|
+
import {
|
|
14
|
+
clampPage,
|
|
15
|
+
pageCount,
|
|
16
|
+
pageSlice,
|
|
17
|
+
pageSummary,
|
|
18
|
+
pageWindow,
|
|
19
|
+
type SortDirection,
|
|
20
|
+
type SortState,
|
|
21
|
+
type SortValue,
|
|
22
|
+
sortRows,
|
|
23
|
+
toggleSort,
|
|
24
|
+
} from "./table-view.ts";
|
|
25
|
+
|
|
26
|
+
// Data table, ported from shadcn's Table (`data-slot="table"`). Plain HTML —
|
|
27
|
+
// there is no Ark primitive to wrap — so the port is the styling only, plus
|
|
28
|
+
// three additions shadcn has no answer for.
|
|
29
|
+
//
|
|
30
|
+
// 1. `<Table>` is two boxes: an outer card that clips, and an inner viewport
|
|
31
|
+
// that scrolls sideways. Scrolling has to live inside the card or the page
|
|
32
|
+
// body scrolls sideways instead — and the card has to be its own box or
|
|
33
|
+
// `footer` (the pager) has nowhere to sit that isn't inside the scroller.
|
|
34
|
+
// 2. `truncate` on a cell + `width` on a head. Auto table layout hands a long
|
|
35
|
+
// unbroken value all the width it asks for and wraps the short columns
|
|
36
|
+
// instead; `max-w-0` on the cell caps that greed and the inner div clips,
|
|
37
|
+
// while `width` on the head states the intent up front. `layout="fixed"`
|
|
38
|
+
// is the stronger form when every column is known.
|
|
39
|
+
// 3. Sorting and paging. Both are controlled: the caller owns the state, and
|
|
40
|
+
// `TableHead` / `TablePagination` render anchors or buttons depending on
|
|
41
|
+
// which callback they were handed. A plain Fresh route that never hydrates
|
|
42
|
+
// (`/admin/audit`) drives them with `<a href>` and query params; a widget
|
|
43
|
+
// or island drives them with `useTableView`. The rules behind both live in
|
|
44
|
+
// `table-view.ts`, so neither path owns a decision the other doesn't have.
|
|
45
|
+
|
|
46
|
+
type TableProps = ElementProps<"table"> & {
|
|
47
|
+
/** `fixed` sizes columns from the header row alone, ignoring cell content. */
|
|
48
|
+
layout?: "auto" | "fixed";
|
|
49
|
+
/**
|
|
50
|
+
* Rendered under the scroll viewport, inside the card. A `<TablePagination>`
|
|
51
|
+
* in practice.
|
|
52
|
+
*
|
|
53
|
+
* Not a `<tfoot>`: a tfoot lives inside the scroll viewport, so on a table
|
|
54
|
+
* wide enough to scroll the pager sits off the right edge until you find it,
|
|
55
|
+
* and under `layout="fixed"` it inherits the header's column widths, so a
|
|
56
|
+
* full-width bar needs a `colSpan` every call site has to keep in step.
|
|
57
|
+
*/
|
|
58
|
+
footer?: ComponentChildren;
|
|
59
|
+
/**
|
|
60
|
+
* Fill the parent's height and scroll inside, with the header pinned.
|
|
61
|
+
*
|
|
62
|
+
* Sticky is folded in here rather than being its own prop because the card
|
|
63
|
+
* clips: a sticky header is only reachable when the viewport is the thing
|
|
64
|
+
* that scrolls. A header that scrolls away in a filled table leaves a screen
|
|
65
|
+
* of unlabelled columns.
|
|
66
|
+
*/
|
|
67
|
+
fill?: boolean;
|
|
68
|
+
/** `compact` tightens cell padding and drops to `text-xs`, for dense monitors. */
|
|
69
|
+
density?: "default" | "compact";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function Table({
|
|
73
|
+
className,
|
|
74
|
+
layout = "auto",
|
|
75
|
+
density = "default",
|
|
76
|
+
fill,
|
|
77
|
+
footer,
|
|
78
|
+
...props
|
|
79
|
+
}: TableProps) {
|
|
80
|
+
return (
|
|
81
|
+
// The split is visually a no-op for a plain table: `overflow-x: auto`
|
|
82
|
+
// already forces `overflow-y` to compute to `auto`, and `overflow: hidden`
|
|
83
|
+
// here clips at the same padding box — the rounded corners still cut the
|
|
84
|
+
// header's tint, and the scrollbar still sits inside the border.
|
|
85
|
+
<div
|
|
86
|
+
data-slot="table-card"
|
|
87
|
+
className={cn(
|
|
88
|
+
"w-full overflow-hidden rounded-2xl border border-border bg-card",
|
|
89
|
+
fill && "flex h-full min-h-0 flex-col",
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
data-slot="table-container"
|
|
94
|
+
className={cn("w-full overflow-x-auto", fill && "min-h-0 flex-1 overflow-auto")}
|
|
95
|
+
>
|
|
96
|
+
<table
|
|
97
|
+
data-slot="table"
|
|
98
|
+
data-layout={layout}
|
|
99
|
+
data-density={density}
|
|
100
|
+
data-fill={fill ? "true" : undefined}
|
|
101
|
+
className={cn(
|
|
102
|
+
"w-full text-sm data-[layout=fixed]:table-fixed data-[density=compact]:text-xs",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
{footer}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function TableHeader({ className, ...props }: ElementProps<"thead">) {
|
|
114
|
+
return (
|
|
115
|
+
<thead
|
|
116
|
+
data-slot="table-header"
|
|
117
|
+
className={cn(
|
|
118
|
+
"bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground",
|
|
119
|
+
// Under `fill` the viewport is the scroller, so the header pins to it.
|
|
120
|
+
// Read off the table's `data-fill` rather than a prop, which keeps the
|
|
121
|
+
// three leaf components stateless and prop-free.
|
|
122
|
+
"[[data-fill]_&]:sticky [[data-fill]_&]:top-0 [[data-fill]_&]:z-10 [[data-fill]_&]:bg-muted/60 [[data-fill]_&]:backdrop-blur",
|
|
123
|
+
className,
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function TableBody({ className, ...props }: ElementProps<"tbody">) {
|
|
131
|
+
return <tbody data-slot="table-body" className={cn(className)} {...props} />;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function TableRow({ className, ...props }: ElementProps<"tr">) {
|
|
135
|
+
return (
|
|
136
|
+
<tr
|
|
137
|
+
data-slot="table-row"
|
|
138
|
+
className={cn("border-t border-border first:border-t-0", className)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type TableHeadProps = ElementProps<"th"> & {
|
|
145
|
+
/** Any CSS length or percentage. A hint under `auto`, binding under `fixed`. */
|
|
146
|
+
width?: string;
|
|
147
|
+
/** This column's direction, or `null` for "sortable, but not the sort". */
|
|
148
|
+
sorted?: SortDirection | null;
|
|
149
|
+
/** Renders the label as an `<a>`. The only form that works unhydrated. */
|
|
150
|
+
sortHref?: string;
|
|
151
|
+
/** Renders the label as a `<button>`. Ignored when `sortHref` is given. */
|
|
152
|
+
onSort?: (event: JSX.TargetedMouseEvent<HTMLElement>) => void;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const ARIA_SORT = { asc: "ascending", desc: "descending" } as const;
|
|
156
|
+
|
|
157
|
+
function TableHead({
|
|
158
|
+
className,
|
|
159
|
+
width,
|
|
160
|
+
style,
|
|
161
|
+
sorted = null,
|
|
162
|
+
sortHref,
|
|
163
|
+
onSort,
|
|
164
|
+
children,
|
|
165
|
+
...props
|
|
166
|
+
}: TableHeadProps) {
|
|
167
|
+
const sortable = sortHref !== undefined || onSort !== undefined;
|
|
168
|
+
// The trigger is the label, not the `<th>`: a click target covering a 14rem
|
|
169
|
+
// cell fires on dead space, and a `<th>` is not keyboard reachable however
|
|
170
|
+
// many handlers it carries.
|
|
171
|
+
//
|
|
172
|
+
// `-mx-1.5` cancels the trigger's own padding, so a sortable label sits on
|
|
173
|
+
// the same pixel as a plain one in the header beside it.
|
|
174
|
+
const triggerClass = cn(
|
|
175
|
+
"group/sort -mx-1.5 inline-flex max-w-full cursor-pointer items-center gap-1 rounded-md px-1.5 py-0.5 text-left align-middle outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
176
|
+
sorted && "text-foreground",
|
|
177
|
+
);
|
|
178
|
+
const caret =
|
|
179
|
+
sorted === "asc" ? (
|
|
180
|
+
<CaretUpIcon className="size-3 shrink-0" />
|
|
181
|
+
) : sorted === "desc" ? (
|
|
182
|
+
<CaretDownIcon className="size-3 shrink-0" />
|
|
183
|
+
) : (
|
|
184
|
+
// Revealed on hover rather than always drawn: six permanent arrows compete
|
|
185
|
+
// with the one that says what the sort actually is. `opacity-0` and not
|
|
186
|
+
// `hidden`, so the column doesn't jump wider the moment it becomes sorted.
|
|
187
|
+
<CaretUpDownIcon className="size-3 shrink-0 opacity-0 transition-opacity group-hover/sort:opacity-60" />
|
|
188
|
+
);
|
|
189
|
+
return (
|
|
190
|
+
<th
|
|
191
|
+
data-slot="table-head"
|
|
192
|
+
// "none" rather than omitted on the unsorted columns: that is what tells
|
|
193
|
+
// a screen reader the other headers are sortable at all.
|
|
194
|
+
aria-sort={sortable ? (sorted ? ARIA_SORT[sorted] : "none") : undefined}
|
|
195
|
+
style={width ? { width, ...(style as Record<string, unknown>) } : style}
|
|
196
|
+
className={cn(
|
|
197
|
+
"whitespace-nowrap px-4 py-2.5 text-left font-semibold",
|
|
198
|
+
"[[data-density=compact]_&]:px-3 [[data-density=compact]_&]:py-2",
|
|
199
|
+
className,
|
|
200
|
+
)}
|
|
201
|
+
{...props}
|
|
202
|
+
>
|
|
203
|
+
{!sortable ? (
|
|
204
|
+
children
|
|
205
|
+
) : sortHref !== undefined ? (
|
|
206
|
+
// `data-sorted` and not the glyph: an e2e selector shouldn't depend on
|
|
207
|
+
// which caret rendered.
|
|
208
|
+
<a
|
|
209
|
+
href={sortHref}
|
|
210
|
+
data-slot="table-sort"
|
|
211
|
+
data-sorted={sorted ?? "none"}
|
|
212
|
+
class={triggerClass}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
{caret}
|
|
216
|
+
</a>
|
|
217
|
+
) : (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
data-slot="table-sort"
|
|
221
|
+
data-sorted={sorted ?? "none"}
|
|
222
|
+
class={triggerClass}
|
|
223
|
+
onClick={onSort}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
{caret}
|
|
227
|
+
</button>
|
|
228
|
+
)}
|
|
229
|
+
</th>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
type TableCellProps = ElementProps<"td"> & {
|
|
234
|
+
/** Clip to one line with an ellipsis instead of wrapping. Sets `title` from `text`. */
|
|
235
|
+
truncate?: boolean;
|
|
236
|
+
/** Tooltip + ellipsis source when the cell's children aren't a plain string. */
|
|
237
|
+
text?: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function TableCell({ className, truncate, text, children, ...props }: TableCellProps) {
|
|
241
|
+
const title = text ?? (typeof children === "string" ? children : undefined);
|
|
242
|
+
return (
|
|
243
|
+
<td
|
|
244
|
+
data-slot="table-cell"
|
|
245
|
+
// max-w-0 stops an auto-layout column claiming width from its content;
|
|
246
|
+
// the cap is the share the header widths leave it, and the inner div
|
|
247
|
+
// clips to that. Without it `truncate` on the cell alone does nothing.
|
|
248
|
+
className={cn(
|
|
249
|
+
"px-4 py-3 align-middle",
|
|
250
|
+
"[[data-density=compact]_&]:px-3 [[data-density=compact]_&]:py-1",
|
|
251
|
+
truncate && "max-w-0",
|
|
252
|
+
className,
|
|
253
|
+
)}
|
|
254
|
+
{...props}
|
|
255
|
+
>
|
|
256
|
+
{truncate ? (
|
|
257
|
+
<div class="truncate" title={title}>
|
|
258
|
+
{children}
|
|
259
|
+
</div>
|
|
260
|
+
) : (
|
|
261
|
+
children
|
|
262
|
+
)}
|
|
263
|
+
</td>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type PageControlProps = {
|
|
268
|
+
page: number;
|
|
269
|
+
label: string;
|
|
270
|
+
/** The page being rendered is the one on screen. */
|
|
271
|
+
current?: boolean;
|
|
272
|
+
/** No such page. Renders inert markup, never a link. */
|
|
273
|
+
disabled?: boolean;
|
|
274
|
+
hrefFor?: (page: number) => string;
|
|
275
|
+
onPageChange?: (page: number) => void;
|
|
276
|
+
children: ComponentChildren;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
function PageControl({
|
|
280
|
+
page,
|
|
281
|
+
label,
|
|
282
|
+
current,
|
|
283
|
+
disabled,
|
|
284
|
+
hrefFor,
|
|
285
|
+
onPageChange,
|
|
286
|
+
children,
|
|
287
|
+
}: PageControlProps) {
|
|
288
|
+
const className = cn(
|
|
289
|
+
buttonVariants({ variant: current ? "secondary" : "ghost", size: "xs" }),
|
|
290
|
+
// tabular-nums + a floor on the width so the box doesn't resize between
|
|
291
|
+
// 9 and 10 and shuffle every control after it.
|
|
292
|
+
"min-w-7 justify-center tabular-nums",
|
|
293
|
+
);
|
|
294
|
+
// A dead control is a `<span>`, never an anchor: anchors have no `disabled`,
|
|
295
|
+
// `pointer-events-none` does nothing for the keyboard, and an `href` that
|
|
296
|
+
// names a page which doesn't exist is a link that lies.
|
|
297
|
+
if (disabled || current) {
|
|
298
|
+
return (
|
|
299
|
+
<span
|
|
300
|
+
aria-current={current ? "page" : undefined}
|
|
301
|
+
aria-disabled={disabled ? "true" : undefined}
|
|
302
|
+
aria-label={current ? undefined : label}
|
|
303
|
+
className={cn(className, disabled && "opacity-40")}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
306
|
+
</span>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (hrefFor) {
|
|
310
|
+
return (
|
|
311
|
+
// `aria-current="false"` is spelled out rather than left off: Fresh stamps
|
|
312
|
+
// its own `aria-current` onto every root-relative `<a>` whose PATH matches
|
|
313
|
+
// the current one, backing off only when the prop is already set
|
|
314
|
+
// (`setActiveUrl` in fresh/runtime). Every control in a pager shares that
|
|
315
|
+
// path and differs only in the query, so without this Fresh marks them all
|
|
316
|
+
// and announces a control the reader is NOT on as the current page.
|
|
317
|
+
// "false" is the ARIA default, so it is inert everywhere else.
|
|
318
|
+
<a href={hrefFor(page)} aria-label={label} aria-current="false" className={className}>
|
|
319
|
+
{children}
|
|
320
|
+
</a>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return (
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
aria-label={label}
|
|
327
|
+
className={className}
|
|
328
|
+
onClick={() => onPageChange?.(page)}
|
|
329
|
+
>
|
|
330
|
+
{children}
|
|
331
|
+
</button>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
type TablePaginationBase = {
|
|
336
|
+
page: number;
|
|
337
|
+
pageSize: number;
|
|
338
|
+
total: number;
|
|
339
|
+
/** Pages either side of the current one. The row is `2 * siblings + 5` wide. */
|
|
340
|
+
siblings?: number;
|
|
341
|
+
/** Extra controls between the summary and the nav, e.g. a rows-per-page picker. */
|
|
342
|
+
children?: ComponentChildren;
|
|
343
|
+
className?: string;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The pager, for `<Table footer={...}>`.
|
|
348
|
+
*
|
|
349
|
+
* Exactly one of `hrefFor` / `onPageChange`, enforced in the type: a pager that
|
|
350
|
+
* looks live and does nothing is a compile error rather than a bug report. There
|
|
351
|
+
* is no `pageCount` prop either — it derives from `total` and `pageSize`, and two
|
|
352
|
+
* inputs that can disagree is one too many.
|
|
353
|
+
*/
|
|
354
|
+
type TablePaginationProps = TablePaginationBase &
|
|
355
|
+
(
|
|
356
|
+
| { hrefFor: (page: number) => string; onPageChange?: never }
|
|
357
|
+
| { onPageChange: (page: number) => void; hrefFor?: never }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
function TablePagination({
|
|
361
|
+
page,
|
|
362
|
+
pageSize,
|
|
363
|
+
total,
|
|
364
|
+
siblings = 1,
|
|
365
|
+
children,
|
|
366
|
+
className,
|
|
367
|
+
hrefFor,
|
|
368
|
+
onPageChange,
|
|
369
|
+
}: TablePaginationProps) {
|
|
370
|
+
const count = pageCount(total, pageSize);
|
|
371
|
+
const current = clampPage(page, count);
|
|
372
|
+
const pages = pageWindow(current, count, siblings);
|
|
373
|
+
return (
|
|
374
|
+
<div
|
|
375
|
+
data-slot="table-pagination"
|
|
376
|
+
className={cn(
|
|
377
|
+
"flex flex-wrap items-center justify-between gap-3 border-t border-border px-4 py-2 text-xs text-muted-foreground",
|
|
378
|
+
className,
|
|
379
|
+
)}
|
|
380
|
+
>
|
|
381
|
+
<span data-slot="table-pagination-summary" className="tabular-nums">
|
|
382
|
+
{pageSummary(current, pageSize, total)}
|
|
383
|
+
</span>
|
|
384
|
+
{children ? <div className="flex flex-wrap items-center gap-2">{children}</div> : null}
|
|
385
|
+
<nav aria-label="Pagination" className="flex items-center gap-1">
|
|
386
|
+
<PageControl
|
|
387
|
+
page={current - 1}
|
|
388
|
+
label="Previous page"
|
|
389
|
+
disabled={current <= 1}
|
|
390
|
+
hrefFor={hrefFor}
|
|
391
|
+
onPageChange={onPageChange}
|
|
392
|
+
>
|
|
393
|
+
<CaretLeftIcon className="size-3" />
|
|
394
|
+
</PageControl>
|
|
395
|
+
{pages.map((entry, index) =>
|
|
396
|
+
entry === "ellipsis" ? (
|
|
397
|
+
<span key={`gap-${index}`} aria-hidden="true" className="px-1">
|
|
398
|
+
…
|
|
399
|
+
</span>
|
|
400
|
+
) : (
|
|
401
|
+
<PageControl
|
|
402
|
+
key={entry}
|
|
403
|
+
page={entry}
|
|
404
|
+
label={`Page ${entry}`}
|
|
405
|
+
current={entry === current}
|
|
406
|
+
hrefFor={hrefFor}
|
|
407
|
+
onPageChange={onPageChange}
|
|
408
|
+
>
|
|
409
|
+
{entry}
|
|
410
|
+
</PageControl>
|
|
411
|
+
),
|
|
412
|
+
)}
|
|
413
|
+
<PageControl
|
|
414
|
+
page={current + 1}
|
|
415
|
+
label="Next page"
|
|
416
|
+
disabled={current >= count}
|
|
417
|
+
hrefFor={hrefFor}
|
|
418
|
+
onPageChange={onPageChange}
|
|
419
|
+
>
|
|
420
|
+
<CaretRightIcon className="size-3" />
|
|
421
|
+
</PageControl>
|
|
422
|
+
</nav>
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface TableView<T, K extends string> {
|
|
428
|
+
/** The rows to render: sorted, and sliced to the current page. */
|
|
429
|
+
rows: readonly T[];
|
|
430
|
+
/** Rows before paging, which is what `TablePagination` wants. */
|
|
431
|
+
total: number;
|
|
432
|
+
page: number;
|
|
433
|
+
pageCount: number;
|
|
434
|
+
pageSize: number;
|
|
435
|
+
setPage: (page: number) => void;
|
|
436
|
+
/** This column's direction, or `null` — feeds `TableHead`'s `sorted`. */
|
|
437
|
+
sorted: (key: K) => SortDirection | null;
|
|
438
|
+
toggle: (key: K, initial?: SortDirection) => void;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Client-side sorting and paging for a hydrated table.
|
|
443
|
+
*
|
|
444
|
+
* Both halves live in one hook because "sorting resets to page 1" is the one
|
|
445
|
+
* invariant a call site can't see: an offset into a re-sorted list is noise,
|
|
446
|
+
* and splitting the hook means repeating `setPage(1)` next to every column.
|
|
447
|
+
*
|
|
448
|
+
* Safe inside a widget iframe, and deliberately so. The runtime's boot gate
|
|
449
|
+
* blanks the whole author subtree while a loader has no data and is fetching,
|
|
450
|
+
* and a loader's query key includes its `request`, so paging by `setState`
|
|
451
|
+
* would re-key the query, blank the data, trip the gate and destroy every
|
|
452
|
+
* nested component's state. Sorting and slicing what's already loaded never
|
|
453
|
+
* touches `request`, so it never fires. Page on the server only when the row
|
|
454
|
+
* count genuinely can't be held in memory.
|
|
455
|
+
*
|
|
456
|
+
* A non-island Fresh route can't use this at all — `useState` there renders its
|
|
457
|
+
* initial value and a header click does nothing. Drive `TableHead`'s `sortHref`
|
|
458
|
+
* and `TablePagination`'s `hrefFor` off query params instead, the same way
|
|
459
|
+
* `timeago.tsx` points non-islands at their own shell.
|
|
460
|
+
*/
|
|
461
|
+
export function useTableView<T, K extends string>(
|
|
462
|
+
rows: readonly T[],
|
|
463
|
+
options: {
|
|
464
|
+
columns: Record<K, (row: T) => SortValue>;
|
|
465
|
+
// NoInfer, or `K` gets pinned to the single key spelled here instead of the
|
|
466
|
+
// union of the `columns` literal, and every other column stops typechecking.
|
|
467
|
+
initial?: { key: NoInfer<K>; direction: SortDirection };
|
|
468
|
+
/** Omit for sort-only: rows come back whole and `pageCount` is 1. */
|
|
469
|
+
pageSize?: number;
|
|
470
|
+
},
|
|
471
|
+
): TableView<T, K> {
|
|
472
|
+
const [sort, setSort] = useState<SortState | null>(options.initial ?? null);
|
|
473
|
+
const [page, setPage] = useState(1);
|
|
474
|
+
|
|
475
|
+
// No useMemo: this is O(n log n) over a list the caller then walks anyway,
|
|
476
|
+
// and the `columns` object literal every call site writes is a new identity
|
|
477
|
+
// per render, which would make the memo a permanent miss with extra steps.
|
|
478
|
+
const sorted = sortRows(rows, sort, options.columns);
|
|
479
|
+
const wanted = options.pageSize ?? 0;
|
|
480
|
+
const paged = wanted > 0;
|
|
481
|
+
const size = paged ? wanted : sorted.length;
|
|
482
|
+
const count = pageCount(sorted.length, size);
|
|
483
|
+
// Clamped on read rather than corrected in an effect: a row deleted off the
|
|
484
|
+
// end of the last page would otherwise paint one empty frame first, and
|
|
485
|
+
// inside a widget iframe that frame is a blank card. The cost is that the
|
|
486
|
+
// stored page survives the shrink, so a list that shrinks below the current
|
|
487
|
+
// page and grows back returns the reader to where they were, not where the
|
|
488
|
+
// shrink left them.
|
|
489
|
+
const current = clampPage(page, count);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
rows: paged ? pageSlice(sorted, current, size) : sorted,
|
|
493
|
+
total: sorted.length,
|
|
494
|
+
page: current,
|
|
495
|
+
pageCount: count,
|
|
496
|
+
pageSize: size,
|
|
497
|
+
setPage,
|
|
498
|
+
sorted: (key) => (sort?.key === key ? sort.direction : null),
|
|
499
|
+
toggle: (key, initial) => {
|
|
500
|
+
setSort((cur) => toggleSort(cur, key, initial));
|
|
501
|
+
setPage(1);
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export { Table, TableBody, TableCell, TableHead, TableHeader, TablePagination, TableRow };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ComponentProps } from "preact";
|
|
2
|
+
import { Tabs as TabsPrimitive } from "@ark-ui/react/tabs";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../utils.ts";
|
|
5
|
+
|
|
6
|
+
// shadcn-style Tabs ported onto Ark UI's tabs machine (Zag). Radix's
|
|
7
|
+
// `asChild` polymorphism is dropped (preact/compat) — same convention as the
|
|
8
|
+
// rest of this dir. Active styling keys off `aria-selected` (the ARIA tabs
|
|
9
|
+
// attribute Zag sets on the active trigger) — NOT Radix's `data-state=active`,
|
|
10
|
+
// which Ark never emits. Triggers accept arbitrary children, so a caller can
|
|
11
|
+
// append a count badge next to the label.
|
|
12
|
+
|
|
13
|
+
function Tabs({ className, ...props }: ComponentProps<typeof TabsPrimitive.Root>) {
|
|
14
|
+
return (
|
|
15
|
+
<TabsPrimitive.Root
|
|
16
|
+
data-slot="tabs"
|
|
17
|
+
className={cn("flex flex-col gap-3", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function TabsList({ className, ...props }: ComponentProps<typeof TabsPrimitive.List>) {
|
|
24
|
+
return (
|
|
25
|
+
<TabsPrimitive.List
|
|
26
|
+
data-slot="tabs-list"
|
|
27
|
+
className={cn(
|
|
28
|
+
"inline-flex w-fit max-w-full flex-wrap items-center gap-1 rounded-2xl bg-muted p-1 text-muted-foreground",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TabsTrigger({ className, ...props }: ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
37
|
+
return (
|
|
38
|
+
<TabsPrimitive.Trigger
|
|
39
|
+
data-slot="tabs-trigger"
|
|
40
|
+
className={cn(
|
|
41
|
+
"inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-xl px-3 py-1.5 text-sm font-medium whitespace-nowrap text-muted-foreground transition-all outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-selected:bg-card aria-selected:font-semibold aria-selected:text-foreground aria-selected:shadow",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function TabsContent({ className, ...props }: ComponentProps<typeof TabsPrimitive.Content>) {
|
|
50
|
+
return (
|
|
51
|
+
<TabsPrimitive.Content
|
|
52
|
+
data-slot="tabs-content"
|
|
53
|
+
className={cn("flex flex-col gap-3 outline-none", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Relative-time helper + presentational <TimeAgo /> wrapper. Renders the
|
|
2
|
+
// relative string ("5 minutes ago", "in 2 days") inside an Ark Tooltip
|
|
3
|
+
// that reveals the absolute UTC timestamp on hover.
|
|
4
|
+
//
|
|
5
|
+
// Pure render-time computation by default: we don't tick on a timer because
|
|
6
|
+
// most callers either re-render naturally (island state changes) or are fine
|
|
7
|
+
// with the value staying frozen until next navigation. Pass `live` for the
|
|
8
|
+
// case that isn't - a duration that must advance while its subject is still
|
|
9
|
+
// running, where nothing else re-renders because nothing else changed.
|
|
10
|
+
//
|
|
11
|
+
// The Ark Tooltip needs hydration to open; consumers rendering this in a
|
|
12
|
+
// non-island Fresh route should wrap it in their own island shell (e.g.
|
|
13
|
+
// `platform/home/src/islands/TimeAgo.tsx`).
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from "preact/hooks";
|
|
16
|
+
|
|
17
|
+
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
18
|
+
|
|
19
|
+
const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
|
|
20
|
+
["year", 60 * 60 * 24 * 365],
|
|
21
|
+
["month", 60 * 60 * 24 * 30],
|
|
22
|
+
["week", 60 * 60 * 24 * 7],
|
|
23
|
+
["day", 60 * 60 * 24],
|
|
24
|
+
["hour", 60 * 60],
|
|
25
|
+
["minute", 60],
|
|
26
|
+
["second", 1],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function timeago(date: Date, now = new Date()): string {
|
|
30
|
+
const diffSeconds = (date.getTime() - now.getTime()) / 1000;
|
|
31
|
+
for (const [unit, secondsInUnit] of UNITS) {
|
|
32
|
+
if (Math.abs(diffSeconds) >= secondsInUnit || unit === "second") {
|
|
33
|
+
return rtf.format(Math.round(diffSeconds / secondsInUnit), unit);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rtf.format(0, "second");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fixed locale + UTC so SSR and client produce identical strings.
|
|
40
|
+
function formatAbsolute(date: Date): string {
|
|
41
|
+
return date.toLocaleString("en-US", {
|
|
42
|
+
year: "numeric",
|
|
43
|
+
month: "short",
|
|
44
|
+
day: "numeric",
|
|
45
|
+
hour: "2-digit",
|
|
46
|
+
minute: "2-digit",
|
|
47
|
+
timeZone: "UTC",
|
|
48
|
+
timeZoneName: "short",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.tsx";
|
|
53
|
+
|
|
54
|
+
export function TimeAgo({
|
|
55
|
+
value,
|
|
56
|
+
className,
|
|
57
|
+
live,
|
|
58
|
+
}: {
|
|
59
|
+
value: Date | string;
|
|
60
|
+
className?: string;
|
|
61
|
+
live?: boolean;
|
|
62
|
+
}) {
|
|
63
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
64
|
+
// `timeago()` reads the clock at render, so the string only moves when
|
|
65
|
+
// something re-renders. A polled loader whose payload is unchanged doesn't
|
|
66
|
+
// re-render (the query keeps the same data by structural sharing), which is
|
|
67
|
+
// exactly the in-flight case - so `live` drives its own tick.
|
|
68
|
+
const [, tick] = useState(0);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!live) return;
|
|
71
|
+
const id = setInterval(() => tick((n) => n + 1), 1000);
|
|
72
|
+
return () => clearInterval(id);
|
|
73
|
+
}, [live]);
|
|
74
|
+
return (
|
|
75
|
+
<Tooltip>
|
|
76
|
+
<TooltipTrigger>
|
|
77
|
+
<time className={className} dateTime={date.toISOString()}>
|
|
78
|
+
{timeago(date)}
|
|
79
|
+
</time>
|
|
80
|
+
</TooltipTrigger>
|
|
81
|
+
<TooltipContent side="top">{formatAbsolute(date)}</TooltipContent>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
);
|
|
84
|
+
}
|