@astrale-os/adapter-cloudflare 0.4.4 → 0.4.5

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 (44) hide show
  1. package/package.json +4 -3
  2. package/template/CLAUDE.md +8 -21
  3. package/template/README.md +30 -107
  4. package/template/client/__tests__/app.test.tsx +6 -57
  5. package/template/client/components.json +25 -0
  6. package/template/client/index.html +1 -1
  7. package/template/client/package.json +10 -2
  8. package/template/client/src/app.tsx +42 -33
  9. package/template/client/src/components/ui/cn.ts +6 -0
  10. package/template/client/src/styles.css +71 -502
  11. package/template/client/vite.config.ts +2 -18
  12. package/template/client/vitest.config.ts +0 -6
  13. package/template/core/README.md +5 -21
  14. package/template/deps.ts +2 -20
  15. package/template/domain.ts +0 -26
  16. package/template/env.ts +1 -17
  17. package/template/icons.ts +4 -25
  18. package/template/integrations/README.md +5 -20
  19. package/template/package.json +7 -5
  20. package/template/pnpm-workspace.yaml +2 -0
  21. package/template/runtime/index.ts +0 -27
  22. package/template/schema/index.ts +0 -20
  23. package/template/simulation/README.md +12 -0
  24. package/template/tsconfig.json +2 -3
  25. package/template/vitest.config.ts +7 -0
  26. package/template/.domain-studio/comments.json +0 -4
  27. package/template/client/README.md +0 -104
  28. package/template/client/__tests__/harness.ts +0 -230
  29. package/template/client/__tests__/shell.test.ts +0 -46
  30. package/template/client/src/shell/client.ts +0 -67
  31. package/template/client/src/shell/index.ts +0 -21
  32. package/template/client/src/shell/invoke.ts +0 -35
  33. package/template/client/src/shell/transformers.ts +0 -75
  34. package/template/client/src/shell/use-async.ts +0 -59
  35. package/template/client/src/shell/use-capability.ts +0 -59
  36. package/template/client/src/shell/use-node.ts +0 -81
  37. package/template/client/src/shell/use-shell.ts +0 -91
  38. package/template/client/src/shell/view-router.tsx +0 -97
  39. package/template/client/src/ui/format.ts +0 -24
  40. package/template/client/src/ui/index.ts +0 -10
  41. package/template/client/src/ui/surface.tsx +0 -56
  42. package/template/client/src/ui/value.tsx +0 -32
  43. package/template/functions/index.ts +0 -22
  44. package/template/views/index.ts +0 -19
