Pages — Constructor (web)
Acest conținut nu este încă disponibil în limba selectată.
Instrucțiuni pentru agenți AI și autori: cum se creează, modifică și șterg paginile unui proiect în Constructor.
Surse de adevăr (nu inventa API):
- Tipuri:
GdsPage,GdsProject,MAX_PAGES,slugifyPageTitle—projects/gds-core/src/lib/blocks.ts - Factory:
makePage,newPageId—projects/constructor/src/app/state/document.factory.ts - Mutări:
DocStore—projects/constructor/src/app/state/doc-store.ts - UI taburi:
projects/constructor/src/app/builder/tabs.component.ts
Locație:
docs/components/pages.md(lângă catalogul de componente șiflutter.md). Nu există încă un ghidglobal-stylessibling; dacă apare, menține același folder.
1. Ce este o pagină
Section titled “1. Ce este o pagină”O pagină (GdsPage) este o unitate web independentă în proiectul Constructor:
- titlu (etichetă tab +
<title>la export) - slug (segment de rută / fișier HTML, ex.
about→about.html) - stivă proprie de
blocks - sidenav opțional doar pe acea pagină (
null= fără shell de navigare)
Paginiile nu partajează sidenav sau blocuri între ele. Tema / limba / accentul trăiesc în GdsProject.meta (global pe proiect).
Pages vs screens (nu confunda)
Section titled “Pages vs screens (nu confunda)”| Pages (web) | Screens (mobil) | |
|---|---|---|
| Tip | GdsPage | GdsScreen |
| Array | project.pages | project.screens |
| Sidenav | da (sidenav: SidenavData | null) | nu |
| Limită | MAX_PAGES = 12 | MAX_SCREENS = 12 |
| UI | taburi Pages (platformă web) | taburi Screens (platformă mobile) |
| Surface DocStore | editSurface === 'page' | editSurface === 'screen' |
Canvas-ul editează o singură surface odată (pagina/ecranul activ). Comutarea web ↔ mobile: DocStore.setPlatform('web' \| 'mobile').
Când un page workflow e activ (project.workflows + activeWorkflowId), stage-ul poate arăta mai multe artboard-uri (câte una per pagină din workflow); doar pagina activă e editabilă.
Chat AI: screens au paritate de acțiuni cu pages (select_screen / add_screen / rename_screen / remove_screen / duplicate_screen); add_block acceptă screenId (nu împreună cu pageId).
2. Forma datelor — GdsPage
Section titled “2. Forma datelor — GdsPage”interface GdsPage { id: string; title: string; slug?: string; // omit pe proiecte legacy → derivat din title sidenav: SidenavData | null; // null = fără sidenav sidenavLayout?: BlockLayout; // plasare pe board (ca layout-ul unui block) sidenavLayouts?: Partial<Record<NarrowViewport, BlockLayout>>; sidenavChrome?: BlockChrome; // radius / border / fill pe frame-ul sidenav blocks: Block[];}| Câmp | Obligatoriu | Note |
|---|---|---|
id | da | Unic în proiect. Generat de newPageId() → p{n}-{random} |
title | da | Etichetă tab; nu poate fi gol la rename / update meta |
slug | opțional | slugifyPageTitle(title) la creare; export: about → about.html |
sidenav | da (null OK) | Per-pagină; vezi gds-sidenav.md |
sidenavLayout / sidenavLayouts / sidenavChrome | opțional | Doar dacă există sidenav |
blocks | da (poate fi []) | Conținutul canvas-ului pentru pagina respectivă |
Helper factory (document.factory.ts):
makePage(title, blocks = [], sidenav = null): GdsPage// → { id: newPageId(), title, slug: slugifyPageTitle(title), sidenav, blocks }Slug (slugifyPageTitle din gds-core): normalizează diacritice → ASCII lowercase, înlocuiește non-alfanumerice cu -. Ex.: „Pagina principală” → pagina-principala. Fallback implicit: 'page'.
3. Unde stau pe proiect — GdsProject.pages
Section titled “3. Unde stau pe proiect — GdsProject.pages”interface GdsProject { version: 2; meta: { /* theme, lang, … */ deletedPageIds?: string[] }; pages: GdsPage[]; // întotdeauna ≥ 1 după normalizare screens?: GdsScreen[]; // mobil — paralel, nu înlocuiește pages}Pagina activă (canvas)
Section titled “Pagina activă (canvas)”În DocStore:
activePageId— id-ul paginii deschiseactivePage—pages.find(p => p.id === activePageId) ?? pages[0]doc— proiecție legacyGdsMockDocdin pagina (sau screen-ul) activ(ă):blocks+sidenavale acelei paginiselectPage(id)— seteazăactivePageId,editSurface = 'page', deselectează blocul
Regulă agent: mutările de conținut (blocuri, sidenav) afectează doar pagina activă. Nu șterge / rescrie alte intrări din pages când editezi conținutul unei pagini.
4. Operații DocStore
Section titled “4. Operații DocStore”CREATE — addPage()
Section titled “CREATE — addPage()”addPage(): GdsPage | null| Pas | Comportament real |
|---|---|
| Limită | canAddPage ⇒ pages.length < MAX_PAGES (12). Dacă e plin → null |
| Titlu | lang === 'ro' → Pagină {n} ; altfel Page {n} (n = length + 1) |
| Conținut | makePage(title, []) — fără blocks, fără sidenav |
| Activare | setează pagina nouă ca activă, editSurface = 'page', selection = null |
| Persist | persistNow() (vizibil imediat altor taburi Constructor) |
Duplicare (opțional): duplicatePage(id) — copie cu titlu … (copie|copy), slug nou, sidenav + layouts + chrome clone, blocks cu id-uri proaspete; inserată imediat după sursă; aceeași limită MAX_PAGES. UI: buton Duplică în modalul ⋯ Setări pagină.
Screens (mobil): duplicateScreen(id) — același pattern (titlu … (copie|copy), blocks cu id-uri noi, inserat după sursă, activează copia, MAX_SCREENS). UI: ⋯ Setări ecran → Duplică.
Nu inventa câmpuri pe pagină. Pentru sidenav pe pagina nouă: după addPage / selectPage, folosește addSidenav() / addPreparedSidenav(...) pe pagina activă (sau setează sidenav pe obiectul paginii dacă construiești JSON direct).
MODIFY
Section titled “MODIFY”| Intenție | API | Note |
|---|---|---|
| Doar titlu | renamePage(id, title) | trim; gol → no-op. Slug: dacă slug-ul curent era auto-derivat din vechiul titlu, se regenerează; dacă userul l-a personalizat (slug !== slugify(oldTitle)), slug-ul rămâne |
| Titlu + slug | updatePageMeta(id, title, slug) | Din modalul de setări al tabului. Titlu gol → no-op. Slug: slugifyPageTitle(slug.trim() || title) |
| Ordine taburi | movePage(id, -1|1) / movePageTo(id, index) | Reordonare; nu schimbă conținutul |
| Conținut (blocks) | mutări pe pagina activă (addBlock, update block, etc.) | Doar activePage.blocks |
| Sidenav | addSidenav / setSidenav / removeSidenav / … | Doar pe pagina activă; pe editSurface === 'screen' → no-op |
UI: click pe tab → selectPage; setări tab (title + slug) → updatePageMeta; drag tab → movePageTo.
DELETE — removePage(id)
Section titled “DELETE — removePage(id)”removePage(id: string): void| Regulă | Comportament |
|---|---|
| Ultima pagină | refuză dacă pages.length <= 1 — proiectul păstrează mereu ≥1 pagină |
| Soft-delete / tombstone | id-ul intră în meta.deletedPageIds (union, deduplicat) — pe merge library cross-tab, peer-ii nu resuscitează pagina |
| Linkuri | sweepPageLinks: șterge target.pageId / linkuri CTA / auth-gate / topbar logout / sidenav logout care pointează la pagina ștearsă |
| Activă ștearsă | trece pe pagina vecină (index clampat) |
| UI | confirm doar dacă pagina are blocks; apoi removePage |
Merge: mergeProjectPages ține paginile locale lipsă din payload-ul peer doar dacă id-ul nu e în deletedPageIds. Tombstone = ștergere intenționată.
5. Ce NU trebuie făcut
Section titled “5. Ce NU trebuie făcut”- Nu inventa câmpuri pe
GdsPage/GdsProjectîn afara tipurilor dinblocks.ts. - Nu refolosi un
idde pagină existent; foloseștenewPageId()/makePage. - Nu confunda
pagescuscreens— operațiile de pagină nu atingproject.screens. - Nu șterge ultima pagină.
- Nu șterge o pagină din
pagesfără a adăuga id-ul înmeta.deletedPageIds(altfel merge-ul o poate readuce). - Nu șterge / rescrie alte pagini când vrei doar să editezi blocks/sidenav pe una.
- Nu presupune că sidenav-ul se copiază automat pe pagina nouă —
addPagecreeazăsidenav: null(doarduplicatePagecopiază sidenav-ul sursei). - Nu confunda blocul
page-header(gds-page-header) cu o pagină de proiect — e un block de conținut, nu o intrare înproject.pages.
6. Exemple JSON — before / after
Section titled “6. Exemple JSON — before / after”CREATE
Section titled “CREATE”Înainte (pages.length === 1):
{ "version": 2, "meta": { "title": "Demo", "product": "Demo", "accent": "indigo", "lang": "en", "seed": 42 }, "pages": [ { "id": "p1-abc", "title": "Home", "slug": "home", "sidenav": null, "blocks": [] } ]}După addPage() (limba en):
{ "pages": [ { "id": "p1-abc", "title": "Home", "slug": "home", "sidenav": null, "blocks": [] }, { "id": "p2-def", "title": "Page 2", "slug": "page-2", "sidenav": null, "blocks": [] } ]}activePageId → "p2-def".
MODIFY (rename + slug personalizat)
Section titled “MODIFY (rename + slug personalizat)”Înainte: { "id": "p2-def", "title": "Page 2", "slug": "page-2", … }
După updatePageMeta("p2-def", "Rapoarte", "reports"):
{ "id": "p2-def", "title": "Rapoarte", "slug": "reports", "sidenav": null, "blocks": [] }(renamePage("p2-def", "Rapoarte") ar fi regenerat slug-ul la rapoarte dacă slug-ul era încă auto.)
DELETE
Section titled “DELETE”Înainte: două pagini, ștergem p2-def.
După removePage("p2-def"):
{ "meta": { "title": "Demo", "product": "Demo", "accent": "indigo", "lang": "en", "seed": 42, "deletedPageIds": ["p2-def"] }, "pages": [ { "id": "p1-abc", "title": "Home", "slug": "home", "sidenav": null, "blocks": [] } ]}Dacă rămâne o singură pagină, un al doilea removePage pe ea nu face nimic.
7. Relația cu canvas-ul
Section titled “7. Relația cu canvas-ul”- Tabul activ =
activePageId→ canvas-ul editeazăactivePage.blocks(+ sidenav dacăsidenav != null). DocStore.docexpune pagina activă caGdsMockDocpentru inspector / preview pe surface.- O pagină = un canvas (implicit): stage-ul randează artboard-ul paginii active. Paginile separate trăiesc ca taburi jos; click pe tab →
selectPage. - Page workflow (multi-artboard): din ⋯ pe stânga tabstrip → selectezi pagini → Combină în workflow. Se salvează pe
project.workflows[](GdsPageWorkflow:id,title,pageIdsordonate). Canvas-ul arată canvasele acelor pagini una lângă alta; doar pagina activă e editabilă (click pe un artboard sibling →selectPage). - Sidenav per-pagină:
addSidenavpe Home nu apare pe „Page 2”. Fiecare pagină își gestionează shell-ul separat (ca orice componentă). - Itemii de sidenav cu
target.pageIdse marcheazăactivecând pagina deschisă coincide cu ținta (în proiecțiadoc). - Platform toggle Mobile trece pe screens — aceleași taburi UI, alt array (
screens), fără sidenav; workflow-ul de pagini se închide.
8. Prototyping in Preview (flows beyond page hrefs)
Section titled “8. Prototyping in Preview (flows beyond page hrefs)”Constructor Preview is a light prototype mode on top of existing NavTarget fields (pageId / url) — no new block kinds.
| Capability | How |
|---|---|
| Page → page | CTA / sidenav / auth / topbar Prototype target → another page. In Preview, clicks navigate inside the iframe. |
| Back stack | After navigating, use ← Back (or Alt+← in the iframe). Tab preview keeps its own history. |
| Scroll-to-block | Each block frame has id="gds-b-{blockId}". Set Prototype target → URL / #anchor… to #gds-b-… (copy from inspector). Same-page scrolls; page.html#gds-b-… works across pages. |
| Screen ↔ page | Mobile Preview shows the active screen. A pageId link from a screen switches to Web and opens that page. |
Sources: projects/constructor/src/app/export/prototype-preview.ts, Preview boot via injectConstructorPreviewBoot, inspector labels “Prototype target”.
Deferred: hotspot blocks, overlay-as-modal pages, NavTarget → screen id (would need a model change).
9. Agent AI — pagini separate, nu un singur workspace
Section titled “9. Agent AI — pagini separate, nu un singur workspace”Regula de bază pentru chat-ul Constructor: fiecare secțiune cerută de user = o pagină proprie. Nu se îngrămădesc Dashboard + Tranzacții + Setări pe aceeași pagină.
În plus, fiecare pagină web creată de agent trebuie finalizată cu scheletul din
layout.md și checklist-ul din ai-designer-checklist.md:
- aplicație/dashboard:
sidenav? → topbar → page-header → conținut → site-footer?; - public/marketing:
site-nav|brand-bar → hero|page-header → conținut → site-footer?; - auth/login:
brand-bar → auth-gate → site-footer?(fărăpage-header/hero); - Utilizatori:
topbar → page-header „Utilizatori”+actions[{Creează}]→tablefără CTA în toolbar →site-footer?(RULE.LIST_CTA_ON_PAGE_HEADER); - Configurare / retenție:
topbar → page-header „Configurare” → form(centrat OK pe pagina settings-only) →site-footer?.
Un job pe pagină (IA de arhitect)
Section titled “Un job pe pagină (IA de arhitect)”| Sidenav activ | page-header | Conținut permis |
|---|---|---|
| Utilizatori | Utilizatori | tabel utilizatori full-width |
| Configurare | Configurare | formular retenție / setări |
| Alerte | Alerte | sumar + coadă, lățime consistentă |
Interzis (dump IA): sidenav Utilizatori + H1 „Administrare” + tabel utilizatori + card „Retenția jurnalelor” centrat sub tabel. Reguli: RULE.ONE_PAGE_ONE_JOB, RULE.HEADER_MATCHES_NAV, RULE.NO_CROSS_CONCERN_STACK.
hero este titlul unui landing, nu chrome: stă sub navigarea de sus. O pagină nouă
rămâne tehnic goală după add_page, dar agentul nu are voie să încheie task-ul până
nu a adăugat explicit un chrome de deschidere și un bloc cu titlul paginii.
| Pas | Acțiune |
|---|---|
| 1. Planifică | Listează paginile înainte de a scrie blocuri („Dashboard, Tranzacții, Setări”) |
| 2. Creează | add_page o dată per secțiune (titlul = numele secțiunii). Prima pagină existentă se refolosește (rename_page), nu se lasă goală |
| 3. Țintește | pageId pe fiecare add_block / add_sidenav (sau select_page înainte). Fără pageId blocul intră pe pagina activă |
| 4. Umple | O pagină pe rând: structură → chrome → conținut, apoi trecerea la următoarea. Canvas-ul arată un singur board, deci nu se creează paginile „în avans” |
| 5. Leagă | Navigare între pagini prin target: { pageId } pe itemi de sidenav / tabs / CTA — paginile rămân separate, dar conectate |
Semnale de „dump” (agentul primește avertisment în rezultatul tool-ului, fără respingere):
add_blockfărăpageIdcând proiectul are >1 pagină → blocul merge pe pagina activă + avertisment să treacăpageIdexplicit- al doilea
page-header/topbar/site-nav/brand-bar/site-footer/hero/auth-gate/bottom-menupe aceeași pagină → conținutul aparține unei pagini noi - peste 18 blocuri pe o pagină → verifică dacă nu e de fapt o pagină separată
add_page/select_pagecând alte pagini au rămas goale → avertisment „o pagină pe rând”: canvas-ul arată un singur board, deci pagina curentă se termină înainte de a deschide alta (pageFocusWarnings)
Limită hard: MAX_PAGES = 12 — add_page peste limită e respins (și pe server, în shadow workspace, ca să nu divergă de DocStore).
Nu confunda cu: viewport-uri adaptive (layouts.phone|tablet|foldable = aceeași pagină) și screens[] (surface Flutter, nu pagini).
Surse: tools/ai-agent-catalog.mjs (PAGE_RULES), tools/kimi-client.mjs (system prompt + descrieri tool), tools/ai-agent-loop.mjs (pageSeparationWarnings, pageFocusWarnings, SHADOW_MAX_PAGES).
9. Mod orchestrat paralel (multi-page workers)
Section titled “9. Mod orchestrat paralel (multi-page workers)”Pentru brief-uri complexe cu mai multe pagini, Constructor poate rula un orchestrator + câte un worker pe pagină, în loc de bucla serială „o pagină pe rând”.
| Rol | Responsabilități | Interzis |
|---|---|---|
| Orchestrator | set_work_plan, add_page / rename_page (≤ MAX_PAGES), sidenav cu target:{pageId}, set_global_styles / set_lang, page workflows (create_workflow / update_workflow / rename_workflow / set_workflow_pages / select_workflow / remove_workflow) | add_block, fill, layout pe conținut |
Worker (1× pageId) | add_block / update / fill / set_block_layout cu pageId explicit; regulă serială pe componentă; adaptive desktop→tablet→phone | add_page, remove_page, meta global, mutări pe altă pagină, ops pe GdsPageWorkflow |
Protocol:
- Client:
POST /api/ai/chatcumode: 'orchestrate'(sau auto pe brief multi-pagină / redesign pe workflow activ) /POST /api/ai/orchestrate. Trimiteselection.activeWorkflowIdcând un workflow e deschis pe canvas. - Server: run părinte în
ai-run-store+ child runs per worker; evenimente NDJSON etichetate{ workerId, pageId, turn }. - Dacă există un page workflow activ (sau
workflowIdpe body) și userul cere build/redesign pe workflow → workerii sunt scoped doar laworkflow.pageIds(subset-ul din modalul „Pages & workflow”). - Client: aplică
actionslive prinapplyAiActions; UI arată progres per pagină (progress.workers). - Anulare părinte → anulează toți workerii.
- Eșec pe o pagină → celelalte continuă; rezultatul părinte e
degradedcu lista paginilor eșuate.
Concurrency: AI_ORCH_MAX_WORKERS (default 4, hard max 6) — restul stau în coadă. Nu rulează doi workeri pe același pageId. Cap pagini: MAX_PAGES / SHADOW_MAX_PAGES = 12.
pageFocusWarnings: dezactivate în modul orchestrat / pe shadow-ul workerului (skipPageFocusWarnings) — umplerea paralelă a paginilor goale este intenționată.
Context token: workerii primesc catalog compact + snapshot page-scoped (titluri + sidenav pe frați, detalii full doar pe pagina lor).
Surse: tools/ai-orchestrate.mjs, tools/ai-agent-loop.mjs (agentRole, pageScope), tools/api-server.mjs.
10. Page workflows — agent ops
Section titled “10. Page workflows — agent ops”Un page workflow (GdsPageWorkflow) grupează pagini existente pe project.workflows[] pentru multi-artboard pe canvas. Este același model ca modalul Pages & workflow (⋯ pe tabstrip) — nu există un API paralel inventat.
| Op agent | DocStore | Note |
|---|---|---|
create_workflow | createWorkflowFromPages(pageIds, title?, id?) | Ordinea pageIds = artboard left→right; deschide workflow-ul |
update_workflow | renameWorkflow +/sau setWorkflowPages | Cel puțin title sau pageIds |
rename_workflow | renameWorkflow | Titlu gol → reject |
set_workflow_pages | setWorkflowPages | Membership + ordine; ids invalide dropate; listă goală → reject |
select_workflow | setActiveWorkflow | workflowId: null închide multi-artboard |
remove_workflow | removeWorkflow | Șterge doar workflow-ul, nu paginile |
Când:
- User selectează pagini în modal / cere „combină în workflow” / „open as workflow” →
create_workflow(+select_workflowdacă e nevoie). - Modifică setul/ordinea →
set_workflow_pages/update_workflow. - Redesign pe workflow-ul deschis → orchestrate cu scope pe
pageIds(1 sesiune concurentă per canvas).
Nu confunda cu set_work_plan / evenimentele type:'workflow' din chat — acelea sunt checklist-ul de calitate al agentului (faze plan/execute), nu GdsPageWorkflow.
Referințe rapide API
Section titled “Referințe rapide API”| Operație | Metodă | Fișier |
|---|---|---|
| Creează | addPage() | doc-store.ts |
| Duplică | duplicatePage(id) / duplicateScreen(id) | doc-store.ts |
| Selectează | selectPage(id) | doc-store.ts |
| Pagina de pe canvas | activePage / activePageId (un singur board) | doc-store.ts |
| Redenumește | renamePage(id, title) | doc-store.ts |
| Titlu + slug | updatePageMeta(id, title, slug) | doc-store.ts |
| Șterge | removePage(id) | doc-store.ts |
| Reordonare | movePage / movePageTo | doc-store.ts |
| Factory | makePage / newPageId | document.factory.ts |
| Slug | slugifyPageTitle(title) | gds-core blocks.ts |
| Limită | MAX_PAGES (= 12) | gds-core blocks.ts |
| Tombstone | meta.deletedPageIds | GdsProject.meta |
| Page workflow | createWorkflowFromPages / setActiveWorkflow / renameWorkflow / setWorkflowPages / removeWorkflow | doc-store.ts |
| Workflow model | GdsPageWorkflow pe project.workflows | gds-core blocks.ts |
| Agent workflow ops | create_workflow / update_workflow / rename_workflow / set_workflow_pages / select_workflow / remove_workflow | ai-agent-actions.ts + shadow |
| Orchestrate paralel | mode:'orchestrate' / POST /api/ai/orchestrate | ai-orchestrate.mjs |