Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: CI

on:
push:
branches: ['**']
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Check formatting and linting
run: npm run lint

typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Type check
run: npm run typecheck

test:
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
# Node 18 not supported - Vite 7 and Vitest 4 require Node 20+
node-version: [20, 22]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests with coverage
run: npm test -- -- --coverage

- name: Upload coverage reports
if: matrix.node-version == 22
uses: codecov/codecov-action@v5
with:
files: ./packages/*/coverage/coverage-final.json
fail_ci_if_error: false
continue-on-error: true

build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build all packages
run: npm run build

- name: Verify build outputs exist
run: |
test -d packages/core/dist || (echo "core/dist missing" && exit 1)
test -d packages/react/dist || (echo "react/dist missing" && exit 1)
test -d packages/shadcn/dist || (echo "shadcn/dist missing" && exit 1)

security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Audit dependencies
run: npm audit --audit-level=high
continue-on-error: true

# Summary job that requires all checks to pass
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs: [lint, typecheck, test, build]
if: always()
steps:
- name: Check all jobs passed
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.typecheck.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.build.result }}" != "success" ]]; then
echo "One or more required jobs failed"
exit 1
fi
echo "All CI checks passed!"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
},
"packageManager": "npm@10.9.2",
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
}
9 changes: 5 additions & 4 deletions packages/core/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import type {
A2UIComponent,
ComponentUpdate,
ComponentUpdateV09,
ComponentUpdateLegacy,
ComponentUpdateV09,
TextComponent,
} from './types/components.js'

Expand Down Expand Up @@ -68,8 +68,9 @@ export function getTextContent(component: TextComponent): string {
* Get components array from surfaceUpdate or updateComponents message
* Handles both 'updates' (legacy) and 'components' (v0.9) keys
*/
export function getComponentsArray(
msg: { updates?: ComponentUpdate[]; components?: ComponentUpdate[] }
): ComponentUpdate[] {
export function getComponentsArray(msg: {
updates?: ComponentUpdate[]
components?: ComponentUpdate[]
}): ComponentUpdate[] {
return msg.components ?? msg.updates ?? []
}
16 changes: 10 additions & 6 deletions packages/example/src/components/ComponentShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export function ComponentShowcase() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)]">Component Showcase</h2>
<h2 className="text-2xl font-bold text-[var(--color-a2ui-text-primary)]">
Component Showcase
</h2>
<div className="flex gap-2">
{CATEGORIES.map((category) => (
<button
Expand All @@ -60,8 +62,8 @@ export function ComponentShowcase() {
onClick={() => setSelectedCategory(category)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === category
? 'bg-[var(--color-accent)] text-white'
: 'bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)]'
? 'bg-[var(--color-a2ui-accent)] text-white'
: 'bg-[var(--color-a2ui-bg-secondary)] text-[var(--color-a2ui-text-secondary)] hover:bg-[var(--color-a2ui-bg-tertiary)]'
}`}
>
{CATEGORY_LABELS[category]}
Expand All @@ -71,9 +73,11 @@ export function ComponentShowcase() {
</div>

{filteredExamples.length === 0 ? (
<div className="text-center py-16 bg-[var(--color-bg-secondary)] rounded-lg border border-[var(--color-border)]">
<p className="text-[var(--color-text-secondary)] mb-2">No components registered yet</p>
<p className="text-sm text-[var(--color-text-tertiary)]">
<div className="text-center py-16 bg-[var(--color-a2ui-bg-secondary)] rounded-lg border border-[var(--color-a2ui-border)]">
<p className="text-[var(--color-a2ui-text-secondary)] mb-2">
No components registered yet
</p>
<p className="text-sm text-[var(--color-a2ui-text-tertiary)]">
Component examples will appear here once shadcn renderers are added
</p>
</div>
Expand Down
28 changes: 15 additions & 13 deletions packages/example/src/components/ComponentTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,27 @@ export function ComponentTree({ messages }: ComponentTreeProps) {
<div key={nodeId}>
<button
type="button"
className="flex items-center gap-2 py-1 px-2 hover:bg-[var(--color-bg-tertiary)] rounded cursor-pointer group w-full text-left"
className="flex items-center gap-2 py-1 px-2 hover:bg-[var(--color-a2ui-bg-tertiary)] rounded cursor-pointer group w-full text-left"
style={{ paddingLeft: `${depth * 20 + 8}px` }}
onClick={() => hasChildren && toggleNode(nodeId)}
>
{hasChildren ? (
isExpanded ? (
<ChevronDown className="w-4 h-4 text-[var(--color-text-tertiary)]" />
<ChevronDown className="w-4 h-4 text-[var(--color-a2ui-text-tertiary)]" />
) : (
<ChevronRight className="w-4 h-4 text-[var(--color-text-tertiary)]" />
<ChevronRight className="w-4 h-4 text-[var(--color-a2ui-text-tertiary)]" />
)
) : (
<div className="w-4" />
)}

<GitBranch className="w-4 h-4 text-[var(--color-accent)]" />
<GitBranch className="w-4 h-4 text-[var(--color-a2ui-accent)]" />

<span className="font-mono text-sm text-[var(--color-text-primary)]">{nodeId}</span>
<span className="font-mono text-sm text-[var(--color-a2ui-text-primary)]">{nodeId}</span>

<span className="text-xs text-[var(--color-text-tertiary)] ml-auto">{node.type}</span>
<span className="text-xs text-[var(--color-a2ui-text-tertiary)] ml-auto">
{node.type}
</span>
</button>

{hasChildren && isExpanded && (
Expand All @@ -105,9 +107,9 @@ export function ComponentTree({ messages }: ComponentTreeProps) {
if (components.size === 0) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<GitBranch className="w-12 h-12 text-[var(--color-text-tertiary)] mb-3" />
<p className="text-[var(--color-text-secondary)]">No components in tree</p>
<p className="text-sm text-[var(--color-text-tertiary)] mt-1">
<GitBranch className="w-12 h-12 text-[var(--color-a2ui-text-tertiary)] mb-3" />
<p className="text-[var(--color-a2ui-text-secondary)]">No components in tree</p>
<p className="text-sm text-[var(--color-a2ui-text-tertiary)] mt-1">
Add component.add messages to see the hierarchy
</p>
</div>
Expand All @@ -116,10 +118,10 @@ export function ComponentTree({ messages }: ComponentTreeProps) {

return (
<div className="space-y-1">
<div className="flex items-center gap-2 pb-2 mb-2 border-b border-[var(--color-border)]">
<GitBranch className="w-4 h-4 text-[var(--color-text-secondary)]" />
<h3 className="font-semibold text-[var(--color-text-primary)]">Component Tree</h3>
<span className="text-xs text-[var(--color-text-tertiary)] ml-auto">
<div className="flex items-center gap-2 pb-2 mb-2 border-b border-[var(--color-a2ui-border)]">
<GitBranch className="w-4 h-4 text-[var(--color-a2ui-text-secondary)]" />
<h3 className="font-semibold text-[var(--color-a2ui-text-primary)]">Component Tree</h3>
<span className="text-xs text-[var(--color-a2ui-text-tertiary)] ml-auto">
{components.size} component{components.size !== 1 ? 's' : ''}
</span>
</div>
Expand Down
36 changes: 19 additions & 17 deletions packages/example/src/components/DataModelViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {

const renderValue = (value: DataValue): React.ReactElement => {
if (value === null) {
return <span className="text-[var(--color-text-tertiary)]">null</span>
return <span className="text-[var(--color-a2ui-text-tertiary)]">null</span>
}
if (value === undefined) {
return <span className="text-[var(--color-text-tertiary)]">undefined</span>
return <span className="text-[var(--color-a2ui-text-tertiary)]">undefined</span>
}
if (typeof value === 'boolean') {
return <span className="text-purple-600 dark:text-purple-400">{String(value)}</span>
Expand All @@ -79,11 +79,11 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {
return <span className="text-green-600 dark:text-green-400">"{value}"</span>
}
if (Array.isArray(value)) {
return <span className="text-[var(--color-text-secondary)]">Array[{value.length}]</span>
return <span className="text-[var(--color-a2ui-text-secondary)]">Array[{value.length}]</span>
}
if (typeof value === 'object') {
const keys = Object.keys(value)
return <span className="text-[var(--color-text-secondary)]">Object({keys.length})</span>
return <span className="text-[var(--color-a2ui-text-secondary)]">Object({keys.length})</span>
}
return <span>{String(value)}</span>
}
Expand All @@ -95,7 +95,7 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {
): React.ReactElement => {
const entries = Object.entries(obj)
if (entries.length === 0) {
return <div className="text-[var(--color-text-tertiary)]">{'{}'}</div>
return <div className="text-[var(--color-a2ui-text-tertiary)]">{'{}'}</div>
}

return (
Expand All @@ -109,21 +109,23 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {
<div key={key}>
<button
type="button"
className="flex items-center gap-2 py-0.5 px-2 hover:bg-[var(--color-bg-tertiary)] rounded cursor-pointer w-full text-left"
className="flex items-center gap-2 py-0.5 px-2 hover:bg-[var(--color-a2ui-bg-tertiary)] rounded cursor-pointer w-full text-left"
style={{ paddingLeft: `${depth * 16 + 8}px` }}
onClick={() => isExpandable && toggleSection(fullKey)}
>
{isExpandable ? (
isExpanded ? (
<ChevronDown className="w-3 h-3 text-[var(--color-text-tertiary)]" />
<ChevronDown className="w-3 h-3 text-[var(--color-a2ui-text-tertiary)]" />
) : (
<ChevronRight className="w-3 h-3 text-[var(--color-text-tertiary)]" />
<ChevronRight className="w-3 h-3 text-[var(--color-a2ui-text-tertiary)]" />
)
) : (
<div className="w-3" />
)}

<span className="font-mono text-sm text-[var(--color-text-primary)]">{key}:</span>
<span className="font-mono text-sm text-[var(--color-a2ui-text-primary)]">
{key}:
</span>

{(!isExpandable || !isExpanded) && (
<span className="font-mono text-sm">{renderValue(value)}</span>
Expand All @@ -143,9 +145,9 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {
if (surfaceStates.size === 0) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<Database className="w-12 h-12 text-[var(--color-text-tertiary)] mb-3" />
<p className="text-[var(--color-text-secondary)]">No data model state</p>
<p className="text-sm text-[var(--color-text-tertiary)] mt-1">
<Database className="w-12 h-12 text-[var(--color-a2ui-text-tertiary)] mb-3" />
<p className="text-[var(--color-a2ui-text-secondary)]">No data model state</p>
<p className="text-sm text-[var(--color-a2ui-text-tertiary)] mt-1">
Surface state will appear here once surfaces are created
</p>
</div>
Expand All @@ -154,18 +156,18 @@ export function DataModelViewer({ messages }: DataModelViewerProps) {

return (
<div className="space-y-3">
<div className="flex items-center gap-2 pb-2 mb-2 border-b border-[var(--color-border)]">
<Database className="w-4 h-4 text-[var(--color-text-secondary)]" />
<h3 className="font-semibold text-[var(--color-text-primary)]">Data Model</h3>
<span className="text-xs text-[var(--color-text-tertiary)] ml-auto">
<div className="flex items-center gap-2 pb-2 mb-2 border-b border-[var(--color-a2ui-border)]">
<Database className="w-4 h-4 text-[var(--color-a2ui-text-secondary)]" />
<h3 className="font-semibold text-[var(--color-a2ui-text-primary)]">Data Model</h3>
<span className="text-xs text-[var(--color-a2ui-text-tertiary)] ml-auto">
{surfaceStates.size} surface{surfaceStates.size !== 1 ? 's' : ''}
</span>
</div>

<div className="space-y-3">
{Array.from(surfaceStates.entries()).map(([surfaceId, state]) => (
<div key={surfaceId} className="space-y-1">
<div className="font-mono text-sm font-semibold text-[var(--color-accent)] px-2">
<div className="font-mono text-sm font-semibold text-[var(--color-a2ui-accent)] px-2">
{surfaceId}
</div>
{renderObject(state, 1)}
Expand Down
6 changes: 5 additions & 1 deletion packages/example/src/components/LivePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ class ErrorBoundary extends Component<{ children: React.ReactNode }, LivePreview
}
}

export const LivePreview = memo(function LivePreview({ messages, surfaceId, onAction }: LivePreviewProps) {
export const LivePreview = memo(function LivePreview({
messages,
surfaceId,
onAction,
}: LivePreviewProps) {
// Extract surface ID from messages if not provided (support both v0.8 and v0.9)
const beginMsg = messages.find((m) => 'beginRendering' in m || 'createSurface' in m)
let activeSurfaceId = surfaceId || 'preview'
Expand Down
Loading