@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.
Files changed (230) hide show
  1. package/LICENSE-EXCEPTION +26 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3R6KEFOI.js +22 -0
  4. package/dist/chunk-67BDAMYX.js +376 -0
  5. package/dist/chunk-AITGS27R.js +139 -0
  6. package/dist/chunk-CMAEHQ4F.js +6 -0
  7. package/dist/chunk-MLKGABMK.js +9 -0
  8. package/dist/chunk-O3UIK34B.js +1430 -0
  9. package/dist/chunk-VHTUSZUX.js +176 -0
  10. package/dist/cli.js +9006 -0
  11. package/dist/docker.js +54 -0
  12. package/dist/index.js +1053 -0
  13. package/dist/ingress.js +436 -0
  14. package/dist/jsx-runtime.js +2 -0
  15. package/dist/preact.js +22 -0
  16. package/dist/service-url.js +11 -0
  17. package/dist/services.js +213 -0
  18. package/dist/test.js +5069 -0
  19. package/dist/ui.js +254 -0
  20. package/dist/worker-protocol.js +89 -0
  21. package/package.json +112 -0
  22. package/src/app-icon.ts +74 -0
  23. package/src/apply.ts +52 -0
  24. package/src/bundle-ui.ts +211 -0
  25. package/src/bundle.ts +546 -0
  26. package/src/cli/build-dev-landing.ts +84 -0
  27. package/src/cli/build-install-panel.ts +90 -0
  28. package/src/cli/build-runtime.ts +133 -0
  29. package/src/cli/build.ts +63 -0
  30. package/src/cli/dev.ts +107 -0
  31. package/src/cli/down.ts +60 -0
  32. package/src/cli/main.ts +56 -0
  33. package/src/cli/test.ts +61 -0
  34. package/src/cli/validate.ts +37 -0
  35. package/src/compile.ts +100 -0
  36. package/src/create-app.ts +982 -0
  37. package/src/deep-merge.ts +33 -0
  38. package/src/dev/fixture.ts +48 -0
  39. package/src/dev/install-handler.ts +383 -0
  40. package/src/dev/server.ts +968 -0
  41. package/src/dev/stack.ts +108 -0
  42. package/src/dev/state.ts +159 -0
  43. package/src/docker.ts +96 -0
  44. package/src/errors.ts +61 -0
  45. package/src/factory.ts +481 -0
  46. package/src/garage.ts +56 -0
  47. package/src/generator/hash.ts +6 -0
  48. package/src/generator/resolve-refs.ts +207 -0
  49. package/src/grants-builder.ts +100 -0
  50. package/src/grants.ts +260 -0
  51. package/src/home-api.ts +172 -0
  52. package/src/home-cookie.ts +10 -0
  53. package/src/home-registry.ts +120 -0
  54. package/src/home.ts +187 -0
  55. package/src/host-registry.ts +143 -0
  56. package/src/http-header.ts +20 -0
  57. package/src/identifiers.ts +49 -0
  58. package/src/index.ts +66 -0
  59. package/src/info.ts +64 -0
  60. package/src/ini.ts +50 -0
  61. package/src/json.ts +14 -0
  62. package/src/jsx-runtime/index.ts +5 -0
  63. package/src/keys.ts +29 -0
  64. package/src/mysql.ts +56 -0
  65. package/src/pack-build.ts +108 -0
  66. package/src/pack.ts +115 -0
  67. package/src/peer-registry.ts +121 -0
  68. package/src/peer-world.ts +117 -0
  69. package/src/permission-meta.ts +116 -0
  70. package/src/permissions.ts +108 -0
  71. package/src/pkg-root.ts +34 -0
  72. package/src/postgres.ts +79 -0
  73. package/src/preact/index.ts +27 -0
  74. package/src/redis.ts +43 -0
  75. package/src/ref.ts +271 -0
  76. package/src/remote-registry.ts +33 -0
  77. package/src/required-host-kinds.ts +110 -0
  78. package/src/resolve-context.ts +111 -0
  79. package/src/resolve.ts +471 -0
  80. package/src/runtime/docker/compose/compose-schema.ts +118 -0
  81. package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
  82. package/src/runtime/docker/compose/default-hardening.ts +21 -0
  83. package/src/runtime/docker/compose/effects.ts +107 -0
  84. package/src/runtime/docker/compose/index.ts +17 -0
  85. package/src/runtime/docker/compose/labels.ts +39 -0
  86. package/src/runtime/docker/compose/types.ts +78 -0
  87. package/src/runtime/docker/home-proxy-server/index.ts +177 -0
  88. package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
  89. package/src/runtime/docker/ingress/build-config.ts +819 -0
  90. package/src/runtime/docker/ingress/index.ts +16 -0
  91. package/src/runtime/docker/ingress/types.ts +182 -0
  92. package/src/runtime/docker/log-frames.ts +121 -0
  93. package/src/runtime/docker/logs-server/index.ts +366 -0
  94. package/src/runtime/docker/spawn-server/context-key.ts +82 -0
  95. package/src/runtime/docker/spawn-server/index.ts +492 -0
  96. package/src/runtime/docker/spawn-server/protocol.ts +70 -0
  97. package/src/runtime/docker/spawn-server/spec.ts +122 -0
  98. package/src/runtime/docker/stats-server/disks.ts +161 -0
  99. package/src/runtime/docker/stats-server/index.ts +322 -0
  100. package/src/runtime/home-client/index.ts +71 -0
  101. package/src/runtime/log-line.ts +24 -0
  102. package/src/runtime/logs-client/index.ts +175 -0
  103. package/src/runtime/spawn-client/index.ts +93 -0
  104. package/src/runtime/spawn-client/kaniko.ts +79 -0
  105. package/src/runtime/stats-client/index.ts +65 -0
  106. package/src/runtime/stats-sample.ts +95 -0
  107. package/src/safe-json.ts +37 -0
  108. package/src/sandbox.ts +1209 -0
  109. package/src/schema/build-apps.ts +38 -0
  110. package/src/schema/home.ts +217 -0
  111. package/src/schema/host.ts +792 -0
  112. package/src/schema/inputs.ts +26 -0
  113. package/src/schema/nodes.ts +75 -0
  114. package/src/schema/peer.ts +400 -0
  115. package/src/schema/public-domain.ts +116 -0
  116. package/src/schema/remote.ts +74 -0
  117. package/src/security.ts +594 -0
  118. package/src/serialize.ts +21 -0
  119. package/src/service-definition.ts +284 -0
  120. package/src/service-url.ts +64 -0
  121. package/src/service.ts +39 -0
  122. package/src/services/index.ts +5 -0
  123. package/src/shared.ts +65 -0
  124. package/src/speculative-silo.ts +96 -0
  125. package/src/stable-secret.ts +14 -0
  126. package/src/surfaces.ts +264 -0
  127. package/src/tailwind-bin.ts +56 -0
  128. package/src/test/boot.ts +192 -0
  129. package/src/test/compile.ts +501 -0
  130. package/src/test/describe.ts +117 -0
  131. package/src/test/index.ts +11 -0
  132. package/src/test/ingress-fixture.ts +37 -0
  133. package/src/test/types.ts +318 -0
  134. package/src/trusted-headers.ts +79 -0
  135. package/src/types.ts +126 -0
  136. package/src/ui/components/aside.tsx +96 -0
  137. package/src/ui/components/badge.tsx +42 -0
  138. package/src/ui/components/button.tsx +59 -0
  139. package/src/ui/components/card.tsx +106 -0
  140. package/src/ui/components/checkbox.tsx +37 -0
  141. package/src/ui/components/code-block.tsx +29 -0
  142. package/src/ui/components/combobox.tsx +119 -0
  143. package/src/ui/components/config-field.tsx +284 -0
  144. package/src/ui/components/dialog.tsx +137 -0
  145. package/src/ui/components/domain-editor.tsx +317 -0
  146. package/src/ui/components/domain-list.ts +205 -0
  147. package/src/ui/components/dropdown-menu.tsx +73 -0
  148. package/src/ui/components/empty-state.tsx +51 -0
  149. package/src/ui/components/highlight.ts +59 -0
  150. package/src/ui/components/icons.tsx +101 -0
  151. package/src/ui/components/image-posture.tsx +85 -0
  152. package/src/ui/components/input.tsx +28 -0
  153. package/src/ui/components/label.tsx +16 -0
  154. package/src/ui/components/line-chart.tsx +113 -0
  155. package/src/ui/components/page.tsx +39 -0
  156. package/src/ui/components/permission-row.tsx +262 -0
  157. package/src/ui/components/permission-section.tsx +51 -0
  158. package/src/ui/components/pie-chart.tsx +245 -0
  159. package/src/ui/components/schema-block.tsx +60 -0
  160. package/src/ui/components/separator.tsx +42 -0
  161. package/src/ui/components/sheet.tsx +130 -0
  162. package/src/ui/components/sidebar.tsx +650 -0
  163. package/src/ui/components/silo-picker.tsx +510 -0
  164. package/src/ui/components/skeleton.tsx +13 -0
  165. package/src/ui/components/sonner.tsx +33 -0
  166. package/src/ui/components/switch.tsx +58 -0
  167. package/src/ui/components/table-view.ts +219 -0
  168. package/src/ui/components/table.tsx +506 -0
  169. package/src/ui/components/tabs.tsx +59 -0
  170. package/src/ui/components/timeago.tsx +84 -0
  171. package/src/ui/components/tooltip.tsx +56 -0
  172. package/src/ui/components.json +21 -0
  173. package/src/ui/dev/dev-shell.tsx +145 -0
  174. package/src/ui/home/home-grid.tsx +177 -0
  175. package/src/ui/hooks/use-mobile.ts +19 -0
  176. package/src/ui/index.ts +189 -0
  177. package/src/ui/install/adapter.ts +193 -0
  178. package/src/ui/install/deploy-targets-picker.tsx +185 -0
  179. package/src/ui/install/install-app-panel.tsx +912 -0
  180. package/src/ui/install/install-error.tsx +62 -0
  181. package/src/ui/install/perms-inactive.ts +50 -0
  182. package/src/ui/install/required-fields.ts +37 -0
  183. package/src/ui/install/tabs-form.tsx +557 -0
  184. package/src/ui/tokens.css +279 -0
  185. package/src/ui/utils.ts +78 -0
  186. package/src/ui-error.ts +139 -0
  187. package/src/ui-runtime/dev-landing-entry.tsx +258 -0
  188. package/src/ui-runtime/index.ts +83 -0
  189. package/src/ui-runtime/install-entry.tsx +119 -0
  190. package/src/ui-runtime/mount.ts +367 -0
  191. package/src/ui-runtime/runtime.css +56 -0
  192. package/src/ui-surface.ts +451 -0
  193. package/src/valkey.ts +43 -0
  194. package/src/worker/extract-tar.ts +125 -0
  195. package/src/worker/identity.ts +183 -0
  196. package/src/worker/index.ts +16 -0
  197. package/src/worker/public-ip.ts +92 -0
  198. package/src/worker/queue.ts +54 -0
  199. package/src/worker/reconcile.ts +127 -0
  200. package/src/worker/start-worker.ts +227 -0
  201. package/src/worker/ui-proxy.ts +335 -0
  202. package/src/worker/uplink.ts +523 -0
  203. package/src/worker/validate.ts +34 -0
  204. package/src/worker/verify-relay.ts +102 -0
  205. package/src/worker-protocol/app-dirs.ts +94 -0
  206. package/src/worker-protocol/home-proxy-paths.ts +76 -0
  207. package/src/worker-protocol/index.ts +55 -0
  208. package/src/worker-protocol/ingress.ts +166 -0
  209. package/src/worker-protocol/logs-socket-path.ts +50 -0
  210. package/src/worker-protocol/networks.ts +39 -0
  211. package/src/worker-protocol/node-version.ts +70 -0
  212. package/src/worker-protocol/silo-paths.ts +114 -0
  213. package/src/worker-protocol/spawn-paths.ts +165 -0
  214. package/src/worker-protocol/stats-socket-path.ts +40 -0
  215. package/src/worker-protocol/update-files.ts +77 -0
  216. package/src/worker-protocol/uplink/constants.ts +86 -0
  217. package/src/worker-protocol/uplink/errors.ts +61 -0
  218. package/src/worker-protocol/uplink/frame.ts +114 -0
  219. package/src/worker-protocol/uplink/identity.ts +40 -0
  220. package/src/worker-protocol/uplink/index.ts +63 -0
  221. package/src/worker-protocol/uplink/methods.ts +462 -0
  222. package/src/worker-protocol/uplink/mux.ts +646 -0
  223. package/src/worker-protocol/uplink/protocol.ts +11 -0
  224. package/src/worker-protocol/uplink/rpc.ts +309 -0
  225. package/src/worker-protocol/uplink/session.ts +540 -0
  226. package/src/worker-protocol/uplink/test-support.ts +188 -0
  227. package/src/worker-protocol/worker-mount.ts +27 -0
  228. package/src/worker-protocol/worker-state.ts +190 -0
  229. package/src/xml.ts +49 -0
  230. package/src/yaml.ts +15 -0
