From f61431bb0547c605e9c4e6b16931460abe47cf7e Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Sat, 4 Jul 2026 03:16:19 +0300 Subject: [PATCH] feat(ai-autopilot): compose vike-themes/vike-layouts for styling + app shell (#192) Add a vike-shell-composer persona to vikeExtensionPersonas so the composed build declares a theme (defineTheme + extends themesExt: system dark mode, picker, CSS-var contract) and picks a vike-layouts shell (centered/topbar/ sidebar + logo/nav slots) instead of hand-writing a CSS design system and a layout/nav shell. Notes the one-line vike-toolbar install. No runtime change. Closes #192. --- .../compose-vike-themes-layouts-shell.md | 7 ++ .../ai-autopilot/src/personas/library.test.ts | 27 ++++++- packages/ai-autopilot/src/personas/library.ts | 72 +++++++++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 .changeset/compose-vike-themes-layouts-shell.md diff --git a/.changeset/compose-vike-themes-layouts-shell.md b/.changeset/compose-vike-themes-layouts-shell.md new file mode 100644 index 0000000..dbf35eb --- /dev/null +++ b/.changeset/compose-vike-themes-layouts-shell.md @@ -0,0 +1,7 @@ +--- +'@gemstack/ai-autopilot': minor +--- + +Compose vike-themes / vike-layouts for styling and the app shell instead of hand-rolling CSS + nav + +After auth, data, and the CRUD/admin UI, the remaining big hand-rolled surface is styling and the app shell: an agent writes its own CSS design system, dark-mode toggle, and layout/nav chrome on every build, and that fresh CSS is the root of the loop's over-polish churn. The new `vike-shell-composer` persona (wired into `vikeExtensionPersonas`) teaches the agent to declare a brand with `defineTheme` and `extends: [themesExt]` (flash-free system dark mode, a picker, and a CSS-variable contract to style against) instead of hand-writing a color system, and to pick a shell with `vike-layouts` (`layout: 'centered' | 'topbar' | 'sidebar'` plus `logo` / cumulative `nav` slots) instead of a hand-written topbar/sidebar. It also notes the one-line `vike-toolbar` install that gives the theme/locale controls a home. No runtime change; the agent stays a black box. Part of #186. Closes #192. diff --git a/packages/ai-autopilot/src/personas/library.test.ts b/packages/ai-autopilot/src/personas/library.test.ts index f2a0c61..c3185a0 100644 --- a/packages/ai-autopilot/src/personas/library.test.ts +++ b/packages/ai-autopilot/src/personas/library.test.ts @@ -7,6 +7,7 @@ import { vikeCrudComposer, vikeDataModeler, vikeExtensionPersonas, + vikeShellComposer, } from './library.js' describe('vike extension personas', () => { @@ -57,9 +58,31 @@ describe('vike extension personas', () => { assert.match(text, /ejectView\(view, \{ framework \}\)/) }) - it('vikeExtensionPersonas composes data + auth + crud (no Prisma, no hand-rolled auth/UI)', () => { + it('vikeShellComposer teaches composing vike-themes/vike-layouts, not hand-rolled CSS + shell', () => { + assert.equal(vikeShellComposer.name, 'vike-shell-composer') + const text = personaInstructions(vikeShellComposer) + // Styling comes from vike-themes (a theme + CSS vars), not a hand-written design system. + assert.match(text, /defineTheme/) + assert.match(text, /vike-themes\/react/) + assert.match(text, /appearance/) + assert.match(text, /var\(--primary\)/) + // The app shell comes from vike-layouts, not a hand-written topbar/sidebar. + assert.match(text, /vike-layouts\/react/) + assert.match(text, /'centered'/) + assert.match(text, /do NOT write your own\s*\n?\s*color system/i) + // The toolbar is an install-and-forget one-liner. + assert.match(text, /vike-toolbar\/react/) + }) + + it('vikeExtensionPersonas composes data + auth + crud + shell (no Prisma, no hand-rolled auth/UI/CSS)', () => { const names = vikeExtensionPersonas.map(p => p.name) - assert.deepEqual(names, ['vike-data-modeler', 'vike-auth-composer', 'vike-crud-composer', 'ui-intent-designer']) + assert.deepEqual(names, [ + 'vike-data-modeler', + 'vike-auth-composer', + 'vike-crud-composer', + 'vike-shell-composer', + 'ui-intent-designer', + ]) assert.ok(vikeExtensionPersonas.includes(uiIntentDesigner)) }) }) diff --git a/packages/ai-autopilot/src/personas/library.ts b/packages/ai-autopilot/src/personas/library.ts index b78b300..7b39341 100644 --- a/packages/ai-autopilot/src/personas/library.ts +++ b/packages/ai-autopilot/src/personas/library.ts @@ -341,6 +341,67 @@ Customization ladder — start generated, refine only where reality demands it: it last, not by hand-rolling screens from the start.`, }) +/** + * Composes `vike-themes` (styling) + `vike-layouts` (app shell) instead of + * hand-rolling a CSS design system and a layout/nav shell. Styling is the most + * over-polished AI surface — freshly-written CSS is infinitely pickable, which is + * the root of the loop's over-polish churn — and the app shell is re-invented on + * every build. Both are declarations here. Opt-in, in-workspace only (the packages + * resolve inside the vike-data workspace) — see `vikeExtensionPersonas`. + */ +export const vikeShellComposer: Persona = definePersona({ + name: 'vike-shell-composer', + role: 'Composes vike-themes + vike-layouts for styling and the app shell, not hand-rolled CSS and nav', + appliesTo: ['vike-themes', 'vike-layouts', 'vike-toolbar'], + systemPrompt: `You compose vike-themes for styling and vike-layouts for the app shell, instead +of hand-writing a CSS design system, a dark-mode toggle, and a layout/nav shell. +Freshly-generated CSS is the most over-polished surface there is (the review loop +always finds something to pick at), and the shell is re-invented on every build. +Both are a declaration here, and both render through the same vike-blocks system +the crud screens use. + +Styling = vike-themes, NOT hand-written CSS: +- Define a brand and extend the theming core; a single \`primary\` expands into the + full color ramp, and you get flash-free system dark mode plus a theme picker: + \`\`\`js + import themesExt from 'vike-themes/react' + import { defineTheme } from 'vike-themes' + const acme = defineTheme({ + name: 'acme', radius: '4px', + light: { bg: '#fffdf7', text: '#2a2016', primary: '#3b82f6' }, + dark: { bg: '#1a140d', text: '#f3ead9', primary: '#e0772a' }, + }) + // +config.js: export default { extends: [themesExt], appearance: 'system', theme: 'acme', themes: [acme] } + \`\`\` +- \`appearance\` is \`'system'\` (follows the OS, no flash) | \`'light'\` | \`'dark'\`. + \`themes\` is cumulative — built-ins and theme packages compose in. +- Style AGAINST the theme's CSS variables (\`var(--bg)\`, \`var(--text)\`, + \`var(--primary)\`, etc.); do NOT hard-code colors, and do NOT write your own + color system or dark-mode switch. That is what the theme owns. + +App shell = vike-layouts, NOT a hand-written layout/nav: +- Extend the layouts core, pick a shell, and fill its slots: + \`\`\`js + import layoutsExt from 'vike-layouts/react' + // +config.js: export default { + // extends: [layoutsExt], layout: 'topbar', logo: '◆ Acme', + // nav: [{ label: 'Home', href: '/' }, { label: 'Admin', href: '/admin' }], + // } + \`\`\` +- \`layout\` is \`'centered'\` (public pages) | \`'topbar'\` | \`'sidebar'\`, and a page + can override the app default. Slots (\`logo\`, the cumulative \`nav\`, footer, user + menu) are config; a shell renders only the slots it declares. Since the shells + are vike-blocks \`layout\` variants, this is the same block system the crud + screens render through. Do NOT hand-write a topbar/sidebar or a nav component. + +Toolbar (one line, not its own concern): \`extends: [toolbarExt]\` from +\`vike-toolbar/react\` gives a settings popover a home; installed extensions (the +theme picker, a locale switcher) populate it automatically. Install it and move on. + +Reach for a bespoke component only for a genuinely custom region, and even then +style it with the theme's CSS variables so it tracks light/dark and the brand.`, +}) + /** * The framework-neutral personas shared by every preset — the data layer and the * intent-based UI guardrail apply the same whether the app is on Vike or Next. @@ -354,15 +415,18 @@ export const sharedPersonas: readonly Persona[] = Object.freeze([ /** * The opt-in vike-extension stack: compose `vike-auth` for authentication, the * universal-orm data layer for domain data (both ride one registered adapter), and - * `vike-crud` / `vike-admin` for the CRUD/admin UI derived from the schema — instead - * of hand-rolling auth, hand-installing an ORM, or hand-writing list/record/form - * screens. Swap this in for {@link sharedPersonas} when composing extensions (Vike - * only; the extensions currently resolve inside the vike-data workspace). + * `vike-crud` / `vike-admin` for the CRUD/admin UI derived from the schema, and + * `vike-themes` / `vike-layouts` for styling and the app shell — instead of + * hand-rolling auth, hand-installing an ORM, or hand-writing list/record/form + * screens, a CSS design system, or a layout/nav shell. Swap this in for + * {@link sharedPersonas} when composing extensions (Vike only; the extensions + * currently resolve inside the vike-data workspace). */ export const vikeExtensionPersonas: readonly Persona[] = Object.freeze([ vikeDataModeler, vikeAuthComposer, vikeCrudComposer, + vikeShellComposer, uiIntentDesigner, ])