AI Design Rules — reguli hard pentru agentul de canvas
Sursa de adevăr: tools/ai-design-rules.mjs.
Lista completă este injectată în fiecare tur al modelului (prompt de sistem)
și id-urile sunt raportate de validare / post_apply_observation.
| Marcaj | Ce înseamnă |
|---|---|
[ENFORCED] | Aplicatorul rescrie sau respinge rezultatul — modelul nu poate produce defectul nici dacă ignoră promptul. |
[CHECKED] | Validarea o raportează ca problemă blocantă; bucla nu avansează la pagina următoare. |
[REQUIRED] | Doar instrucțiune de prompt, fără poartă mecanică. |
ORDERING / COMPOSITION
Section titled “ORDERING / COMPOSITION”[ENFORCED]RULE.CHROME_FIRST— Leading chrome MUST be the first block of a web page: exactly one of topbar, site-nav or brand-bar. Nothing may sit above it.[CHECKED]RULE.TITLE_SECOND— Immediately after leading chrome comes exactly one title block: page-header (app/dashboard), hero (public landing), OR auth-gate (auth/login page). Never both page-header and hero. Auth pages use auth-gate as the title — do not also add page-header or hero.[CHECKED]RULE.AUTH_NO_REDUNDANT_HEADER— FORBIDDEN on a page that contains auth-gate: a co-present page-header or hero. Auth-gate already carries logo, title and description. Auth recipe = brand-bar|minimal topbar → centered auth-gate → optional site-footer. Remove the redundant header.[ENFORCED]RULE.FOOTER_TIGHT— site-footer must sit immediately under the last content block (row = contentBottom + 2-cell gutter). FORBIDDEN: orphan whitespace above the footer that makes it float mid-board. Empty board BELOW the footer on a fixed 1080 artboard is normal — the defect is the gap ABOVE.[ENFORCED]RULE.NO_HERO_AT_ROW_0— A hero or page-header MUST NOT start at row 0 unless it is the only block on the page: it always begins below leading chrome.[ENFORCED]RULE.CONTENT_FLOW— Content blocks follow in one strictly increasing vertical flow. A later block MUST NOT have a row smaller than an earlier block already placed on the page.[ENFORCED]RULE.FOOTER_LAST— site-footer and bottom-menu MUST be the LAST blocks of the page, below every content block. If you add content after a footer, the footer moves down — never place content below a footer and never leave a footer mid-stack.[CHECKED]RULE.FOOTER_ONCE— At most one site-footer and one bottom-menu per page.[CHECKED]RULE.SINGLETON— At most one each of topbar, site-nav, brand-bar, page-header, hero, auth-gate, site-footer per page. A second one means the content belongs on a NEW page (add_page).[REQUIRED]RULE.SECTION_HEADER_BEFORE_GRID— A card grid, chart pair or list group is introduced by a section-header directly above it, in the same vertical flow — never beside it.[CHECKED]RULE.NO_TRAILING_EMPTY_PAGE— Never leave a page with zero blocks. Finish the active page before creating or opening the next one.[CHECKED]RULE.ONE_PAGE_ONE_JOB— Each page answers ONE job. FORBIDDEN: stacking unrelated admin concerns on one board (e.g. users table + log-retention form). Split into separate pages that match sidenav items (Utilizatori vs Configurare).[CHECKED]RULE.HEADER_MATCHES_NAV— page-header title MUST match the active sidenav / section job (Utilizatori → „Utilizatori”, not a vague „Administrare” that covers several nav items). Breadcrumb current crumb follows the same label.[CHECKED]RULE.NO_CROSS_CONCERN_STACK— FORBIDDEN: a list/table page that also carries a settings form (retention, archive, configuration). List pages stay list-only; settings/forms live on their own Configurare page. Constrained forms are OK centered only on settings-only pages.[CHECKED]RULE.NO_STACKED_TITLES— FORBIDDEN: page-header H1 immediately followed by table/log-journal/activity whose title repeats or translates the same name (e.g. „Jurnal loguri” + „Log journals”). Insert a useful buffer (kpi-row, filter-chips, alert) OR omit/clear the block title so the page-header owns the heading.[REQUIRED]RULE.LIST_CTA_ON_PAGE_HEADER— On list pages (table under page-header): the primary create/add CTA (Creează / Adaugă / Stație nouă / Vendor nou) MUST live on page-header.actions[] (variant primary, optional icon +). FORBIDDEN: putting that CTA on table.toolbarActionLabel / toolbarActionIcon or using page-header.badge as a substitute for the create button. Table toolbar actions stay for secondary ops (Import, Export, Filtre). Omit table.title when page-header already names the list (RULE.NO_STACKED_TITLES). Recipe: topbar → page-header{title, text?, actions:[{label, variant:‘primary’, icon:’+’}]} → table{title:”, searchable…}.[REQUIRED]RULE.PAGE_PURPOSE— Before composing, name the page job and follow its recipe (dashboard / journal / alerts / users / settings / auth). Sparse dumps of unrelated blocks are not architect quality.
PLACEMENT GEOMETRY (hard numbers)
Section titled “PLACEMENT GEOMETRY (hard numbers)”[ENFORCED]RULE.GRID_CELL— One grid cell = 8px. Every col/row/colSpan/rowSpan is an integer count of cells. The desktop artboard is 1920×1080px = 240×135 cells.[ENFORCED]RULE.TWELVE_COLUMNS— Content uses a 12-column structural grid. Legal spans are 12, 8, 6, 4, 3 only. Prefer layout:{span:12|8|6|4|3, row:“next”} and let the engine compute cells.[ENFORCED]RULE.CONTENT_PADDING— Content is inset 4 cells (32px) from the left and right board edges; on a page with a docked sidenav the inset starts after the rail. Chrome (topbar / site-nav / brand-bar / site-footer / bottom-menu) spans edge to edge of the area next to the rail with zero inset.[ENFORCED]RULE.SHARED_EDGES— Every full row shares the same left edge AND the same right edge as every other row. Column starts of a 3-up card row on a 1920 board with a 240-cell rail-less area are the only ones the engine emits — do not hand-compute alternatives.[ENFORCED]RULE.ROW_GAP— The vertical gap between two consecutive stacked blocks is EXACTLY 2 cells (16px). Not 3, not 10. row(next) = bottom(previous) + 2.[ENFORCED]RULE.NO_OVERLAP— FORBIDDEN: two block rectangles that intersect. Overlap is repaired or rejected before it reaches the canvas; never leave an overlap “for later”.[ENFORCED]RULE.MAX_GAP— FORBIDDEN: an orphan vertical gap larger than 6 cells (48px) beyond the normal 2-cell gutter between consecutive content blocks. A row value that jumps below the current stack bottom is clamped to bottom + 2.[ENFORCED]RULE.NO_ROW_JUMP— FORBIDDEN: inventing large absolute row values (row: 116, 138, 146…) to “make space”. Omit layout, or use row:“next”. An absolute row is a hint only and is snapped, packed or discarded.[CHECKED]RULE.DEAD_ROW— FORBIDDEN: a content row that leaves more than 2 cells — never ≥25% of the content width — unused on the right. Rows must sum to 12: 12, 8+4, 6+6, 4+4+4. Exception: a lone compact widget (wallet-card, card, toggle-card, progress-*, deadline-list, donut) stays at its natural span and is centered — that unused margin is intentional, not a dead row.[ENFORCED]RULE.MAX_ROW_ITEMS— At most 3 panels share one desktop row. A fourth panel opens a new row.[ENFORCED]RULE.IN_BOUNDS— FORBIDDEN: a rectangle that leaves the artboard (col < 0 or col + colSpan > board columns) or overlaps the sidenav rail.[ENFORCED]RULE.SIDENAV_RAIL— The sidenav rail is a fixed 30-cell (240px) dock at col 0 on desktop, or a full-width top bar. Never resize the rail per block: a changing rail width shifts every content column and is the classic cause of misaligned card rows.[ENFORCED]RULE.LAYOUT_IS_A_HINT— When layout is omitted the engine auto-packs. When layout is provided it is a HINT: it is snapped to the 12-column grid, packed into an open row, clamped for gaps, or rejected if invalid. Do not fight the engine with pixel math.[CHECKED]RULE.ADAPTIVE_VIEWPORTS— Desktop, tablet and phone are ONE shared page — never desktop-only. After placing a non-float block on desktop, also author layouts.tablet and layouts.phone for the SAME id via set_block_layout (viewport:“tablet”|“phone”). Widths: desktop 1920 / tablet 820 / phone 430. Optional foldable (720) may follow tablet. screens[] is Flutter-only, not a responsive substitute.[CHECKED]RULE.NARROW_STACK— On phone (layouts.phone): every content band is a single full-width column — FORBIDDEN side-by-side content peers on the same row. On tablet (layouts.tablet): at most TWO content panels share a row (pairs OK; 3-up desktop cards must stack or become 2+1). KPI/table/list stay full content width on both.
SIZING
Section titled “SIZING”[ENFORCED]RULE.NATURAL_HEIGHT— rowSpan MUST NOT be smaller than the natural height of the block content (per-kind floor, plus rows for table rows / kpi cards). A short frame clips real content.[ENFORCED]RULE.NO_GIANT_FRAME— Never inflate rowSpan to reserve space. A frame taller than ~2.5 screens (240 cells) is a layout error, and an oversized frame leaves a hollow interior.[ENFORCED]RULE.PAIR_SAME_ROW— A pair (8+4 or 6+6) MUST share the same row and be height-equalized to the taller panel. Two panels with different bottoms read as broken even when both rects are legal.[ENFORCED]RULE.ROW_ALIGN_TOP— Side-by-side panels that belong on one row MUST share the same row (top). Staggered tops (“praguri”) are repaired by snapping onto the partner row.[ENFORCED]RULE.ROW_EQUAL_HEIGHT— Side-by-side panels on one row MUST share the same rowSpan (equalized to the taller natural height). Unequal bottoms are a layout defect.[ENFORCED]RULE.CONTENT_WIDTH_CONSISTENCY— Solo full-role content bands stacked vertically MUST share the same left and right content edges. FORBIDDEN: a full-width summary above a narrower left-shifted table, or a centered form island under a full-width list on the same page. Preference: list pages = full content width only; settings-only pages may center a constrained form intentionally; mixed list+settings fails (split pages) rather than centering under a table. Compact widgets (wallet-card / card / …) are NOT full-role bands — see RULE.COMPACT_CARD_WIDTH.[ENFORCED]RULE.COMPACT_CARD_WIDTH— Compact content widgets (wallet-card, card, toggle-card, progress-rings, progress-bars, deadline-list, donut-chart) keep their natural span (typically 4) and are CENTERED when alone on a desktop row. FORBIDDEN: stretching them edge-to-edge across the content area when the payload is a small balance, profile chip, stat, or single card — that reads as a broken empty strip. Pair with an 8-span peer (8+4) or pack 3-up (4+4+4) when a full row is needed. Full-bleed is reserved for full-role bands (page-header, kpi-row, table, log-journal, search, …) and intentionally full lists (activity alone may fill width). Profile recipe: user-profile (constrained span 6 centered) → wallet-card (span 4 centered) — never wallet span 12.[ENFORCED]RULE.NO_INNER_SCROLL— Artboard blocks NEVER show an inner vertical scrollbar. Grow rowSpan to natural height (with comfortable padding) instead. Auth-gate signup needs more rows than signin.[ENFORCED]RULE.DOCKED_SQUARE_EDGES— Docked structural chrome (sidenav, topbar, brand-bar, site-nav, site-footer) uses square outer edges (radius 0) where it meets the artboard or adjacent docked panels. Do not card-ify docked chrome with large corner radius. Floating content cards keep —gds-radius.[ENFORCED]RULE.DENSE_CONTROL_LIST— Dense control lists (log-routing, log-levels, method-list, action-list, toggle-card rows) keep label↔controls as a scannable cluster — no huge mid-row void. Prefer full content width with trailing space after controls; frame tall enough for all rows + bottom padding.[CHECKED]RULE.INFO_DENSITY— Ops pages (alerts, journals, users) need purposeful density: status summary → actionable queue. FORBIDDEN: thin pages with sparse strips, large empty interiors, and a lonely narrow table.[CHECKED]RULE.HERO_HEIGHT— A landing hero needs at least its natural height (≈40 cells / 320px); do not shrink it to a banner strip.[ENFORCED]RULE.TABLE_HEIGHT— A table frame grows with its rows (~3 cells per record plus header/toolbar chrome). Never fit 20 records into an 8-row frame.
DATA / COMPONENTS
Section titled “DATA / COMPONENTS”[CHECKED]RULE.LANG_CONSISTENCY— User-facing titles and labels match project meta.lang. FORBIDDEN: English block titles on a Romanian page (e.g. „Log journals” under „Jurnal loguri”).[ENFORCED]RULE.GLYPH_ICONS— Icons are single monochrome glyph characters from catalog.icons.glyphs (▣ ▦ ☰ ⚙ ◈ ⇄). FORBIDDEN: icon-library names (home, bell, shield, credit-card, lucide:*) — they render as raw text. Omit icon when unsure.[ENFORCED]RULE.NO_DEBUG_TEXT— FORBIDDEN in user-facing copy: debug hashes, sha256/base64 blobs, ids, “ULTIMUL BLOC”, “TODO”, lorem placeholders, or any internal marker.[REQUIRED]RULE.PLAUSIBLE_CONTENT— Copy is plausible domain content in the project language (Romanian when lang=ro): real-sounding names, dates, amounts, statuses. No English filler on a Romanian page.[CHECKED]RULE.NO_INVENTED_FIELDS— Use exactly the keys listed in catalog.fields for that kind. Never invent props, kinds or composite layout components.[REQUIRED]RULE.CARDS_GRID— For a set of cards emit N separate span-4 card blocks in the same flow so the engine packs them 3-up, or one block that already renders a grid (kpi-row). Never hand-place overlapping card frames at improvised columns.[CHECKED]RULE.ACCENT_TOKENS— Colour only via accent/global tokens (indigo|emerald|amber|rose|blue|teal). FORBIDDEN: hardcoded hex in block data.[REQUIRED]RULE.LINKED_PAGES— Navigation targets use target:{pageId:}. Never invent an id from a title.
RESPONSIVE / MULTI-VIEWPORT
Section titled “RESPONSIVE / MULTI-VIEWPORT”[CHECKED]RULE.MULTI_VIEWPORT_CONCURRENT— Adaptive web is ONE GdsPage with shared blocks/data. ALWAYS author desktop + tablet + phone together — never desktop-only. Desktop writes block.layout (1920×1080); tablet writes layouts.tablet (820×1180); phone writes layouts.phone (430×932). screens[] is Flutter-only, not a responsive variant. Finish narrow layouts for the current component before adding the next one.[REQUIRED]RULE.RESPONSIVE_COMPOSITION— Preserve the same reading order across viewports. Desktop uses 12-column pairs (8+4 / 6+6 / 4+4+4). Tablet reduces density and stacks secondary panels; KPI rows become 2×2. Phone is a single full-width column. Sidenav: desktop rail → tablet/foldable icon-rail → phone top app bar. FORBIDDEN: collisions, clip-risk, dead rows, or desktop-width frames left unbroken on narrow viewports.[REQUIRED]RULE.NARROW_LAYOUTS_EXPLICIT— After a desktop placement passes, call set_block_layout on the SAME block ids with viewport=tablet then viewport=phone (or accept engine seed/repair). Prefer layout:{span:12|8|6|4,row:“next”} hints. Never invent a second page or screen to fake responsiveness.
PROCESS (agent cycle)
Section titled “PROCESS (agent cycle)”[ENFORCED]RULE.SMALL_BATCH— Emit at most 2 tool calls per turn about ONE component (select + add|update|fill|layout). A second add_block or a mutation on a different id in the same turn is REJECTED. Never batch two components; finish verify/correct before the next.[ENFORCED]RULE.ONE_PAGE_AT_A_TIME— The canvas shows exactly one board. Finish and verify the active page before you create or open another. Page advancement is refused while the active page has blocking issues.[CHECKED]RULE.OBSERVE_THEN_ACT— Read post_apply_observation after every apply. If status is needs-correction, fix THAT component before adding anything new.[REQUIRED]RULE.CORRECT_WITH_TOOLS— Correct with set_block_layout, update_block, fill_block_fake or remove+replace. Prefer removing an explicit layout over inventing a new absolute row.[CHECKED]RULE.PAGE_ID_EXPLICIT— Once the project has more than one page, set pageId on every add_block / add_sidenav (or select_page first). Otherwise blocks land on page 1.[CHECKED]RULE.FINAL_PAGE_AUDIT— Before leaving a page confirm: order = chrome → title → content → footer; overlaps = 0; dead rows = 0; orphan gaps = 0; no clip-risk; every non-float block has layouts.tablet AND layouts.phone (RULE.ADAPTIVE_VIEWPORTS) with phone single-column / tablet ≤2-up (RULE.NARROW_STACK). Never claim success over failed validation.[REQUIRED]RULE.NO_SILENT_SUCCESS— If a component cannot be made valid within the correction budget, simplify its data or skip it with an explicit warning — do not loop and do not report it as done.
Total: 63 reguli.
Enforcement geometric: projects/gds-core/src/lib/layout-system.ts
(packGdsBlock, repairGdsLayout, validateGdsLayout), oglindit server-side în
tools/ai-agent-loop.mjs (repairShadowPage, validateShadowPage).