mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
feat(ui-tui): widget primitives — charts, accordion, shimmer, stable streams
Reusable render primitives the SDK exposes to widget authors: sparkline/gauge/ hbars chart helpers (dimension-stable so live updates never resize the card), an Accordion for expand/collapse sections, animated shimmer loaders, and a streams demo that no longer reserves a phantom icon column on unfocused titles.
This commit is contained in:
parent
ca0b1b130c
commit
ed93d6afe0
10 changed files with 269 additions and 87 deletions
|
|
@ -62,7 +62,35 @@ export default function register(sdk) {
|
|||
|
||||
`sdk` contents: `defineWidgetApp`, `openWidget`, `updateWidget`, `isCtrl`,
|
||||
`React`, `h` (createElement — no JSX in .mjs), components `Box`, `Text`,
|
||||
`Dialog`, `Overlay`, `WidgetGrid`, `GridAreas`.
|
||||
`Dialog`, `Overlay`, `WidgetGrid`, `GridAreas`, and loaders `Shimmer`,
|
||||
`ShimmerRows`, `useShimmerPhase` — use `ShimmerRows` for loading phases
|
||||
instead of a bare "loading…" line.
|
||||
|
||||
Expand/collapse: `sdk.Accordion` — the same primitive the session panel's
|
||||
tool/skill sections use. `h(Accordion, { t, title: 'details', count: 3,
|
||||
defaultOpen: false }, body)` toggles on CLICK (works in ambient widgets,
|
||||
which receive no keys); modal apps may pass `open` + `onToggle` to drive it
|
||||
from reducer state instead.
|
||||
|
||||
Stable sizing (cards must NEVER resize while ticking):
|
||||
|
||||
- Give `Dialog` an explicit `width`; charts already return exactly the
|
||||
`width` you ask for (short series pad-left while history warms up).
|
||||
- Pad dynamic numbers: `String(v).padStart(6)` — `51 ms` → `112 ms` must
|
||||
not change the line length.
|
||||
- Keep row counts constant per phase; swap content, not structure.
|
||||
|
||||
Charts (pure string builders — color the result with theme tones):
|
||||
|
||||
- `sdk.sparkline(series, width?)` → `▂▃▅▇█▆` one-row trend
|
||||
- `sdk.sparkRows(series, width, rows)` → multi-row column chart (top line
|
||||
first) — the mission-control panel look; taller cells gain resolution
|
||||
- `sdk.gauge(ratio, width)` → `█████░░░` fill bar for a 0..1 value
|
||||
- `sdk.hbars(values, width)` → horizontal bar chart, one bar per value,
|
||||
eighth-block tips, scaled to the max
|
||||
|
||||
Keep a rolling series in component state (push per tick, cap ~120 samples)
|
||||
and render `sparkRows` for dashboard panels, `sparkline` for one-liners.
|
||||
|
||||
Contract essentials:
|
||||
|
||||
|
|
|
|||
47
ui-tui/src/__tests__/charts.test.ts
Normal file
47
ui-tui/src/__tests__/charts.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { gauge, hbars, sparkline, sparkRows } from '../lib/charts.js'
|
||||
|
||||
describe('chart primitives', () => {
|
||||
it('sparkline spans the block ramp and respects width', () => {
|
||||
const line = sparkline([0, 1, 2, 3, 4, 5, 6, 7], 8)
|
||||
|
||||
expect(line).toBe('▁▂▃▄▅▆▇█')
|
||||
expect(sparkline([1, 2, 3, 4], 2)).toHaveLength(2) // window = last N
|
||||
})
|
||||
|
||||
it('is dimension-stable: short/empty series pad to exactly width', () => {
|
||||
// Warm-up must never resize the card — latest sample pins right.
|
||||
expect(sparkline([5], 6)).toHaveLength(6)
|
||||
expect(sparkline([5], 6).endsWith('▁')).toBe(true) // flat series → bottom block, right-pinned
|
||||
expect(sparkline([], 6)).toBe(' ')
|
||||
expect(sparkRows([7], 5, 2).every(row => row.length === 5)).toBe(true)
|
||||
expect(sparkRows([], 5, 2)).toEqual([' ', ' '])
|
||||
expect(hbars([1, 4], 8).every(bar => bar.length === 8)).toBe(true)
|
||||
})
|
||||
|
||||
it('sparkRows partitions each column across rows (top line first)', () => {
|
||||
const rows = sparkRows([0, 8, 4], 3, 2)
|
||||
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows.every(r => r.length === 3)).toBe(true)
|
||||
// Max value fills the top row cell; min value leaves it blank.
|
||||
expect(rows[0]![1]).toBe('█')
|
||||
expect(rows[0]![0]).toBe(' ')
|
||||
})
|
||||
|
||||
it('gauge clamps and fills proportionally', () => {
|
||||
expect(gauge(0.5, 8)).toBe('████░░░░')
|
||||
expect(gauge(-1, 4)).toBe('░░░░')
|
||||
expect(gauge(9, 4)).toBe('████')
|
||||
})
|
||||
|
||||
it('hbars scales to the max with eighth-block tips', () => {
|
||||
const [half, full] = hbars([4, 8], 8)
|
||||
|
||||
expect(full).toBe('████████')
|
||||
expect(half).toBe('████ ')
|
||||
expect(hbars([3, 8], 8)[0]).toBe('███ ') // 3/8 of 8 cells, padded
|
||||
expect(hbars([1, 2], 3)[0]).toMatch(/^█?[▏▎▍▌▋▊▉█] *$/) // fractional tip, padded
|
||||
})
|
||||
})
|
||||
58
ui-tui/src/components/accordion.tsx
Normal file
58
ui-tui/src/components/accordion.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Box, Text } from '@hermes/ink'
|
||||
import { type ReactNode, useState } from 'react'
|
||||
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
/**
|
||||
* THE expand/collapse primitive — the session panel's tool/skill sections
|
||||
* and widget-app accordions are the same component. Click the header to
|
||||
* toggle (mouse works even in ambient widgets, which receive no keys);
|
||||
* modal apps may instead drive `open` from reducer state (controlled).
|
||||
* Uncontrolled by default: pass `defaultOpen` and forget it.
|
||||
*/
|
||||
export function Accordion({
|
||||
children,
|
||||
count,
|
||||
defaultOpen = false,
|
||||
onToggle,
|
||||
open,
|
||||
suffix,
|
||||
t,
|
||||
title
|
||||
}: {
|
||||
children: ReactNode
|
||||
count?: number
|
||||
defaultOpen?: boolean
|
||||
/** Controlled open state; omit for internal (click-toggled) state. */
|
||||
open?: boolean
|
||||
onToggle?: () => void
|
||||
suffix?: string
|
||||
t: Theme
|
||||
title: string
|
||||
}) {
|
||||
const [uncontrolled, setUncontrolled] = useState(defaultOpen)
|
||||
const isOpen = open ?? uncontrolled
|
||||
|
||||
const toggle = () => {
|
||||
onToggle?.()
|
||||
|
||||
if (open === undefined) {
|
||||
setUncontrolled(v => !v)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box onClick={toggle}>
|
||||
<Text color={t.color.accent}>{isOpen ? '▾ ' : '▸ '}</Text>
|
||||
<Text bold color={t.color.accent}>
|
||||
{title}
|
||||
</Text>
|
||||
{typeof count === 'number' ? <Text color={t.color.muted}> ({count})</Text> : null}
|
||||
{suffix ? <Text color={t.color.muted}> {suffix}</Text> : null}
|
||||
</Box>
|
||||
|
||||
{isOpen ? children : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { flat } from '../lib/text.js'
|
|||
import type { Theme } from '../theme.js'
|
||||
import type { PanelSection, SessionInfo } from '../types.js'
|
||||
|
||||
import { Accordion } from './accordion.js'
|
||||
import { ShimmerRows } from './loaders.js'
|
||||
import { WidgetGrid } from './widgetGrid.js'
|
||||
|
||||
|
|
@ -203,35 +204,6 @@ const SKELETON_ROWS: readonly (readonly [number, number])[] = [
|
|||
[10, 13]
|
||||
]
|
||||
|
||||
// ── Collapsible helpers ──────────────────────────────────────────────
|
||||
|
||||
function CollapseToggle({
|
||||
count,
|
||||
open,
|
||||
suffix,
|
||||
t,
|
||||
title,
|
||||
onToggle
|
||||
}: {
|
||||
count?: number
|
||||
open: boolean
|
||||
suffix?: string
|
||||
t: Theme
|
||||
title: string
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<Box onClick={onToggle}>
|
||||
<Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
|
||||
<Text bold color={t.color.accent}>
|
||||
{title}
|
||||
</Text>
|
||||
{typeof count === 'number' ? <Text color={t.color.muted}> ({count})</Text> : null}
|
||||
{suffix ? <Text color={t.color.muted}> {suffix}</Text> : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// ── SessionPanel ─────────────────────────────────────────────────────
|
||||
|
||||
const SKILLS_MAX = 8
|
||||
|
|
@ -431,49 +403,53 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) {
|
|||
|
||||
{/* ── Tools (expanded by default) ── */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle onToggle={() => setToolsOpen(v => !v)} open={toolsOpen} t={t} title="Available Tools" />
|
||||
{toolsOpen && toolsBody()}
|
||||
<Accordion onToggle={() => setToolsOpen(v => !v)} open={toolsOpen} t={t} title="Available Tools">
|
||||
{toolsBody()}
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
||||
{/* ── Skills (collapsed by default) ── */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
<Accordion
|
||||
count={skillsTotal}
|
||||
onToggle={() => setSkillsOpen(v => !v)}
|
||||
open={skillsOpen}
|
||||
suffix={skillsCatCount > 0 ? `in ${skillsCatCount} categor${skillsCatCount === 1 ? 'y' : 'ies'}` : undefined}
|
||||
t={t}
|
||||
title="Available Skills"
|
||||
/>
|
||||
{skillsOpen && skillsBody()}
|
||||
>
|
||||
{skillsBody()}
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
||||
{/* ── System Prompt (collapsed by default) ── */}
|
||||
{sysPromptLen > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
<Accordion
|
||||
onToggle={() => setSystemOpen(v => !v)}
|
||||
open={systemOpen}
|
||||
suffix={`— ${sysPromptLen.toLocaleString()} chars`}
|
||||
t={t}
|
||||
title="System Prompt"
|
||||
/>
|
||||
{systemOpen && systemBody()}
|
||||
>
|
||||
{systemBody()}
|
||||
</Accordion>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── MCP Servers (collapsed by default) ── */}
|
||||
{mcpServers.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
<Accordion
|
||||
count={mcpConnected}
|
||||
onToggle={() => setMcpOpen(v => !v)}
|
||||
open={mcpOpen}
|
||||
suffix="connected"
|
||||
t={t}
|
||||
title="MCP Servers"
|
||||
/>
|
||||
{mcpOpen && mcpBody()}
|
||||
>
|
||||
{mcpBody()}
|
||||
</Accordion>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Box, Text } from '@hermes/ink'
|
||||
import { memo, type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { sparkRows } from '../lib/charts.js'
|
||||
import type { GridAreaCell, GridTrackSize } from '../lib/widgetGrid.js'
|
||||
import type { GridTestState } from '../sdk/apps/gridTestState.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
|
@ -18,8 +19,6 @@ const HEADER_ROWS = 3
|
|||
const STREAM_ROWS = 3
|
||||
const GRID_HEIGHT = HEADER_ROWS + STREAM_ROWS * 6
|
||||
|
||||
const SPARK_CHARS = '▁▂▃▄▅▆▇█'
|
||||
|
||||
interface StreamDef {
|
||||
id: string
|
||||
render: (inner: { height: number; t: Theme; width: number }) => ReactNode
|
||||
|
|
@ -57,37 +56,6 @@ const useHistory = (tick: number, sample: () => number, cap = 240) => {
|
|||
return historyRef.current
|
||||
}
|
||||
|
||||
/**
|
||||
* Render history as a column chart `rows` lines tall (top line first). Each
|
||||
* column gets `rows * 8` vertical levels — full blocks below the value, a
|
||||
* partial eighth-block at the value row, spaces above — so a promoted (taller)
|
||||
* cell genuinely gains chart resolution rather than just whitespace.
|
||||
*/
|
||||
const sparkRows = (history: number[], width: number, rows: number): string[] => {
|
||||
const window = history.slice(-Math.max(1, width))
|
||||
|
||||
if (!window.length) {
|
||||
return Array.from({ length: rows }, () => '')
|
||||
}
|
||||
|
||||
const min = Math.min(...window)
|
||||
const max = Math.max(...window)
|
||||
const range = max - min || 1
|
||||
const levels = window.map(v => Math.max(1, Math.round(((v - min) / range) * rows * 8)))
|
||||
|
||||
return Array.from({ length: rows }, (_, lineIdx) => {
|
||||
const rowFromBottom = rows - 1 - lineIdx
|
||||
|
||||
return levels
|
||||
.map(level => {
|
||||
const filled = Math.min(8, Math.max(0, level - rowFromBottom * 8))
|
||||
|
||||
return filled === 0 ? ' ' : SPARK_CHARS[filled - 1]
|
||||
})
|
||||
.join('')
|
||||
})
|
||||
}
|
||||
|
||||
// ── stream panels ───────────────────────────────────────────────────────────
|
||||
|
||||
const TOKEN_WORDS = (
|
||||
|
|
@ -311,8 +279,10 @@ function StreamPanel({
|
|||
paddingX={1}
|
||||
width={cell.width}
|
||||
>
|
||||
{/* No phantom icon column: unfocused titles sit flush left — the ▸
|
||||
appears (and shifts the title) only while focused. */}
|
||||
<Text bold={focused} color={focused ? t.color.primary : t.color.label} wrap="truncate">
|
||||
{focused ? '▸ ' : ' '}
|
||||
{focused ? '▸ ' : ''}
|
||||
{title}
|
||||
{main ? ' ·' : ''}
|
||||
</Text>
|
||||
|
|
|
|||
80
ui-tui/src/lib/charts.ts
Normal file
80
ui-tui/src/lib/charts.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Chart primitives — pure string builders (no React), THE charting layer for
|
||||
* widget apps and demos alike. Everything returns plain strings the caller
|
||||
* colors with theme tones; everything auto-scales to the series' min/max.
|
||||
*/
|
||||
|
||||
const BLOCKS = '▁▂▃▄▅▆▇█'
|
||||
|
||||
const normalize = (series: number[], window: number): { min: number; range: number; window: number[] } => {
|
||||
const view = series.slice(-Math.max(1, window))
|
||||
const min = Math.min(...view)
|
||||
|
||||
return { min, range: Math.max(...view) - min || 1, window: view }
|
||||
}
|
||||
|
||||
/** One-row sparkline: `▂▃▅▇█▆…`, last `width` samples. ALWAYS exactly
|
||||
* `width` cells — short series pad-left so the card never resizes while
|
||||
* history warms up (latest sample stays pinned to the right edge). */
|
||||
export function sparkline(series: number[], width = series.length): string {
|
||||
if (!series.length) {
|
||||
return ' '.repeat(Math.max(0, width))
|
||||
}
|
||||
|
||||
const { min, range, window } = normalize(series, width)
|
||||
|
||||
return window
|
||||
.map(v => BLOCKS[Math.min(BLOCKS.length - 1, Math.floor(((v - min) / range) * BLOCKS.length))])
|
||||
.join('')
|
||||
.padStart(width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-row column chart, top line first — the streams-demo panel chart.
|
||||
* Each column resolves to `rows * 8` vertical levels (full blocks below the
|
||||
* value, a partial eighth-block at it), so taller cells genuinely gain
|
||||
* resolution.
|
||||
*/
|
||||
export function sparkRows(series: number[], width: number, rows: number): string[] {
|
||||
if (!series.length) {
|
||||
return Array.from({ length: rows }, () => ' '.repeat(width))
|
||||
}
|
||||
|
||||
const { min, range, window } = normalize(series, width)
|
||||
const levels = window.map(v => Math.max(1, Math.round(((v - min) / range) * rows * 8)))
|
||||
|
||||
return Array.from({ length: rows }, (_, lineIdx) => {
|
||||
const rowFromBottom = rows - 1 - lineIdx
|
||||
|
||||
return levels
|
||||
.map(level => {
|
||||
const filled = Math.min(8, Math.max(0, level - rowFromBottom * 8))
|
||||
|
||||
return filled === 0 ? ' ' : BLOCKS[filled - 1]
|
||||
})
|
||||
.join('')
|
||||
.padStart(width) // warm-up pads left: chart grows from the right, card never resizes
|
||||
})
|
||||
}
|
||||
|
||||
/** Horizontal fill gauge: `█████░░░` for a 0..1 ratio. */
|
||||
export function gauge(ratio: number, width: number): string {
|
||||
const filled = Math.round(Math.min(1, Math.max(0, ratio)) * width)
|
||||
|
||||
return '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled))
|
||||
}
|
||||
|
||||
/** Horizontal bar chart: one `███▌`-style bar per value, scaled to the max,
|
||||
* each padded to exactly `width` (stable card sizing). Eighth-block tips
|
||||
* keep adjacent values distinguishable. */
|
||||
export function hbars(values: number[], width: number): string[] {
|
||||
const max = Math.max(...values, 0) || 1
|
||||
|
||||
return values.map(v => {
|
||||
const cells = (Math.min(max, Math.max(0, v)) / max) * width
|
||||
const full = Math.floor(cells)
|
||||
const rest = Math.round((cells - full) * 8)
|
||||
|
||||
return ('█'.repeat(full) + (rest > 0 ? '▏▎▍▌▋▊▉█'[rest - 1] : '')).padEnd(width)
|
||||
})
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { Box, Text } from '@hermes/ink'
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Dialog } from '../../components/overlay.js'
|
||||
import { sparkline } from '../../lib/charts.js'
|
||||
import type { Theme } from '../../theme.js'
|
||||
import { defineWidgetApp } from '../registry.js'
|
||||
import { isCtrl } from '../types.js'
|
||||
|
|
@ -13,7 +14,6 @@ import { isCtrl } from '../types.js'
|
|||
*/
|
||||
|
||||
const USAGE = 'usage: /ticker [symbol]'
|
||||
const BLOCKS = '▁▂▃▄▅▆▇█'
|
||||
const POINTS = 26
|
||||
const TICK_MS = 250
|
||||
const PIP = 0.0001
|
||||
|
|
@ -22,13 +22,6 @@ export interface TickerState {
|
|||
symbol: string
|
||||
}
|
||||
|
||||
const spark = (series: number[]): string => {
|
||||
const min = Math.min(...series)
|
||||
const span = Math.max(...series) - min || 1
|
||||
|
||||
return series.map(v => BLOCKS[Math.min(BLOCKS.length - 1, Math.floor(((v - min) / span) * BLOCKS.length))]).join('')
|
||||
}
|
||||
|
||||
function Chart({ symbol, t }: { symbol: string; t: Theme }) {
|
||||
const [series, setSeries] = useState<number[]>(() => {
|
||||
const seed = 1.1 + Math.random() * 0.4
|
||||
|
|
@ -67,7 +60,7 @@ function Chart({ symbol, t }: { symbol: string; t: Theme }) {
|
|||
{Math.abs(delta / PIP).toFixed(1)}p
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color={dir}>{spark(series)}</Text>
|
||||
<Text color={dir}>{sparkline(series)}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Box, Text } from '@hermes/ink'
|
||||
|
||||
import { ShimmerRows } from '../../components/loaders.js'
|
||||
import { Dialog } from '../../components/overlay.js'
|
||||
import { mix } from '../../lib/color.js'
|
||||
import type { Theme } from '../../theme.js'
|
||||
import { updateWidget } from '../host.js'
|
||||
import { defineWidgetApp } from '../registry.js'
|
||||
|
|
@ -15,6 +17,14 @@ import { isCtrl } from '../types.js'
|
|||
|
||||
const USAGE = 'usage: /weather [location] (blank = geolocate by IP)'
|
||||
|
||||
// Skeleton mirrors the ready layout: art column + four stat lines.
|
||||
const LOADING_ROWS: readonly (readonly [number, number])[] = [
|
||||
[13, 12],
|
||||
[13, 16],
|
||||
[13, 14],
|
||||
[13, 11]
|
||||
]
|
||||
|
||||
type Phase = { kind: 'error'; message: string } | { kind: 'loading' } | { kind: 'ready'; report: Report }
|
||||
|
||||
export interface WeatherState {
|
||||
|
|
@ -154,7 +164,13 @@ export const weatherApp = defineWidgetApp<WeatherState>({
|
|||
|
||||
return (
|
||||
<Dialog title={title} width={Math.min(42, cols - 4)}>
|
||||
{phase.kind === 'loading' && <Text color={t.color.muted}>fetching wttr.in…</Text>}
|
||||
{phase.kind === 'loading' && (
|
||||
<ShimmerRows
|
||||
color={mix(t.color.muted, t.color.completionBg, 0.5)}
|
||||
highlight={t.color.label}
|
||||
rows={LOADING_ROWS}
|
||||
/>
|
||||
)}
|
||||
{phase.kind === 'error' && <Text color={t.color.error}>{phase.message}</Text>}
|
||||
{phase.kind === 'ready' && <ReadyBody report={phase.report} t={t} />}
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@
|
|||
* See `sdk/apps/` for the reference apps (`/grid-test`, `/dialog-test`).
|
||||
*/
|
||||
|
||||
// Theme + chrome primitives
|
||||
export { Accordion } from '../components/accordion.js'
|
||||
export { Shimmer, ShimmerRows, shimmerSegments, useShimmerPhase } from '../components/loaders.js'
|
||||
// Layout components + overlay primitives
|
||||
export { Dialog, Overlay, type OverlayZone } from '../components/overlay.js'
|
||||
// Theme + chrome primitives
|
||||
export { OverlayHint, windowItems } from '../components/overlayControls.js'
|
||||
export {
|
||||
ActionRow,
|
||||
|
|
@ -26,6 +28,7 @@ export {
|
|||
|
||||
export { GridAreas, WidgetGrid } from '../components/widgetGrid.js'
|
||||
|
||||
export { gauge, hbars, sparkline, sparkRows } from '../lib/charts.js'
|
||||
export { contrastRatio, liftForContrast, mix, relativeLuminance } from '../lib/color.js'
|
||||
// Layout engine
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ import { pathToFileURL } from 'url'
|
|||
import { Box, Text } from '@hermes/ink'
|
||||
import * as React from 'react'
|
||||
|
||||
import { Accordion } from '../components/accordion.js'
|
||||
import { Shimmer, ShimmerRows, useShimmerPhase } from '../components/loaders.js'
|
||||
import { Dialog, Overlay } from '../components/overlay.js'
|
||||
import { GridAreas, WidgetGrid } from '../components/widgetGrid.js'
|
||||
import { gauge, hbars, sparkline, sparkRows } from '../lib/charts.js'
|
||||
import { recordParentLifecycle } from '../lib/parentLog.js'
|
||||
|
||||
import { openWidget, updateWidget } from './host.js'
|
||||
|
|
@ -31,18 +34,26 @@ import { isCtrl } from './types.js'
|
|||
/** Everything a user widget may touch, passed INTO its register() — user
|
||||
* files have no resolvable import path to the bundle. */
|
||||
export const widgetSdk = {
|
||||
Accordion,
|
||||
Box,
|
||||
Dialog,
|
||||
GridAreas,
|
||||
Overlay,
|
||||
React,
|
||||
Shimmer,
|
||||
ShimmerRows,
|
||||
Text,
|
||||
WidgetGrid,
|
||||
defineWidgetApp,
|
||||
gauge,
|
||||
h: React.createElement,
|
||||
hbars,
|
||||
isCtrl,
|
||||
openWidget,
|
||||
updateWidget
|
||||
sparkRows,
|
||||
sparkline,
|
||||
updateWidget,
|
||||
useShimmerPhase
|
||||
} as const
|
||||
|
||||
export type WidgetSdk = typeof widgetSdk
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue