Skip to content

Commit 1819c41

Browse files
committed
Merge remote-tracking branch 'origin/main' into fancy-dodos-remain
# Conflicts: # plugins/terminals/src/client/App.svelte # plugins/terminals/src/client/design.ts
2 parents 28ea9f4 + 97b65e9 commit 1819c41

96 files changed

Lines changed: 2764 additions & 1484 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

alias.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export const alias = {
7979
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),
8080
'@devframes/plugin-inspect/vite': p('inspect/src/vite.ts'),
8181
'@devframes/plugin-inspect': p('inspect/src/index.ts'),
82+
'@devframes/plugin-og/client': p('og/src/client/index.ts'),
83+
'@devframes/plugin-og/node': p('og/src/node/index.ts'),
84+
'@devframes/plugin-og/rpc': p('og/src/rpc/index.ts'),
85+
'@devframes/plugin-og/cli': p('og/src/cli.ts'),
86+
'@devframes/plugin-og/vite': p('og/src/vite.ts'),
87+
'@devframes/plugin-og': p('og/src/index.ts'),
8288
'@devframes/plugin-a11y/client': p('a11y/src/client/index.ts'),
8389
'@devframes/plugin-a11y/node': p('a11y/src/node/index.ts'),
8490
'@devframes/plugin-a11y/cli': p('a11y/src/cli.ts'),