@@ -1,67 +0,0 @@
1
- /**
2
- * Graph/prop helpers for the nodes the kernel returns — reading fully-qualified
3
- * props and short class names off a `KernelNode`. Pure shaping: the kernel
4
- * TRANSPORT now lives in `@astrale-os/shell` (`shell.kernel` → `session.call`),
5
- * so this file no longer speaks the wire.
6
- */
7
-
8
- /**
9
- * Prop key constants — the graph stores props with fully-qualified keys
10
- * (`<domain>:<member>.property.<name>`). The kernel `Named.name` key is fixed
11
- * and known; domain props (`url`, `status`) are qualified by the (build-time
12
- * unknown) domain origin — read those by suffix with `readPropBySuffix`.
13
- */
14
- export const PROP = {
15
- named: {
16
- name: 'kernel.astrale.ai:interface.Named.property.name',
17
- },
18
- } as const
19
-
20
- export type KernelNode = {
21
- id: string
22
- path: string
23
- class: string | { raw?: string }
24
- props: Record<string, unknown>
25
- }
26
-
27
- export function readProp(props: Record<string, unknown>, key: string): string | undefined {
28
- const v = props[key]
29
- return typeof v === 'string' ? v : undefined
30
- }
31
-
32
- /**
33
- * Read a string prop by key suffix — for domain-qualified props whose full key
34
- * embeds the (build-time-unknown) domain origin. e.g.
35
- * `readPropBySuffix(props, '.property.url')`.
36
- */
37
- export function readPropBySuffix(
38
- props: Record<string, unknown>,
39
- suffix: string,
40
- ): string | undefined {
41
- for (const [k, v] of Object.entries(props)) {
42
- if (k.endsWith(suffix) && typeof v === 'string') return v
43
- }
44
- return undefined
45
- }
46
-
47
- /** Short class name from a `class.raw` path `/:<domain>:class.<Name>` → `<Name>`. */
48
- export function classShortName(node: KernelNode): string {
49
- const raw = (typeof node.class === 'string' ? node.class : node.class?.raw) ?? ''
50
- const last = raw.split(':').pop() ?? ''
51
- const dot = last.indexOf('.')
52
- return dot >= 0 ? last.slice(dot + 1) : last
53
- }
54
-
55
- /**
56
- * Operator-facing message for a failed kernel call. Errors from the shell's
57
- * kernel client carry a numeric `code` (e.g. `NotFoundError` → `code 3002`) with
58
- * the raw server text in `message`; surface them as `<code>: <message>` so the
59
- * code stays visible. Plain errors fall back to their message.
60
- */
61
- export function errorMessage(err: unknown): string {
62
- if (err instanceof Error) {
63
- const code = (err as { code?: unknown }).code
64
- return typeof code === 'number' ? `${code}: ${err.message}` : err.message
65
- }
66
- return String(err)
67
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * The kernel boundary — everything for talking to the kernel and the GUI
3
- * shell, plus the common read/write primitives features build their
4
- * `.query`/`.mutation` hooks on. Import from the barrel: `@/shell`.
5
- */
6
- export { classShortName, errorMessage, PROP, readProp, readPropBySuffix } from './client'
7
- export type { KernelNode } from './client'
8
- export { callMethod, invokeNode, nodeAddr } from './invoke'
9
- export type { KernelClient } from '@astrale-os/shell'
10
- export { useNode } from './use-node'
11
- export type { NodeState } from './use-node'
12
- export { useShell } from './use-shell'
13
- export type { ShellState, ShellStatus } from './use-shell'
14
- export { useAsync } from './use-async'
15
- export type { AsyncResource, AsyncState } from './use-async'
16
- export { useCapability } from './use-capability'
17
- export type { Capability, Phase } from './use-capability'
18
- export { asNodeArray, linkTargets, qualifiedProp, qualifiedString } from './transformers'
19
- export type { GraphEdge } from './transformers'
20
- export { resolveView, ViewFrame } from './view-router'
21
- export type { ViewComponent, ViewRoutes } from './view-router'
@@ -1,35 +0,0 @@
1
- /**
2
- * Kernel invocation primitives — the single place a view reaches the kernel,
3
- * over the shell's proper `KernelClient` (`session.call`, wired by the sandboxed
4
- * handshake: fresh delegation token per call, redirects followed). `callMethod`
5
- * is the generic method-ref call; `invokeNode` is instance dispatch by node id
6
- * (`@<id>::<method>` — the shell hands views node IDs, not paths); `nodeAddr`
7
- * picks the stable address for a node.
8
- */
9
- import type { KernelClient } from '@astrale-os/shell'
10
-
11
- import type { KernelNode } from './client'
12
-
13
- /** One kernel call through the live session (proper client: fresh token, redirects). */
14
- export function callMethod(
15
- session: KernelClient,
16
- method: string,
17
- params: Record<string, unknown> = {},
18
- ): Promise<unknown> {
19
- return session.call(method, params)
20
- }
21
-
22
- /** Instance-method dispatch by node id. */
23
- export function invokeNode(
24
- session: KernelClient,
25
- nodeId: string,
26
- method: string,
27
- params: Record<string, unknown> = {},
28
- ): Promise<unknown> {
29
- return callMethod(session, `@${nodeId}::${method}`, params)
30
- }
31
-
32
- /** Stable instance address: tree path when known, else `@id`. */
33
- export function nodeAddr(node: KernelNode): string {
34
- return node.path && node.path.startsWith('/') ? node.path : `@${node.id}`
35
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Graph-shape helpers at the kernel boundary — turn `shell.kernel.graph` results
3
- * (`graph.node` / `graph.children` / `graph.links`, backed by `function.get`)
4
- * into the shapes features consume. Domain-neutral: per-class node→record mappers
5
- * live in each feature's `*.mappers.ts` and build on `qualifiedString` here.
6
- *
7
- * The graph sugar already returns typed records (`{ nodes }`, `{ edges }`) — no
8
- * envelope-sniffing needed — so these are thin projections over those shapes.
9
- *
10
- * Props are stored under QUALIFIED keys
11
- * (`monitors.astrale.ai:class.Monitor.property.url`). The client must not import
12
- * server schema, so values resolve by KEY SUFFIX (`.property.<name>`),
13
- * preferring domain-qualified keys over kernel ones
14
- * (`kernel.astrale.ai:interface.Named.property.name` would otherwise shadow
15
- * `Monitor.property.name` — both end in `.property.name`).
16
- */
17
- import type { KernelNode } from './client'
18
-
19
- /** One incident edge as `shell.kernel.graph.links` returns it (a `GraphEdgeWire`). */
20
- export type GraphEdge = {
21
- class: string
22
- source: string
23
- target: string
24
- slug?: string | null
25
- props?: Record<string, unknown>
26
- }
27
-
28
- /** Resolve a prop by `.property.<name>` suffix; domain keys win over kernel ones. */
29
- export function qualifiedProp(props: Record<string, unknown>, name: string): unknown {
30
- const suffix = `.property.${name}`
31
- let kernelFallback: unknown
32
- for (const [key, value] of Object.entries(props)) {
33
- if (!key.endsWith(suffix)) continue
34
- if (key.startsWith('kernel.astrale.ai:')) {
35
- if (kernelFallback === undefined) kernelFallback = value
36
- } else {
37
- return value
38
- }
39
- }
40
- return kernelFallback
41
- }
42
-
43
- export function qualifiedString(props: Record<string, unknown>, name: string): string | undefined {
44
- const v = qualifiedProp(props, name)
45
- return typeof v === 'string' && v !== '' ? v : undefined
46
- }
47
-
48
- /**
49
- * Targets of `graph.links` edges whose edge class ends with `classTail`
50
- * (e.g. `'monitor_on_host'`). Accepts the `graph.links` result (`{ edges }`) or a
51
- * bare `GraphEdge[]`.
52
- */
53
- export function linkTargets(
54
- edges: GraphEdge[] | { edges: GraphEdge[] } | null | undefined,
55
- classTail: string,
56
- ): string[] {
57
- const list: GraphEdge[] = Array.isArray(edges) ? edges : (edges?.edges ?? [])
58
- const out: string[] = []
59
- for (const edge of list) {
60
- if (typeof edge.class !== 'string' || !edge.class.endsWith(classTail)) continue
61
- if (typeof edge.target === 'string') out.push(edge.target)
62
- }
63
- return out
64
- }
65
-
66
- /**
67
- * The nodes of a `graph.children` / `graph.tree` result. Accepts the sugar result
68
- * (`{ nodes }`) or a bare `KernelNode[]`.
69
- */
70
- export function asNodeArray(
71
- result: KernelNode[] | { nodes: KernelNode[] } | null | undefined,
72
- ): KernelNode[] {
73
- if (Array.isArray(result)) return result
74
- return result?.nodes ?? []
75
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * Tiny async-resource hook — load on mount / when `deps` change, expose a
3
- * `reload()` for refresh buttons and post-mutation refetches. Mirrors the
4
- * cancellation discipline of `use-node.ts` (stale resolutions are dropped).
5
- */
6
- import { useCallback, useEffect, useState } from 'react'
7
-
8
- import { errorMessage } from './client'
9
-
10
- export type AsyncState<T> =
11
- | { status: 'loading' }
12
- | { status: 'ok'; data: T }
13
- | { status: 'error'; message: string }
14
-
15
- export type AsyncResource<T> = {
16
- state: AsyncState<T>
17
- /** Refetch. Keeps showing the previous data while the reload is in flight. */
18
- reload: () => void
19
- /** True while a reload is in flight on top of an `ok` state. */
20
- reloading: boolean
21
- }
22
-
23
- export function useAsync<T>(fn: () => Promise<T>, deps: unknown[]): AsyncResource<T> {
24
- const [state, setState] = useState<AsyncState<T>>({ status: 'loading' })
25
- const [reloading, setReloading] = useState(false)
26
- const [epoch, setEpoch] = useState(0)
27
-
28
- useEffect(() => {
29
- let cancelled = false
30
- setState((prev) => {
31
- if (prev.status === 'ok') {
32
- setReloading(true)
33
- return prev // keep stale data visible during a reload
34
- }
35
- return { status: 'loading' }
36
- })
37
- fn()
38
- .then((data) => {
39
- if (cancelled) return
40
- setReloading(false)
41
- setState({ status: 'ok', data })
42
- })
43
- .catch((err: unknown) => {
44
- if (cancelled) return
45
- setReloading(false)
46
- setState({ status: 'error', message: errorMessage(err) })
47
- })
48
- return () => {
49
- cancelled = true
50
- }
51
- // eslint-disable-next-line react-hooks/exhaustive-deps -- deps are the caller's cache key
52
- }, [...deps, epoch])
53
-
54
- const reload = useCallback(() => {
55
- setReloading(true)
56
- setEpoch((e) => e + 1)
57
- }, [])
58
- return { state, reload, reloading }
59
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * `useCapability` — the convention's WRITE primitive: a node method made
3
- * interactive, wrapped in one uniform lifecycle (idle → running → done/failed).
4
- * Every actuator (a form submit, a danger button) shares this machine instead
5
- * of re-implementing busy/error flags by hand.
6
- *
7
- * const check = useCapability(() => invokeNode(session, nodeId, 'check', {}))
8
- * …
9
- * if (await check.run()) reload()
10
- *
11
- * (This is the single seam where permission-gating would later land — a
12
- * `permitted` flag computed from the node's affordances — without touching any
13
- * presentational component.)
14
- */
15
- import { useCallback, useState } from 'react'
16
-
17
- import { errorMessage } from './client'
18
-
19
- export type Phase = 'idle' | 'running' | 'done' | 'failed'
20
-
21
- export interface Capability<P = void> {
22
- /** Phase of the most recent invocation. */
23
- phase: Phase
24
- /** Failure message when `phase === 'failed'`, else null. */
25
- error: string | null
26
- /** Invoke; resolves true on success, false on failure (error then set). */
27
- run: [P] extends [void] ? () => Promise<boolean> : (params: P) => Promise<boolean>
28
- /** Return to idle, clearing any error. */
29
- reset: () => void
30
- }
31
-
32
- export function useCapability<P = void>(perform: (params: P) => Promise<unknown>): Capability<P> {
33
- const [phase, setPhase] = useState<Phase>('idle')
34
- const [error, setError] = useState<string | null>(null)
35
-
36
- const run = useCallback(
37
- async (params: P): Promise<boolean> => {
38
- setPhase('running')
39
- setError(null)
40
- try {
41
- await perform(params)
42
- setPhase('done')
43
- return true
44
- } catch (err) {
45
- setPhase('failed')
46
- setError(errorMessage(err))
47
- return false
48
- }
49
- },
50
- [perform],
51
- )
52
-
53
- const reset = useCallback(() => {
54
- setPhase('idle')
55
- setError(null)
56
- }, [])
57
-
58
- return { phase, error, run, reset } as Capability<P>
59
- }
@@ -1,81 +0,0 @@
1
- import type { KernelClient } from '@astrale-os/shell'
2
-
3
- import { useCallback, useEffect, useState } from 'react'
4
-
5
- import { errorMessage, type KernelNode } from './client'
6
-
7
- /**
8
- * The node shape `shell.kernel.get()` resolves — a graph `Node` ({@link
9
- * KernelClient.get} returns `Node | null`). Derived from the client's own
10
- * return type so this file never imports server schema; mapped onto the plain
11
- * {@link KernelNode} the view helpers consume by `toKernelNode`.
12
- */
13
- type GraphNode = NonNullable<Awaited<ReturnType<KernelClient['get']>>>
14
-
15
- /**
16
- * Project a graph `Node` onto the plain, schema-free {@link KernelNode}. `path`
17
- * and `class` arrive as typed `Path` instances over the wire — `String()` yields
18
- * their raw form (and is the identity on a plain string), so this stays correct
19
- * whether or not the client hydrates them.
20
- */
21
- function toKernelNode(node: GraphNode): KernelNode {
22
- return {
23
- id: String(node.id),
24
- path: String(node.path),
25
- class: String(node.class),
26
- props: { ...node.props },
27
- }
28
- }
29
-
30
- export type NodeState =
31
- | { status: 'idle' }
32
- | { status: 'loading' }
33
- | { status: 'ok'; node: KernelNode }
34
- | { status: 'error'; message: string }
35
-
36
- /**
37
- * Fetch a node by id through `shell.kernel.get('@<id>')`.
38
- * `null` means missing or masked; `reload()` refetches the current id.
39
- */
40
- export function useNode(
41
- session: KernelClient,
42
- nodeId: string | undefined,
43
- ): NodeState & { reload(): void } {
44
- const [state, setState] = useState<NodeState>({ status: 'idle' })
45
- const [nonce, setNonce] = useState(0)
46
-
47
- const reload = useCallback(() => {
48
- setNonce((n) => n + 1)
49
- }, [])
50
-
51
- useEffect(() => {
52
- if (!nodeId) {
53
- setState({ status: 'idle' })
54
- return
55
- }
56
-
57
- let cancelled = false
58
- setState({ status: 'loading' })
59
- session
60
- .get(`@${nodeId}`)
61
- .then((node) => {
62
- if (cancelled) return
63
- if (node === null) {
64
- setState({ status: 'error', message: 'Node not found or not visible' })
65
- return
66
- }
67
- setState({ status: 'ok', node: toKernelNode(node) })
68
- })
69
- .catch((err: unknown) => {
70
- if (!cancelled) {
71
- setState({ status: 'error', message: errorMessage(err) })
72
- }
73
- })
74
-
75
- return () => {
76
- cancelled = true
77
- }
78
- }, [session, nodeId, nonce])
79
-
80
- return { ...state, reload }
81
- }
@@ -1,91 +0,0 @@
1
- import {
2
- createShell,
3
- type IntentMessage,
4
- type IntentRegistry,
5
- type KernelClient,
6
- type Shell,
7
- } from '@astrale-os/shell'
8
- import { useEffect, useMemo, useState } from 'react'
9
-
10
- export type ShellStatus = 'loading' | 'ready' | 'standalone'
11
-
12
- export type ShellState = {
13
- status: ShellStatus
14
- /** The kernel handle once `ready` — view hooks call through `session.call`. */
15
- session: KernelClient | null
16
- /** Current target node id — updates on `setTarget` hot-swaps. */
17
- nodeId: string | undefined
18
- /** Why we fell back to `standalone` (no parent / handshake timeout). */
19
- reason: string | null
20
- }
21
-
22
- /**
23
- * Boots the real Astrale shell in sandboxed (child) mode: runs the init
24
- * handshake with the parent window to obtain the kernel session + target node,
25
- * then tracks `setTarget` hot-swaps. The shell's `KernelClient` owns the
26
- * delegation token (refreshed by the parent) and the kernel transport, so view
27
- * hooks just call `session.call('@<id>::<method>')`.
28
- *
29
- * Three outcomes:
30
- * - `loading` — handshake in flight
31
- * - `ready` — parent completed the handshake; `session`/`nodeId` populated
32
- * - `standalone` — no parent or it timed out (opened directly in a tab); the
33
- * view renders a self-describing fallback
34
- */
35
- export function useShell(): ShellState {
36
- const [status, setStatus] = useState<ShellStatus>('loading')
37
- const [session, setSession] = useState<KernelClient | null>(null)
38
- const [nodeId, setNodeId] = useState<string | undefined>(undefined)
39
- const [reason, setReason] = useState<string | null>(null)
40
-
41
- useEffect(() => {
42
- // Not framed (opened directly in a tab): skip the handshake entirely rather
43
- // than waiting out the init timeout — there is no parent to answer it.
44
- if (!window.parent || window.parent === window) {
45
- setReason('not running inside a parent frame')
46
- setStatus('standalone')
47
- return
48
- }
49
-
50
- let cancelled = false
51
- let shell: Shell | null = null
52
-
53
- void (async () => {
54
- try {
55
- const built = createShell({ mode: 'sandboxed', initTimeoutMs: 5000 })
56
- await built.init()
57
- if (cancelled) {
58
- void built.dispose()
59
- return
60
- }
61
- shell = built
62
-
63
- // Hand views the shell's real `KernelClient` directly. It reads the live
64
- // (parent-refreshed) token on each call and owns the transport, so view
65
- // hooks just call `session.call('@<id>::<method>', params)`.
66
- setSession(built.kernel)
67
- setNodeId(built.targetNodeId)
68
- setStatus('ready')
69
-
70
- // Hot-swap: the parent pushes a new target via the typed `setTarget`
71
- // intent; the iframe stays mounted, only `nodeId` updates.
72
- built.parent?.on('intent', (msg: IntentMessage) => {
73
- if (msg.envelope.name !== 'setTarget') return
74
- const payload = msg.envelope.payload as IntentRegistry['setTarget']['payload']
75
- if (typeof payload.nodeId === 'string') setNodeId(payload.nodeId)
76
- })
77
- } catch (err) {
78
- if (cancelled) return
79
- setReason(err instanceof Error ? err.message : String(err))
80
- setStatus('standalone')
81
- }
82
- })()
83
-
84
- return () => {
85
- cancelled = true
86
- if (shell) void shell.dispose()
87
- }
88
- }, [])
89
-
90
- return useMemo(() => ({ status, session, nodeId, reason }), [status, session, nodeId, reason])
91
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Tiny multi-view router for the domain's one SPA bundle.
3
- *
4
- * The domain declares several SPA Views, each mounted by the shell at its own
5
- * path under `/ui/*` (e.g. `/ui/status-page`). The shell mounts ONE iframe per view,
6
- * so inside the iframe `window.location.pathname` is that view's mount path. This
7
- * bundle reads that path and renders the matching view — one build, many views.
8
- *
9
- * Two pieces:
10
- * - `resolveView` — map `window.location.pathname` → a view component.
11
- * - `ViewFrame` — DRYs the shell-handshake gating (`loading`/`standalone`/
12
- * `ready`) every view repeats, so a view only writes its `ready` body.
13
- *
14
- * To add a view: write a `ViewComponent` (usually wrapping `ViewFrame`), add a
15
- * `ROUTES` entry keyed by its mount path in `src/app.tsx`, and register a
16
- * matching `defineView({ mount })` in the domain's `views/`.
17
- */
18
-
19
- import type { KernelClient } from '@astrale-os/shell'
20
- import type { ReactNode } from 'react'
21
-
22
- import type { ShellState } from './use-shell'
23
-
24
- /** A view: given the shell state, render its tree. */
25
- export type ViewComponent = (shell: ShellState) => ReactNode
26
-
27
- /** Mount path → view component (key e.g. `/ui/status-page`). */
28
- export type ViewRoutes = Record<string, ViewComponent>
29
-
30
- /** Strip a single trailing slash (but keep a bare `/`). */
31
- function normalizePath(pathname: string): string {
32
- return pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
33
- }
34
-
35
- /**
36
- * Resolve the view for a mount path — exact match, tolerating a trailing slash.
37
- * Returns `undefined` when no route is registered (caller renders a fallback).
38
- */
39
- export function resolveView(
40
- routes: ViewRoutes,
41
- pathname: string = window.location.pathname,
42
- ): ViewComponent | undefined {
43
- return routes[normalizePath(pathname)]
44
- }
45
-
46
- /**
47
- * Shared shell-handshake gate. Renders the `.wrap`/`.card` shell and:
48
- * - `loading` → title + subline + "Waiting for the shell handshake…"
49
- * - `standalone` → title + subline + the "No parent shell" banner copy
50
- * - `ready` → `children(session, nodeId)` (session is non-null here)
51
- *
52
- * Mirrors the markup/classes the original single-view app used, so styling is
53
- * unchanged across views.
54
- */
55
- export function ViewFrame({
56
- shell,
57
- title,
58
- subline,
59
- children,
60
- }: {
61
- shell: ShellState
62
- title: string
63
- subline?: string
64
- children: (session: KernelClient, nodeId: string | undefined) => ReactNode
65
- }) {
66
- const { status, session, nodeId, reason } = shell
67
-
68
- return (
69
- <div className="wrap">
70
- <div className="card">
71
- {status === 'loading' && (
72
- <>
73
- <h1 className="title">{title}</h1>
74
- {subline && <p className="subline">{subline}</p>}
75
- <p className="muted">Waiting for the shell handshake…</p>
76
- </>
77
- )}
78
-
79
- {status === 'standalone' && (
80
- <>
81
- <h1 className="title">{title}</h1>
82
- {subline && <p className="subline">{subline}</p>}
83
- <div className="banner">
84
- No parent shell ({reason}). This view is meant to be mounted by the Astrale GUI, which
85
- hands it a target node and a kernel session. Showing a standalone preview.
86
- </div>
87
- <p className="body muted">
88
- When mounted by the shell, this card renders the node you opened.
89
- </p>
90
- </>
91
- )}
92
-
93
- {status === 'ready' && session && children(session, nodeId)}
94
- </div>
95
- </div>
96
- )
97
- }
@@ -1,24 +0,0 @@
1
- /** Shared display formatters — pure, presentation-side (used across views). */
2
-
3
- /**
4
- * Coarse relative time for `lastCheckedAt`-style ISO stamps:
5
- * `just now` / `5m ago` / `2h ago` / a `YYYY-MM-DD` date for anything older than
6
- * 30 days. Returns `—` for a missing stamp and the raw string for an unparseable
7
- * one.
8
- */
9
- export function relativeTime(iso: string | undefined, now = Date.now()): string {
10
- if (!iso) return '—'
11
- const t = Date.parse(iso)
12
- if (Number.isNaN(t)) return iso
13
- const diff = now - t
14
- if (diff < 0) return 'just now'
15
- const s = Math.floor(diff / 1000)
16
- if (s < 45) return 'just now'
17
- const m = Math.floor(s / 60)
18
- if (m < 60) return `${m}m ago`
19
- const h = Math.floor(m / 60)
20
- if (h < 48) return `${h}h ago`
21
- const d = Math.floor(h / 24)
22
- if (d < 30) return `${d}d ago`
23
- return new Date(t).toISOString().slice(0, 10)
24
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * The design system — generic, feature-AGNOSTIC presentational primitives +
3
- * display formatters, no domain knowledge and no kernel session. Import from the
4
- * barrel: `import { Panel, KV, relativeTime } from '@/ui'`. A view's own
5
- * health/status chips and other domain-shaped widgets live alongside their
6
- * feature, not here.
7
- */
8
- export { EmptyState, ErrorBanner, Panel, Spinner } from './surface'
9
- export { ExternalLink, KV, Mono } from './value'
10
- export { relativeTime } from './format'
@@ -1,56 +0,0 @@
1
- /** Containers + feedback surfaces — pure presentational. */
2
- import type { ReactNode } from 'react'
3
-
4
- /**
5
- * A titled card section. `actions` render in the header (e.g. a button); `tone:
6
- * 'danger'` tints the border/title for destructive panels.
7
- */
8
- export function Panel({
9
- title,
10
- actions,
11
- tone,
12
- children,
13
- }: {
14
- title: string
15
- actions?: ReactNode
16
- tone?: 'danger'
17
- children: ReactNode
18
- }) {
19
- return (
20
- <section className={tone === 'danger' ? 'panel panel-danger' : 'panel'}>
21
- <header className="panel-head">
22
- <h2 className="panel-title">{title}</h2>
23
- {actions && <div className="panel-actions">{actions}</div>}
24
- </header>
25
- {children}
26
- </section>
27
- )
28
- }
29
-
30
- /** A red alert banner for a failed call. */
31
- export function ErrorBanner({ children }: { children: ReactNode }) {
32
- return (
33
- <div className="banner banner-error" role="alert">
34
- {children}
35
- </div>
36
- )
37
- }
38
-
39
- /** Teaching empty state — what's missing and an optional command that fixes it. */
40
- export function EmptyState({ children, hint }: { children: ReactNode; hint?: string }) {
41
- return (
42
- <div className="empty">
43
- <p className="empty-text">{children}</p>
44
- {hint && <code className="empty-hint">{hint}</code>}
45
- </div>
46
- )
47
- }
48
-
49
- /** A spinner with a label, for in-flight loads. */
50
- export function Spinner({ label }: { label: string }) {
51
- return (
52
- <p className="muted loading-line">
53
- <span className="spinner" aria-hidden="true" /> {label}
54
- </p>
55
- )
56
- }