Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- '@constructive-io/ui@*'
- '@constructive-io/data@*'
- '@constructive-io/sheets@*'
- '@constructive-io/command-palette@*'
- '@constructive-io/schema-builder@*'
pull_request:
branches: [main]
Expand Down Expand Up @@ -44,6 +45,11 @@ jobs:
- name: Build shadcn registry
run: pnpm build:registry

- name: Verify migration registry installs
env:
SMOKE_CASE: org-chart,storage-browser,sheets,schema-builder,command-palette
run: pnpm --filter @constructive-io/registry smoke:install

packages:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

This public monorepo owns the Constructive Blocks documentation, the
`@constructive` shadcn registry, and the `@constructive-io/ui`,
`@constructive-io/data`, and `@constructive-io/sheets` packages. The local
schema-builder package remains in the workspace but is not part of the public
registry.
`@constructive-io/data`, `@constructive-io/sheets`,
`@constructive-io/command-palette`, and `@constructive-io/schema-builder`
packages. Schema Builder is also distributed as editable source through the
public registry and binds to each host through an explicit adapter.

## Invariants

- Canonical public source lives in `apps/blocks`, `packages/ui`,
`packages/data`, and `packages/sheets`; do not edit generated registry output.
`packages/data`, `packages/sheets`, `packages/command-palette`, and
`packages/schema-builder`; do not edit generated registry output.
- Keep the registry collision-free and `@constructive`-namespaced.
- Preserve the seven feature-pack manifests and four experimental preset
profiles installed under `.constructive/feature-packs`.
- Preserve the reviewed feature-pack manifests and preset profiles installed
under `.constructive/feature-packs`.
- Never auto-discover or mutate sibling repositories from flow tooling.
- Keep normal CI independent of generated SDKs and live endpoints.
- Never add automated npm publishing; publish verified tarballs manually.
Expand Down
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

This public monorepo owns the Constructive Blocks documentation, the
`@constructive` shadcn registry, and the `@constructive-io/ui`,
`@constructive-io/data`, and `@constructive-io/sheets` packages. The local
schema-builder package remains available for development but is delisted from
the public registry.
`@constructive-io/data`, `@constructive-io/sheets`,
`@constructive-io/command-palette`, and `@constructive-io/schema-builder`
packages. Schema Builder is also distributed as editable source through the
public registry and binds to each host through an explicit adapter.

## Invariants

- `apps/blocks`, `packages/ui`, `packages/data`, and `packages/sheets` are
canonical public source trees; generated registry output is never edited.
- `apps/blocks`, `packages/ui`, `packages/data`, `packages/sheets`,
`packages/command-palette`, and `packages/schema-builder` are canonical public
source trees; generated registry output is never edited.
- The combined registry must remain collision-free and every install command
must use the `@constructive` namespace.
- Feature-pack and preset manifests installed under
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ component documentation, and published React packages.
- `packages/ui` — the `@constructive-io/ui` npm package and UI registry source.
- `packages/data` — runtime GraphQL generation and the strict July 2026 `_meta` contract.
- `packages/sheets` — the metadata-driven application CRUD grid.
- `packages/schema-builder` — retained platform/operator tooling that is delisted from the public registry.
- `packages/command-palette` — the headless command registry, shortcuts, workflows, and background-task engine.
- `packages/schema-builder` — the source-installable Schema Builder, its host-adapter contract, and its npm package.

The documentation site is published at
<https://constructive-io.github.io/blocks/>. Registry JSON is served from
Expand All @@ -21,7 +22,7 @@ expect an existing shadcn project; initialize one if the application does not
already contain `components.json`:

```bash
pnpm dlx shadcn@4.13.1 init
pnpm dlx shadcn@latest init
```

Keep the generated alias configuration, then map the public registry in
Expand All @@ -43,10 +44,11 @@ Then choose the smallest surface that owns the workflow you need:
| Backend-aligned official composition | `preset-auth-hardened`, `preset-b2b-storage`, or `preset-full` |
| Custom Console Kit composition | `console-kit-core` plus selected `console-module-*` items |
| Provider-neutral view with host-owned data and actions | `feature-pack-*` |
| Application command center with editable presentation | `command-palette` |
| Reusable primitive | npm subpath or namespaced primitive registry item |

```bash
pnpm dlx shadcn@4.13.1 add @constructive/console-kit-nextjs
pnpm dlx shadcn@latest add @constructive/console-kit-nextjs
```

Render the installed umbrella with a secret-free tenant descriptor returned by
Expand Down Expand Up @@ -85,13 +87,14 @@ source through the same namespace:

```bash
pnpm add @constructive-io/ui
pnpm dlx shadcn@4.13.1 add @constructive/button
pnpm dlx shadcn@latest add @constructive/button
```

The UI package exposes its Tailwind foundation at
`@constructive-io/ui/globals.css`. Registry installs copy the required UI
source and Constructive theme into the consumer and do not install the npm
package. Registry consumers require shadcn CLI 4.13.1 or newer.
package. Registry consumers should invoke the current shadcn CLI through
`shadcn@latest`.