design/design.ts

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// @unocss-include
2+
// Shared devframe -> @antfu/design class helpers: framework-neutral builders
3+
// returning @antfu/design's semantic shortcut classes, so every surface (Solid,
4+
// Svelte, React, Preact, vanilla) looks identical to the antfu Vue components.
5+
// The `@unocss-include` marker makes UnoCSS emit the runtime-assembled class
6+
// chains below; `design/uno.config.ts` adds this file to every app's content so
7+
// the chains are extracted regardless of framework.
8+
// Tag palette kept literal for extraction: badge-color-blue badge-color-amber
9+
// badge-color-green badge-color-red badge-color-sky badge-color-violet
10+
// badge-color-rose badge-color-teal badge-color-orange badge-color-emerald
11+
12+
export function cx(...parts: Array<string | false | null | undefined>): string {
13+
return parts.filter(Boolean).join(' ')
14+
}
15+
16+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link'
17+
export type ButtonSize = 'md' | 'sm' | 'lg'
18+
export interface ButtonProps { variant?: ButtonVariant, size?: ButtonSize, class?: string }
19+
20+
export function button({ variant = 'primary', size = 'md', class: extra }: ButtonProps = {}): string {
21+
const variantClass: Record<ButtonVariant, string> = {
22+
primary: 'btn-primary',
23+
secondary: 'btn-action',
24+
outline: 'btn-action',
25+
ghost: 'inline-flex items-center justify-center gap-1.5 rounded px2 py1 op75 hover:op100 hover:bg-active transition disabled:pointer-events-none disabled:op30!',
26+
destructive: 'btn-action text-error border-error/30!',
27+
link: 'inline-flex items-center gap-1.5 color-active hover:underline underline-offset-2',
28+
}
29+
const sizeClass = size === 'sm'
30+
? (variant === 'primary' ? 'text-sm px-2.5! py-1!' : 'text-sm')
31+
: size === 'lg' ? 'text-base px-4! py-2!' : ''
32+
return cx(variantClass[variant], sizeClass, extra)
33+
}
34+
35+
export type IconButtonVariant = 'outline' | 'ghost'
36+
export type IconButtonSize = 'md' | 'sm'
37+
export interface IconButtonProps { variant?: IconButtonVariant, size?: IconButtonSize, class?: string }
38+
39+
export function iconButton({ variant = 'outline', size = 'md', class: extra }: IconButtonProps = {}): string {
40+
const base = variant === 'ghost' ? 'btn-icon' : 'btn-icon-square'
41+
const sizeClass = size === 'sm' ? 'w-7! h-7! text-sm' : ''
42+
return cx(base, sizeClass, extra)
43+
}
44+
45+
export type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
46+
export interface BadgeProps { variant?: BadgeVariant, class?: string }
47+
48+
export function badge({ variant = 'secondary', class: extra }: BadgeProps = {}): string {
49+
const variantClass: Record<BadgeVariant, string> = {
50+
primary: 'badge-active',
51+
secondary: 'badge-muted',
52+
success: 'badge badge-color-green',
53+
warning: 'badge badge-color-amber',
54+
destructive: 'badge badge-color-red',
55+
outline: 'badge border border-base',
56+
}
57+
return cx(variantClass[variant], extra)
58+
}
59+
60+
export function tag(color: string, extra?: string): string {
61+
return cx('badge', `badge-color-${color}`, extra)
62+
}
63+
64+
export function tabsList(extra?: string): string {
65+
return cx('inline-flex items-center gap-1 p-1 rounded-lg bg-secondary w-max', extra)
66+
}
67+
68+
export function tab(extra?: string): string {
69+
return cx(
70+
'px-3 py-1 rounded-md text-sm color-muted inline-flex gap-1.5 items-center whitespace-nowrap select-none cursor-pointer transition outline-none hover:color-base focus-visible:ring-2 focus-visible:ring-primary-500/40 data-[state=active]:bg-base data-[state=active]:color-base data-[state=active]:shadow-sm',
71+
extra,
72+
)
73+
}
74+
75+
export interface NavTabProps { active?: boolean, class?: string }
76+
export function navTab({ active = false, class: extra }: NavTabProps = {}): string {
77+
return cx(
78+
'relative inline-flex items-center gap-1.5 max-w-52 px-2 py-1 rounded-md border border-transparent text-sm op-fade select-none cursor-pointer transition hover:op100 hover:bg-active',
79+
active && 'op100! bg-active border-base! color-base',
80+
extra,
81+
)
82+
}
83+
84+
export function nav(extra?: string): string {
85+
return cx('flex items-center gap-2 shrink-0 h-nav px-3 border-b border-base bg-base z-nav', extra)
86+
}
87+
88+
export function navBrand(extra?: string): string {
89+
return cx('flex items-center gap-1.5 shrink-0 font-semibold text-sm select-none', extra)
90+
}
91+
92+
// Mirrors devframe's `DevframeConnectionStatus` (kept local so this class-helper
93+
// module stays free of package imports); the two share the same string members.
94+
export type ConnectionStatus = 'connecting' | 'connected' | 'unauthorized' | 'disconnected' | 'error'
95+
96+
export interface ConnectionIndicator {
97+
/** Short status label, e.g. `disconnected`. */
98+
label: string
99+
/** Class chain for the status dot. */
100+
dot: string
101+
/** Class chain for the pill wrapper. */
102+
class: string
103+
}
104+
105+
const CONNECTION_TONE: Record<Exclude<ConnectionStatus, 'connected'>, { label: string, dot: string }> = {
106+
connecting: { label: 'connecting…', dot: 'bg-neutral-400 animate-pulse' },
107+
disconnected: { label: 'disconnected', dot: 'bg-error' },
108+
unauthorized: { label: 'unauthorized', dot: 'bg-warning' },
109+
error: { label: 'error', dot: 'bg-error' },
110+
}
111+
112+
// The shared top-nav connection indicator: a small status dot + label. Returns
113+
// `null` when the client is `connected`, so every surface renders the indicator
114+
// only while the connection is not live.
115+
export function connectionIndicator(status: ConnectionStatus, extra?: string): ConnectionIndicator | null {
116+
if (status === 'connected')
117+
return null
118+
const tone = CONNECTION_TONE[status]
119+
return {
120+
label: tone.label,
121+
dot: cx('inline-block size-1.5 rounded-full shrink-0', tone.dot),
122+
class: cx('flex items-center gap-1.5 shrink-0 text-xs color-muted select-none', extra),
123+
}
124+
}
125+
126+
export interface ConnectionStateCopy {
127+
/** Phosphor icon for the state glyph. */
128+
icon: string
129+
/** Short heading, e.g. `Disconnected`. */
130+
title: string
131+
/** One-line explanation of the state and how to recover. */
132+
body: string
133+
/** Whether to offer the reload recovery button (every state but `connecting`). */
134+
reloadable: boolean
135+
/** Whether the glyph should animate while the handshake is in flight. */
136+
spin: boolean
137+
}
138+
139+
const CONNECTION_STATE: Record<Exclude<ConnectionStatus, 'connected'>, ConnectionStateCopy> = {
140+
connecting: {
141+
icon: 'i-ph-plugs-connected-duotone',
142+
title: 'Connecting…',
143+
body: 'Establishing a connection to the devframe server.',
144+
reloadable: false,
145+
spin: true,
146+
},
147+
disconnected: {
148+
icon: 'i-ph-plugs-duotone',
149+
title: 'Disconnected',
150+
body: 'Lost the connection to the devframe server. Reload once it is back up.',
151+
reloadable: true,
152+
spin: false,
153+
},
154+
unauthorized: {
155+
icon: 'i-ph-lock-key-duotone',
156+
title: 'Not authorized',
157+
body: 'This client isn’t authorized. Reopen the link printed by your dev server, then reload.',
158+
reloadable: true,
159+
spin: false,
160+
},
161+
error: {
162+
icon: 'i-ph-warning-octagon-duotone',
163+
title: 'Connection failed',
164+
body: 'Could not reach the devframe server.',
165+
reloadable: true,
166+
spin: false,
167+
},
168+
}
169+
170+
// The shared full-panel connection state copy: shown whenever the client isn't
171+
// `connected`, so a surface never sits on an infinite spinner without saying
172+
// why. Returns `null` when connected. Pair with the `connection*` class builders
173+
// below so every surface renders the identical centered glyph + title + body.
174+
export function connectionState(status: ConnectionStatus): ConnectionStateCopy | null {
175+
if (status === 'connected')
176+
return null
177+
return CONNECTION_STATE[status]
178+
}
179+
180+
// Centered fill for the full-panel state; each surface adds its own fill
181+
// strategy (`h-full`, `h-svh w-full`, `absolute inset-0`, …) via `extra`.
182+
export function connectionPanel(extra?: string): string {
183+
return cx('flex flex-col items-center justify-center gap-4 bg-base p-8 text-center', extra)
184+
}
185+
186+
export function connectionGlyph(spin = false, extra?: string): string {
187+
return cx('text-4xl color-active', spin && 'animate-pulse', extra)
188+
}
189+
190+
export function connectionTitle(extra?: string): string {
191+
return cx('text-lg font-medium color-base', extra)
192+
}
193+
194+
export function connectionBody(extra?: string): string {
195+
return cx('max-w-sm text-sm color-muted', extra)
196+
}
197+
198+
export function connectionDetail(extra?: string): string {
199+
return cx('mt-1 max-w-sm break-words font-mono text-xs color-faint', extra)
200+
}
201+
202+
export function toolbar(extra?: string): string {
203+
return cx('flex items-center gap-2 shrink-0 h-8 px-2.5 border-b border-base bg-secondary text-sm', extra)
204+
}
205+
206+
export function card(extra?: string): string {
207+
return cx('flex flex-col rounded-xl border border-base bg-base shadow-sm', extra)
208+
}
209+
210+
export function modalBackdrop(extra?: string): string {
211+
return cx('fixed inset-0 z-modal-backdrop grid place-items-center p-4 bg-black/40 backdrop-blur-sm', extra)
212+
}
213+
214+
export function modalCard(extra?: string): string {
215+
return cx('z-modal-content w-full max-w-sm flex flex-col gap-3 p-4 rounded-xl border border-base bg-base shadow-lg', extra)
216+
}
217+
218+
export function panel(extra?: string): string {
219+
return cx('rounded-lg border border-base bg-base', extra)
220+
}
221+
222+
export function input(extra?: string): string {
223+
return cx('w-full min-w-0 rounded border border-base bg-base px-2.5 py-1 text-sm outline-none transition placeholder:color-faint focus-visible:border-active focus-visible:ring-2 focus-visible:ring-primary-500/40', extra)
224+
}
225+
226+
export function link(extra?: string): string {
227+
return cx('color-active hover:underline underline-offset-2', extra)
228+
}
229+
230+
export type DotState = 'running' | 'idle' | 'error'
231+
export function dot(state: DotState, extra?: string): string {
232+
const stateClass: Record<DotState, string> = {
233+
running: 'bg-success',
234+
idle: 'bg-neutral-400',
235+
error: 'bg-error',
236+
}
237+
return cx('inline-block size-1.5 rounded-full shrink-0', stateClass[state], extra)
238+
}
239+
240+
export function spinner(extra?: string): string {
241+
return cx('inline-block size-4 rounded-full border-2 border-current border-t-transparent animate-spin', extra)
242+
}