@@ -0,0 +1,59 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn, type ElementProps } from "../utils.ts";
4
+
5
+ const buttonVariants = cva(
6
+ "cursor-pointer group/button inline-flex shrink-0 items-center justify-center text-sm font-bold whitespace-nowrap transition-all outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "bg-primary text-primary-foreground shadow-clay-primary hover:brightness-110 dark:shadow-clay-dark-primary",
12
+ outline:
13
+ "border-border bg-background shadow-sm hover:shadow hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:shadow-clay-dark-sm dark:hover:shadow-clay-dark",
14
+ secondary:
15
+ "bg-surface-light text-foreground shadow-sm hover:shadow aria-expanded:bg-surface-light aria-expanded:text-foreground dark:bg-secondary dark:shadow-clay-dark-sm dark:hover:shadow-clay-dark",
16
+ ghost:
17
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
18
+ destructive:
19
+ "bg-destructive/10 text-destructive shadow-clay-danger hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default:
24
+ "gap-1.5 rounded-[10px] px-4 py-2 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
25
+ xs: "gap-1 rounded-lg px-2.5 py-1 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: "gap-1 rounded-[10px] px-3 py-1.5 text-[0.8rem] has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3.5",
27
+ lg: "gap-2 rounded-2xl px-6 py-3.5 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
28
+ xl: "gap-2 rounded-xl px-9 py-4 text-[15px] has-data-[icon=inline-end]:pr-6 has-data-[icon=inline-start]:pl-6",
29
+ icon: "size-9 rounded-[10px]",
30
+ "icon-xs": "size-6 rounded-lg [&_svg:not([class*='size-'])]:size-3",
31
+ "icon-sm": "size-7 rounded-[10px]",
32
+ "icon-lg": "size-11 rounded-xl",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ size: "default",
38
+ },
39
+ },
40
+ );
41
+
42
+ function Button({
43
+ className,
44
+ variant = "default",
45
+ size = "default",
46
+ type = "button",
47
+ ...props
48
+ }: ElementProps<"button"> & VariantProps<typeof buttonVariants>) {
49
+ return (
50
+ <button
51
+ data-slot="button"
52
+ type={type}
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ export { Button, buttonVariants };
@@ -0,0 +1,106 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn, type ElementProps } from "../utils.ts";
4
+
5
+ const cardVariants = cva(
6
+ "group/card flex flex-col gap-4 overflow-hidden text-card-foreground has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "rounded-2xl bg-card px-2 py-4 text-sm shadow *:[img:first-child]:rounded-t-2xl *:[img:last-child]:rounded-b-2xl data-[size=sm]:py-3",
12
+ glass: "rounded-3xl bg-card/35 backdrop-blur-xl shadow-xl",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ },
19
+ );
20
+
21
+ type CardProps = ElementProps<"div"> &
22
+ VariantProps<typeof cardVariants> & {
23
+ size?: "default" | "sm";
24
+ };
25
+
26
+ function Card({ className, size = "default", variant, ...props }: CardProps) {
27
+ return (
28
+ <div
29
+ data-slot="card"
30
+ data-size={size}
31
+ className={cn(cardVariants({ variant, className }))}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function CardHeader({ className, ...props }: ElementProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="card-header"
41
+ className={cn(
42
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-2xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function CardTitle({ className, ...props }: ElementProps<"div">) {
51
+ return (
52
+ <div
53
+ data-slot="card-title"
54
+ className={cn(
55
+ "text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ function CardDescription({ className, ...props }: ElementProps<"div">) {
64
+ return (
65
+ <div
66
+ data-slot="card-description"
67
+ className={cn("text-sm text-muted-foreground", className)}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ function CardAction({ className, ...props }: ElementProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="card-action"
77
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function CardContent({ className, ...props }: ElementProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="card-content"
87
+ className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function CardFooter({ className, ...props }: ElementProps<"div">) {
94
+ return (
95
+ <div
96
+ data-slot="card-footer"
97
+ className={cn(
98
+ "flex items-center rounded-b-2xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
99
+ className,
100
+ )}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
@@ -0,0 +1,37 @@
1
+ import type { ComponentProps } from "preact";
2
+ import { Checkbox as CheckboxPrimitive } from "@ark-ui/react/checkbox";
3
+
4
+ import { cn } from "../utils.ts";
5
+ import { CheckBoldIcon } from "./icons.tsx";
6
+
7
+ function Checkbox({
8
+ className,
9
+ id,
10
+ ...props
11
+ }: ComponentProps<typeof CheckboxPrimitive.Root> & { id?: string }) {
12
+ return (
13
+ <CheckboxPrimitive.Root
14
+ ids={id ? { hiddenInput: id } : undefined}
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ // The root is a <label>; focus lands on the hidden input, so the ring
18
+ // has to key off the `data-focus-visible` Zag mirrors onto every part.
19
+ "cursor-pointer peer relative flex size-4 shrink-0 items-center justify-center rounded-[5px] border border-border bg-card shadow-sm transition-all outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 data-[focus-visible]:ring-3 data-[focus-visible]:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:shadow-clay-dark-sm dark:aria-invalid:ring-destructive/40 data-[state=checked]:bg-primary data-[state=checked]:shadow-clay-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary dark:data-[state=checked]:shadow-clay-dark-primary",
20
+ className,
21
+ )}
22
+ {...props}
23
+ >
24
+ <CheckboxPrimitive.Control
25
+ data-slot="checkbox-indicator"
26
+ className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
27
+ >
28
+ <CheckboxPrimitive.Indicator>
29
+ <CheckBoldIcon />
30
+ </CheckboxPrimitive.Indicator>
31
+ </CheckboxPrimitive.Control>
32
+ <CheckboxPrimitive.HiddenInput />
33
+ </CheckboxPrimitive.Root>
34
+ );
35
+ }
36
+
37
+ export { Checkbox };
@@ -0,0 +1,29 @@
1
+ // Pure presentational component: renders pre-highlighted HTML. The Shiki-backed
2
+ // highlightCode() lives in ./highlight.ts so this stays client-island-safe (no
3
+ // Shiki/WASM in the bundle). See that file for why.
4
+ export function CodeBlock({
5
+ html,
6
+ filename,
7
+ className,
8
+ }: {
9
+ html: string;
10
+ filename?: string;
11
+ className?: string;
12
+ }) {
13
+ return (
14
+ <div className="bg-text-dark rounded-2xl shadow-clay-dark overflow-hidden">
15
+ {filename && (
16
+ <div className="flex items-center gap-2 px-4 py-2.5 border-b border-white/10">
17
+ <div className="w-2.5 h-2.5 rounded-full bg-red-400/60" />
18
+ <div className="w-2.5 h-2.5 rounded-full bg-yellow-400/60" />
19
+ <div className="w-2.5 h-2.5 rounded-full bg-green-400/60" />
20
+ <span className="ml-2 text-xs text-white/30 font-mono">{filename}</span>
21
+ </div>
22
+ )}
23
+ <pre
24
+ className={`font-mono text-xs leading-relaxed p-5 m-0 overflow-x-auto ${className ?? ""}`}
25
+ dangerouslySetInnerHTML={{ __html: html }}
26
+ />
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ Combobox as ComboboxPrimitive,
3
+ type ComboboxInputValueChangeDetails,
4
+ type ComboboxValueChangeDetails,
5
+ useListCollection,
6
+ } from "@ark-ui/react/combobox";
7
+ import { useFilter } from "@ark-ui/react/locale";
8
+ import { useEffect } from "preact/hooks";
9
+
10
+ import { cn } from "../utils.ts";
11
+ import { CheckIcon, MagnifyingGlassIcon } from "./icons.tsx";
12
+
13
+ // One selectable option. `label` is what shows + what typing filters against;
14
+ // `value` is what `onValueChange` reports.
15
+ interface ComboboxOption {
16
+ value: string;
17
+ label: string;
18
+ }
19
+
20
+ interface ComboboxProps {
21
+ items: ComboboxOption[];
22
+ value?: string;
23
+ onValueChange?: (value: string) => void;
24
+ placeholder?: string;
25
+ emptyText?: string;
26
+ disabled?: boolean;
27
+ className?: string;
28
+ }
29
+
30
+ // A searchable single-select on Ark UI's Combobox. Ark drives filtering through
31
+ // a collection, so this owns an input-value filter and re-renders the list from
32
+ // it. Self-contained: pass `items` and it renders the search input, the filtered
33
+ // list, and a check on the selected row.
34
+ export function Combobox({
35
+ items,
36
+ value,
37
+ onValueChange,
38
+ placeholder = "Search…",
39
+ emptyText = "No results.",
40
+ disabled,
41
+ className,
42
+ }: ComboboxProps) {
43
+ const { contains } = useFilter({ sensitivity: "base" });
44
+ const { collection, filter, set } = useListCollection({
45
+ initialItems: items,
46
+ filter: contains,
47
+ itemToValue: (item) => item.value,
48
+ itemToString: (item) => item.label,
49
+ });
50
+
51
+ // `useListCollection` seeds `initialItems` once; reseed when the item set changes
52
+ // (an async loader can deliver a different list after mount). Key on the values so
53
+ // this fires on real changes, not on every render — the parent passes a fresh array
54
+ // each time, so depending on the array reference would loop.
55
+ const itemsKey = items.map((item) => item.value).join(" ");
56
+ useEffect(() => {
57
+ set(items);
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [itemsKey]);
60
+
61
+ return (
62
+ <ComboboxPrimitive.Root
63
+ data-slot="combobox"
64
+ collection={collection}
65
+ value={value ? [value] : []}
66
+ onValueChange={(details: ComboboxValueChangeDetails) =>
67
+ onValueChange?.(details.value[0] ?? "")
68
+ }
69
+ onInputValueChange={(details: ComboboxInputValueChangeDetails) => filter(details.inputValue)}
70
+ disabled={disabled}
71
+ openOnClick
72
+ className={cn("relative w-full", className)}
73
+ >
74
+ <ComboboxPrimitive.Control className="relative">
75
+ <MagnifyingGlassIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 opacity-50" />
76
+ <ComboboxPrimitive.Input
77
+ data-slot="combobox-input"
78
+ placeholder={placeholder}
79
+ className={cn(
80
+ "h-9 w-full rounded-md border border-input bg-background pl-8 pr-3 text-sm shadow-sm outline-none",
81
+ "placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring",
82
+ "disabled:cursor-not-allowed disabled:opacity-50",
83
+ )}
84
+ />
85
+ </ComboboxPrimitive.Control>
86
+ <ComboboxPrimitive.Positioner className="z-50">
87
+ <ComboboxPrimitive.Content
88
+ data-slot="combobox-content"
89
+ className={cn(
90
+ "z-50 max-h-72 w-[var(--reference-width)] overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground shadow-md outline-none",
91
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
92
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
93
+ )}
94
+ >
95
+ {collection.items.map((item) => (
96
+ <ComboboxPrimitive.Item
97
+ key={item.value}
98
+ item={item}
99
+ data-slot="combobox-item"
100
+ className={cn(
101
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none",
102
+ "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
103
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
104
+ )}
105
+ >
106
+ <ComboboxPrimitive.ItemText>{item.label}</ComboboxPrimitive.ItemText>
107
+ <ComboboxPrimitive.ItemIndicator className="ml-auto">
108
+ <CheckIcon className="size-4" />
109
+ </ComboboxPrimitive.ItemIndicator>
110
+ </ComboboxPrimitive.Item>
111
+ ))}
112
+ {collection.items.length === 0 && (
113
+ <div class="py-6 text-center text-sm text-muted-foreground">{emptyText}</div>
114
+ )}
115
+ </ComboboxPrimitive.Content>
116
+ </ComboboxPrimitive.Positioner>
117
+ </ComboboxPrimitive.Root>
118
+ );
119
+ }
@@ -0,0 +1,284 @@
1
+ import type { ComponentChildren } from "preact";
2
+ import { useState } from "preact/hooks";
3
+ import { EyeIcon, EyeSlashIcon, FadersIcon, LockIcon, ListChecksIcon } from "./icons.tsx";
4
+ import { Input } from "./input.tsx";
5
+ import { Switch } from "./switch.tsx";
6
+
7
+ export type ConfigFieldKind = "text" | "secret" | "number" | "boolean" | "enum";
8
+
9
+ const ICONS: Record<ConfigFieldKind, ComponentChildren> = {
10
+ secret: <LockIcon className="w-[22px] h-[22px]" />,
11
+ enum: <ListChecksIcon className="w-[22px] h-[22px]" />,
12
+ text: <FadersIcon className="w-[22px] h-[22px]" />,
13
+ number: <FadersIcon className="w-[22px] h-[22px]" />,
14
+ boolean: <FadersIcon className="w-[22px] h-[22px]" />,
15
+ };
16
+
17
+ interface ConfigFieldProps {
18
+ kind: ConfigFieldKind;
19
+ label: string;
20
+ description?: string;
21
+ placeholder?: string;
22
+ /** Omit to hide the required/optional badge. */
23
+ required?: boolean;
24
+ /** Choices for `kind="enum"`. */
25
+ values?: string[];
26
+ /** Bounds for `kind="number"`, forwarded to the `<input>`. */
27
+ min?: number;
28
+ max?: number;
29
+ step?: number;
30
+ /** Controlled value. Pair with `onChange`. */
31
+ value?: string | number | boolean;
32
+ onChange?: (value: unknown) => void;
33
+ /** Uncontrolled seed (also drives the input's initial state). */
34
+ defaultValue?: string | number | boolean;
35
+ disabled?: boolean;
36
+ /**
37
+ * For `kind="secret"`: render the secret's display mask as the placeholder
38
+ * and a "leave blank to keep current value" hint instead of the regular
39
+ * placeholder.
40
+ */
41
+ redacted?: boolean;
42
+ /**
43
+ * For a redacted secret: the non-reversible display mask (e.g.
44
+ * `pk_live_••••`) computed at wrap time. Shown as the placeholder so the
45
+ * operator can tell which key is set; never the input's value, so it can't
46
+ * be submitted.
47
+ */
48
+ secretMask?: string;
49
+ id?: string;
50
+ /**
51
+ * Forwarded to the underlying `<input>`. Useful for docs-style previews
52
+ * that want to suppress browser form restoration / autofill so a prefill
53
+ * value isn't replaced by the user's last typed value on reload.
54
+ */
55
+ autoComplete?: string;
56
+ }
57
+
58
+ function ConfigField({
59
+ kind,
60
+ label,
61
+ description,
62
+ placeholder,
63
+ required,
64
+ values,
65
+ min,
66
+ max,
67
+ step,
68
+ value,
69
+ onChange,
70
+ defaultValue,
71
+ disabled,
72
+ redacted,
73
+ secretMask,
74
+ id: idProp,
75
+ autoComplete,
76
+ }: ConfigFieldProps) {
77
+ const id = idProp ?? `cfg-${label.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
78
+
79
+ const [internal, setInternal] = useState<unknown>(defaultValue);
80
+ // Controlled mode is determined by the caller passing `onChange`. A
81
+ // controlled component can legitimately have `value === undefined`
82
+ // (a required-no-prefill input the user hasn't filled in yet), so gating
83
+ // on `value !== undefined` would silently flip into uncontrolled mode and
84
+ // drop every keystroke into local state instead of the parent's signal.
85
+ const isControlled = onChange !== undefined;
86
+ const effective = isControlled ? value : internal;
87
+ const emit = (v: unknown) => {
88
+ if (isControlled) onChange(v);
89
+ else setInternal(v);
90
+ };
91
+
92
+ return (
93
+ <div className="bg-card rounded-2xl shadow p-3.5 px-4 flex items-start gap-3">
94
+ <span className="text-trust">{ICONS[kind]}</span>
95
+ <div className="flex-1 min-w-0">
96
+ <div className="flex items-center gap-2">
97
+ <label htmlFor={id} className="text-sm font-bold text-foreground cursor-pointer">
98
+ {label}
99
+ </label>
100
+ {required !== undefined && (
101
+ <span className="text-[10px] font-bold px-[7px] py-0.5 rounded uppercase tracking-wide text-muted-foreground bg-surface-light">
102
+ {required ? "required" : "optional"}
103
+ </span>
104
+ )}
105
+ </div>
106
+ {description && (
107
+ <p className="text-xs mt-1 leading-snug text-muted-foreground">{description}</p>
108
+ )}
109
+ {kind !== "boolean" && (
110
+ <div className="mt-2">
111
+ <ConfigFieldInput
112
+ id={id}
113
+ kind={kind}
114
+ label={label}
115
+ placeholder={placeholder}
116
+ values={values}
117
+ min={min}
118
+ max={max}
119
+ step={step}
120
+ value={effective}
121
+ onChange={emit}
122
+ disabled={disabled === true}
123
+ redacted={redacted === true}
124
+ secretMask={secretMask}
125
+ autoComplete={autoComplete}
126
+ />
127
+ </div>
128
+ )}
129
+ {redacted && (
130
+ <p className="text-xs text-muted-foreground leading-snug italic mt-1.5">
131
+ Leave blank to keep current value.
132
+ </p>
133
+ )}
134
+ </div>
135
+ {kind === "boolean" && (
136
+ <Switch
137
+ id={id}
138
+ checked={effective === true}
139
+ onCheckedChange={(d: { checked: boolean }) => emit(d.checked === true)}
140
+ disabled={disabled === true}
141
+ label={label}
142
+ labelClassName="sr-only"
143
+ />
144
+ )}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ interface ConfigFieldInputProps {
150
+ id: string;
151
+ /** A boolean renders as a Switch beside the card, not as an input. */
152
+ kind: Exclude<ConfigFieldKind, "boolean">;
153
+ /** Only for the secret reveal button's aria-label — the visible label is the
154
+ * `<label htmlFor>` above, and several secrets on one form would otherwise
155
+ * all announce as an identical "Show value". */
156
+ label: string;
157
+ placeholder?: string;
158
+ values?: string[];
159
+ min?: number;
160
+ max?: number;
161
+ step?: number;
162
+ value: unknown;
163
+ onChange: (value: unknown) => void;
164
+ disabled: boolean;
165
+ redacted: boolean;
166
+ secretMask?: string;
167
+ autoComplete?: string;
168
+ }
169
+
170
+ function ConfigFieldInput({
171
+ id,
172
+ kind,
173
+ label,
174
+ placeholder,
175
+ values,
176
+ min,
177
+ max,
178
+ step,
179
+ value,
180
+ onChange,
181
+ disabled,
182
+ redacted,
183
+ secretMask,
184
+ autoComplete,
185
+ }: ConfigFieldInputProps) {
186
+ // Reveal state for `kind="secret"`. Declared unconditionally (hooks can't
187
+ // sit behind the kind branches below) and per-field, so revealing one
188
+ // secret never unmasks its neighbours.
189
+ const [revealed, setRevealed] = useState(false);
190
+
191
+ if (kind === "enum") {
192
+ return (
193
+ <select
194
+ id={id}
195
+ className="h-8 w-full rounded-2xl border border-border bg-secondary px-2.5 text-sm shadow-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50"
196
+ value={typeof value === "string" ? value : ""}
197
+ disabled={disabled}
198
+ onChange={(e) => onChange((e.target as HTMLSelectElement).value)}
199
+ >
200
+ <option value="" disabled>
201
+ Select…
202
+ </option>
203
+ {(values ?? []).map((v) => (
204
+ <option key={v} value={v}>
205
+ {v}
206
+ </option>
207
+ ))}
208
+ </select>
209
+ );
210
+ }
211
+
212
+ if (kind === "number") {
213
+ return (
214
+ <Input
215
+ id={id}
216
+ type="number"
217
+ value={typeof value === "number" ? String(value) : ((value as string) ?? "")}
218
+ placeholder={placeholder}
219
+ min={min}
220
+ max={max}
221
+ step={step}
222
+ disabled={disabled}
223
+ autoComplete={autoComplete}
224
+ onInput={(e) => {
225
+ const raw = (e.target as HTMLInputElement).value;
226
+ onChange(raw === "" ? undefined : Number(raw));
227
+ }}
228
+ />
229
+ );
230
+ }
231
+
232
+ if (kind === "secret") {
233
+ return (
234
+ <div className="relative">
235
+ <Input
236
+ id={id}
237
+ type={revealed ? "text" : "password"}
238
+ autoComplete={autoComplete ?? "new-password"}
239
+ value={typeof value === "string" ? value : ""}
240
+ placeholder={redacted ? secretMask : placeholder}
241
+ disabled={disabled}
242
+ // Room for the reveal button, which overlays the input's right edge.
243
+ className="pr-9"
244
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
245
+ />
246
+ {/* An API key typed blind is a support ticket: the operator pastes or
247
+ types it once, cannot proof-read it, and the app fails to
248
+ authenticate with no way to tell a typo from a wrong key. Masking
249
+ by default still matters (screen shares, over-the-shoulder), so
250
+ this is opt-in per field rather than a global preference.
251
+ `type="button"` is load-bearing — this sits inside the install and
252
+ edit forms, and the default `submit` would deploy the app. */}
253
+ <button
254
+ type="button"
255
+ onClick={() => setRevealed(!revealed)}
256
+ disabled={disabled}
257
+ aria-label={`${revealed ? "Hide" : "Show"} ${label}`}
258
+ aria-pressed={revealed}
259
+ className="absolute right-0 top-0 h-8 w-9 grid place-items-center rounded-r-2xl text-muted-foreground hover:text-foreground disabled:pointer-events-none disabled:opacity-50 cursor-pointer"
260
+ >
261
+ {revealed ? (
262
+ <EyeSlashIcon className="w-[18px] h-[18px]" />
263
+ ) : (
264
+ <EyeIcon className="w-[18px] h-[18px]" />
265
+ )}
266
+ </button>
267
+ </div>
268
+ );
269
+ }
270
+
271
+ return (
272
+ <Input
273
+ id={id}
274
+ type="text"
275
+ value={typeof value === "string" ? value : ""}
276
+ placeholder={placeholder}
277
+ disabled={disabled}
278
+ autoComplete={autoComplete}
279
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
280
+ />
281
+ );
282
+ }
283
+
284
+ export { ConfigField };