## Development

Expand Down
2 changes: 1 addition & 1 deletion apps/blocks/e2e/docs.interaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ test('documentation order, anchors, and shared install/source mode remain synchr

const install = page.locator('#installation');
await install.getByRole('tab', { name: 'registry' }).click();
await expect(install).toContainText('pnpm dlx shadcn@4.13.1 add @constructive/select');
await expect(install).toContainText('pnpm dlx shadcn@latest add @constructive/select');
await expect(install).toContainText("from '@/components/ui/select'");
await expect(install).toContainText('The npm package is not required.');

Expand Down
127 changes: 127 additions & 0 deletions apps/blocks/e2e/mobile.interaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import {
} from '@playwright/test';

const billingRoute = (name: string) => `/blocks/blocks/billing/${name}/`;
const applicationRoute = (name: string) => `/blocks/blocks/${name}/`;

function billingPreviewFrame(page: Page): FrameLocator {
return page.frameLocator(
'[data-slot="billing-showcase-preview"] iframe[title$="live preview"]',
);
}

function applicationPreviewFrame(page: Page): FrameLocator {
return page.frameLocator(
'[data-slot="application-block-showcase-preview"] iframe[title$="live preview"]',
);
}

async function expectTouchTargets(targets: Locator) {
const targetCount = await targets.count();
expect(targetCount).toBeGreaterThan(0);
Expand Down Expand Up @@ -173,3 +180,123 @@ test('mobile billing controls expose 44px touch targets and switch account conte
),
);
});

test('mobile application blocks keep their primary workflows inside the viewport', async ({
page,
}) => {
await test.step('Org Chart fits every node and action target', async () => {
await page.goto(applicationRoute('org-chart'), { waitUntil: 'networkidle' });
expect(await page.evaluate(() => window.scrollY)).toBe(0);
const frame = applicationPreviewFrame(page);
const nodes = frame.locator('.react-flow__node');
await expect(nodes).toHaveCount(5);

await expect
.poll(() =>
nodes.evaluateAll((elements) =>
elements.every((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= window.innerWidth;
}),
),
)
.toBe(true);

const actions = frame.getByRole('button', { name: /^Actions for / });
await expect(actions).toHaveCount(5);
expect(
await actions.evaluateAll((elements) =>
elements.every((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= window.innerWidth;
}),
),
).toBe(true);

for (const person of ['Maya Chen', 'Theo Brooks']) {
await frame
.getByRole('button', { name: new RegExp(`^Actions for ${person}`) })
.click();
const menu = frame.getByRole('menu');
await expect(menu).toBeVisible();
expect(
await menu.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= window.innerWidth;
}),
).toBe(true);
await page.keyboard.press('Escape');
await expect(menu).toBeHidden();
}

const preview = page.locator(
'[data-slot="application-block-showcase-preview"]',
);
await preview
.getByRole('button', { name: 'Desktop preview, 1280 pixels' })
.click();
const fullscreenTrigger = preview.getByRole('button', {
name: 'Open full-screen preview',
});
await fullscreenTrigger.click();
const dialog = page.getByRole('dialog', { name: 'Org Chart preview' });
const frameContainer = dialog.locator(
'[data-slot="application-block-preview-frame"]',
);
const fullscreenFrame = dialog.locator('iframe');
await expect
.poll(async () => Number(await frameContainer.getAttribute('data-preview-scale')))
.toBeLessThan(1);
expect(
await fullscreenFrame.evaluate((element) => {
const frameBounds = element.getBoundingClientRect();
const containerBounds = element.parentElement!.getBoundingClientRect();
return frameBounds.width <= containerBounds.width + 1;
}),
).toBe(true);
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(fullscreenTrigger).toBeFocused();
});