design/uno.config.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { presetAnthonyDesign } from '@antfu/design/unocss'
3+
import {
4+
defineConfig,
5+
presetIcons,
6+
presetWebFonts,
7+
presetWind4,
8+
transformerDirectives,
9+
transformerVariantGroup,
10+
} from 'unocss'
11+
12+
// Shared devframe UnoCSS base. Every plugin and example composes `@antfu/design`
13+
// the same way — its preset (tuned to devframe's sage green) over a Wind4 base,
14+
// Phosphor icons, DM Sans/Mono web fonts, and the directive/variant-group
15+
// transformers — so the surfaces look and feel like one product across
16+
// frameworks. Each app extends this via `mergeConfigs([designConfig, { … }])`
17+
// and contributes only its own extraction globs (and any safelist).
18+
//
19+
// The shared web fonts (`sans`/`mono`), the named `z-*` layers and the `h-nav`
20+
// navbar height live here so every surface shares one font stack, one z-index
21+
// scale and one fixed navbar height. The `@antfu/design` preset blocks plain
22+
// `z-<number>`, so the layers are named on purpose.
23+
export const designConfig = defineConfig({
24+
presets: [
25+
presetAnthonyDesign({ primary: '#3a6a45' }),
26+
presetWind4(),
27+
presetIcons({ scale: 1.1 }),
28+
presetWebFonts({ provider: 'none', fonts: { sans: 'DM Sans', mono: 'DM Mono' } }),
29+
],
30+
transformers: [transformerDirectives(), transformerVariantGroup()],
31+
// The shared class-helper builders (`design/design.ts`) assemble their class
32+
// chains at runtime, so every app scans that one file (it carries
33+
// `@unocss-include`) for extraction regardless of its own framework globs.
34+
content: {
35+
filesystem: [fileURLToPath(new URL('./design.ts', import.meta.url))],
36+
},
37+
// Wind4 leaves bare `border`/`border-b` at currentColor; restore the subtle
38+
// shared border color (matching `border-base`) for unqualified borders.
39+
preflights: [{ getCSS: () => '*,::before,::after{border-color:#8882}' }],
40+
shortcuts: {
41+
// Fixed navbar height, shared by every surface's top nav.
42+
'h-nav': 'h-10',
43+
// Named z-index layers, shared across every surface.
44+
'z-nav': 'z-[30]',
45+
'z-dropdown': 'z-[40]',
46+
'z-tooltip': 'z-[45]',
47+
'z-toast': 'z-[50]',
48+
'z-modal-backdrop': 'z-[60]',
49+
'z-modal-content': 'z-[70]',
50+
'z-drawer-backdrop': 'z-[80]',
51+
'z-drawer-content': 'z-[90]',
52+
},
53+
})
54+
55+
export default designConfig

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function pluginsItems(prefix: string) {
6464
{ text: 'Overview', link: `${prefix}/plugins/` },
6565
{ text: 'Data Inspector', link: `${prefix}/plugins/data-inspector` },
6666
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
67+
{ text: 'Open Graph Viewer', link: `${prefix}/plugins/og` },
6768
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
6869
{ text: 'Git', link: `${prefix}/plugins/git` },
6970
{ text: 'Terminals', link: `${prefix}/plugins/terminals` },

docs/plugins/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf
1212
|--------|--------------|--------------|
1313
| [Data Inspector](./data-inspector) | Vue | Query live server-side objects with jora — sources contributed by plugins, hosts, data files, or attached processes. |
1414
| [Devframe Inspector](./inspect) | Vue | Browse the RPC registry, invoke read-only queries, watch shared state update live, and explore the agent surface. |
15+
| [Open Graph Viewer](./og) | Vue | Inspect Open Graph and Twitter metadata and compare social-card previews. |
1516
| [Accessibility Inspector](./a11y) | Solid | Run axe-core against a host app, list WCAG violations, and highlight the offending element in the page on hover. |
1617
| [Git](./git) | React (Next.js) | A repository dashboard — status, a commit graph, branches, and diffs, with optional staging and committing. |
1718
| [Terminals](./terminals) | Svelte | Stream read-only command output and run fully interactive PTY shells in the browser. |
@@ -29,6 +30,7 @@ Most plugins publish a `bin`, so the quickest path is `npx`:
2930

3031
```sh
3132
npx @devframes/plugin-inspect # the Devframe Inspector, standalone
33+
npx @devframes/plugin-og # inspect Open Graph metadata and social cards
3234
npx @devframes/plugin-git # the Git dashboard against the current repo
3335
```
3436

docs/plugins/og.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Open Graph Viewer
6+
7+
Inspect a page's resolved Open Graph and Twitter metadata and compare its social cards across common platforms.
8+
9+
## Run Standalone
10+
11+
```sh
12+
npx @devframes/plugin-og
13+
```
14+
15+
Enter any HTTP or HTTPS page reachable from the devframe process. The viewer resolves relative image and icon URLs against the fetched document and identifies missing metadata with a ready-to-use Nuxt `useSeoMeta()` snippet.
16+
17+
## Create A Definition
18+
19+
```ts
20+
import { createOgDevframe } from '@devframes/plugin-og'
21+
22+
export default createOgDevframe({
23+
defaultUrl: 'http://localhost:3000/about',
24+
})
25+
```
26+
27+
`defaultUrl` supplies the initial target and makes `devframe build` bake that page into the static RPC dump. The resulting report keeps its assets relative and runs from any deployment path.
28+
29+
The standalone server requires devframe's trust handshake by default because it can request developer-supplied URLs. Set `auth: false` only for an isolated local environment.

0 commit comments

Comments
 (0)