await test.step('Storage opens object details from the keyboard', async () => {
await page.goto(applicationRoute('storage-browser'), {
waitUntil: 'networkidle',
});
const frame = applicationPreviewFrame(page);
const objectLink = frame.getByRole('button', {
name: 'Open details for launch-cover.png',
});
await objectLink.focus();
await objectLink.press('Enter');
await expect(frame.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(frame.getByRole('dialog')).toBeHidden();
await expect(objectLink).toBeFocused();
});

await test.step('Schema Builder create cards do not overflow', async () => {
await page.goto(applicationRoute('schema-builder'), {
waitUntil: 'networkidle',
});
expect(await page.evaluate(() => window.scrollY)).toBe(0);
const frame = applicationPreviewFrame(page);
await frame.getByRole('button', { name: 'Create table' }).first().click();
await expect(frame.getByTestId('table-name-input')).not.toBeFocused();
const cards = frame.locator('[data-testid^="policy-card-"]');
await expect(cards.first()).toBeVisible();
expect(
await cards.evaluateAll((elements) =>
elements.every((element) => {
const bounds = element.getBoundingClientRect();
return bounds.left >= 0 && bounds.right <= window.innerWidth;
}),
),
).toBe(true);
expect(
await frame.locator('html').evaluate(
(element) => element.scrollWidth <= element.clientWidth,
),
).toBe(true);
});
});
2 changes: 2 additions & 0 deletions apps/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
"@ai-sdk/openai-compatible": "^3.0.9",
"@ai-sdk/react": "^4.0.27",
"@base-ui/react": "^1.0.0",
"@constructive-io/command-palette": "workspace:*",
"@constructive-io/data": "workspace:*",
"@constructive-io/graphql-types": "^3.4.3",
"@constructive-io/schema-builder": "workspace:*",
"@constructive-io/sheets": "workspace:*",
"@constructive-io/ui": "workspace:*",
"@tanstack/react-form": "^1.27.7",
Expand Down
76 changes: 69 additions & 7 deletions apps/blocks/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,71 @@
}
]
},
{
"name": "command-palette",
"type": "registry:block",
"title": "Command Palette",
"description": "Framework-neutral command discovery with page-scoped registration, structured shortcuts, multi-step flows, and cancellable background tasks.",
"categories": [
"blocks",
"application-shell"
],
"docs": "## Usage\n\nCreate one registry for the application, adapt navigation at the host boundary, and render the palette near the root layout. Pages can register commands while mounted with `usePageCommands`.\n\n```tsx\nimport { createCommandRegistry, kbd } from '@constructive-io/command-palette';\nimport { CommandPalette } from '@/blocks/command-palette/command-palette';\n\nconst registry = createCommandRegistry({\n groups: [{ id: 'navigation', label: 'Navigation', priority: 1 }],\n commands: [{\n id: 'settings',\n label: 'Open settings',\n type: 'navigation',\n group: 'navigation',\n href: '/settings',\n shortcut: kbd(',', 'mod')\n }]\n});\n\n<CommandPalette registry={registry} navigate={(href) => router.push(href)} />;\n```\n\nThe installed block owns presentation and keyboard interaction. The headless package owns command registration, execution, multi-step state, and background-task lifecycle. Route authorization, action permissions, errors, and business workflows remain the host application's responsibility.",
"dependencies": [
"@constructive-io/command-palette@^0.3.0",
"lucide-react",
"motion"
],
"registryDependencies": [
"badge",
"button",
"cn",
"command",
"separator"
],
"files": [
{
"path": "registry/constructive/blocks/command-palette/command-palette.tsx",
"target": "src/blocks/command-palette/command-palette.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/kbd-shortcut.tsx",
"target": "src/blocks/command-palette/kbd-shortcut.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/multi-step/multi-step-view.tsx",
"target": "src/blocks/command-palette/multi-step/multi-step-view.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/multi-step/step-indicator.tsx",
"target": "src/blocks/command-palette/multi-step/step-indicator.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/background/background-task-stack.tsx",
"target": "src/blocks/command-palette/background/background-task-stack.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/background/inline-task-bar.tsx",
"target": "src/blocks/command-palette/background/inline-task-bar.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/background/task-card.tsx",
"target": "src/blocks/command-palette/background/task-card.tsx",
"type": "registry:component"
},
{
"path": "registry/constructive/blocks/command-palette/background/task-icons.tsx",
"target": "src/blocks/command-palette/background/task-icons.tsx",
"type": "registry:component"
}
]
},
{
"name": "pack-foundation",
"type": "registry:lib",
Expand Down Expand Up @@ -371,7 +436,7 @@
"description": "The leaf-independent Console Kit shell, runtime, discovery, and single modular Zustand store.",
"docs": "`console-kit-core` installed the shell, runtime, semantic routing, callback boundary, and one per-instance modular Zustand store. Core intentionally includes no feature view, so add selected `console-module-*` items.\n\nDegraded states: explicit endpoint, current `_meta`, introspection, capability, and adapter evidence fail closed independently; installation never grants authority.\n\nGuide: https://constructive-io.github.io/blocks/blocks/console-kit/",
"dependencies": [
"@constructive-io/data",
"@constructive-io/data@^0.4.0",
"@tanstack/react-query",
"graphql",
"lucide-react",
Expand Down Expand Up @@ -501,15 +566,12 @@
"description": "A current-_meta-only application data explorer with application-table filtering and spreadsheet CRUD.",
"docs": "`feature-pack-data` installed a provider-neutral view and `.constructive/feature-packs/data.json`. Import from `@/blocks/feature-packs/data/data-feature-pack`. The host must supply the view resource, policy, and actions; add `console-module-data` when Console Kit should own discovery and Constructive integration.\n\nDegraded states: The host owns Sheets state and endpoint binding; incompatible metadata stays explicit, and PostgreSQL privileges and RLS decide every query and mutation.\n\nGuide: https://constructive-io.github.io/blocks/blocks/features/data/",
"dependencies": [
"@constructive-io/data",
"@constructive-io/sheets",
"@constructive-io/data@^0.4.0",
"lucide-react"
],
"css": {
"@import '@constructive-io/sheets/styles.css'": {}
},
"registryDependencies": [
"pack-foundation"
"pack-foundation",
"sheets"
],
"files": [
{
Expand Down
Loading