Skip to content

Pages — Constructor (web)

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 și flutter.md). Nu există încă un ghid global-styles sibling; dacă apare, menține același folder.


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 (web)Screens (mobil)
TipGdsPageGdsScreen
Arrayproject.pagesproject.screens
Sidenavda (sidenav: SidenavData | null)nu
LimităMAX_PAGES = 12MAX_SCREENS = 12
UItaburi Pages (platformă web)taburi Screens (platformă mobile)
Surface DocStoreeditSurface === '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).


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âmpObligatoriuNote
iddaUnic în proiect. Generat de newPageId() → p{n}-{random}
titledaEtichetă tab; nu poate fi gol la rename / update meta
slugopționalslugifyPageTitle(title) la creare; export: about → about.html
sidenavda (null OK)Per-pagină; vezi gds-sidenav.md
sidenavLayout / sidenavLayouts / sidenavChromeopționalDoar dacă există sidenav
blocksda (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
}

În DocStore:

  • activePageId — id-ul paginii deschise
  • activePage — pages.find(p => p.id === activePageId) ?? pages[0]
  • doc — proiecție legacy GdsMockDoc din pagina (sau screen-ul) activ(ă): blocks + sidenav ale acelei pagini
  • selectPage(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.


addPage(): GdsPage | null
PasComportament real
LimităcanAddPage ⇒ pages.length < MAX_PAGES (12). Dacă e plin → null
Titlulang === 'ro' → Pagină {n} ; altfel Page {n} (n = length + 1)
ConținutmakePage(title, []) — fără blocks, fără sidenav
Activaresetează pagina nouă ca activă, editSurface = 'page', selection = null
PersistpersistNow() (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).

IntențieAPINote
Doar titlurenamePage(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 + slugupdatePageMeta(id, title, slug)Din modalul de setări al tabului. Titlu gol → no-op. Slug: slugifyPageTitle(slug.trim() || title)
Ordine taburimovePage(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
SidenavaddSidenav / 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.

removePage(id: string): void
RegulăComportament
Ultima paginărefuză dacă pages.length <= 1 — proiectul păstrează mereu ≥1 pagină
Soft-delete / tombstoneid-ul intră în meta.deletedPageIds (union, deduplicat) — pe merge library cross-tab, peer-ii nu resuscitează pagina
LinkurisweepPageLinks: șterge target.pageId / linkuri CTA / auth-gate / topbar logout / sidenav logout care pointează la pagina ștearsă
Activă ștearsătrece pe pagina vecină (index clampat)
UIconfirm 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ă.


  1. Nu inventa câmpuri pe GdsPage / GdsProject în afara tipurilor din blocks.ts.
  2. Nu refolosi un id de pagină existent; folosește newPageId() / makePage.
  3. Nu confunda pages cu screens — operațiile de pagină nu ating project.screens.
  4. Nu șterge ultima pagină.
  5. Nu șterge o pagină din pages fără a adăuga id-ul în meta.deletedPageIds (altfel merge-ul o poate readuce).
  6. Nu șterge / rescrie alte pagini când vrei doar să editezi blocks/sidenav pe una.
  7. Nu presupune că sidenav-ul se copiază automat pe pagina nouă — addPage creează sidenav: null (doar duplicatePage copiază sidenav-ul sursei).
  8. Nu confunda blocul page-header (gds-page-header) cu o pagină de proiect — e un block de conținut, nu o intrare în project.pages.

Î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".

Î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.)

Î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.


  1. Tabul activ = activePageId → canvas-ul editează activePage.blocks (+ sidenav dacă sidenav != null).
  2. DocStore.doc expune pagina activă ca GdsMockDoc pentru inspector / preview pe surface.
  3. O pagină = un canvas (implicit): stage-ul randează artboard-ul paginii active. Paginile separate trăiesc ca taburi jos; click pe tab → selectPage.
  4. Page workflow (multi-artboard): din ⋯ pe stânga tabstrip → selectezi pagini → Combină în workflow. Se salvează pe project.workflows[] (GdsPageWorkflow: id, title, pageIds ordonate). Canvas-ul arată canvasele acelor pagini una lângă alta; doar pagina activă e editabilă (click pe un artboard sibling → selectPage).
  5. Sidenav per-pagină: addSidenav pe Home nu apare pe „Page 2”. Fiecare pagină își gestionează shell-ul separat (ca orice componentă).
  6. Itemii de sidenav cu target.pageId se marchează active când pagina deschisă coincide cu ținta (în proiecția doc).
  7. 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.

CapabilityHow
Page → pageCTA / sidenav / auth / topbar Prototype target → another page. In Preview, clicks navigate inside the iframe.
Back stackAfter navigating, use ← Back (or Alt+← in the iframe). Tab preview keeps its own history.
Scroll-to-blockEach 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 ↔ pageMobile 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ă}] → table fă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?.
Sidenav activpage-headerConținut permis
UtilizatoriUtilizatoritabel utilizatori full-width
ConfigurareConfigurareformular retenție / setări
AlerteAlertesumar + 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.

PasAcț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ștepageId pe fiecare add_block / add_sidenav (sau select_page înainte). Fără pageId blocul intră pe pagina activă
4. UmpleO 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_block fără pageId când proiectul are >1 pagină → blocul merge pe pagina activă + avertisment să treacă pageId explicit
  • al doilea page-header / topbar / site-nav / brand-bar / site-footer / hero / auth-gate / bottom-menu pe 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_page câ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”.

RolResponsabilitățiInterzis
Orchestratorset_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→phoneadd_page, remove_page, meta global, mutări pe altă pagină, ops pe GdsPageWorkflow

Protocol:

  1. Client: POST /api/ai/chat cu mode: 'orchestrate' (sau auto pe brief multi-pagină / redesign pe workflow activ) / POST /api/ai/orchestrate. Trimite selection.activeWorkflowId când un workflow e deschis pe canvas.
  2. Server: run părinte în ai-run-store + child runs per worker; evenimente NDJSON etichetate { workerId, pageId, turn }.
  3. Dacă există un page workflow activ (sau workflowId pe body) și userul cere build/redesign pe workflow → workerii sunt scoped doar la workflow.pageIds (subset-ul din modalul „Pages & workflow”).
  4. Client: aplică actions live prin applyAiActions; UI arată progres per pagină (progress.workers).
  5. Anulare părinte → anulează toți workerii.
  6. Eșec pe o pagină → celelalte continuă; rezultatul părinte e degraded cu 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.


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 agentDocStoreNote
create_workflowcreateWorkflowFromPages(pageIds, title?, id?)Ordinea pageIds = artboard left→right; deschide workflow-ul
update_workflowrenameWorkflow +/sau setWorkflowPagesCel puțin title sau pageIds
rename_workflowrenameWorkflowTitlu gol → reject
set_workflow_pagessetWorkflowPagesMembership + ordine; ids invalide dropate; listă goală → reject
select_workflowsetActiveWorkflowworkflowId: null închide multi-artboard
remove_workflowremoveWorkflowȘterge doar workflow-ul, nu paginile

Când:

  • User selectează pagini în modal / cere „combină în workflow” / „open as workflow” → create_workflow (+ select_workflow dacă 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.


OperațieMetodăFișier
CreeazăaddPage()doc-store.ts
DuplicăduplicatePage(id) / duplicateScreen(id)doc-store.ts
SelecteazăselectPage(id)doc-store.ts
Pagina de pe canvasactivePage / activePageId (un singur board)doc-store.ts
RedenumeșterenamePage(id, title)doc-store.ts
Titlu + slugupdatePageMeta(id, title, slug)doc-store.ts
ȘtergeremovePage(id)doc-store.ts
ReordonaremovePage / movePageTodoc-store.ts
FactorymakePage / newPageIddocument.factory.ts
SlugslugifyPageTitle(title)gds-core blocks.ts
LimităMAX_PAGES (= 12)gds-core blocks.ts
Tombstonemeta.deletedPageIdsGdsProject.meta
Page workflowcreateWorkflowFromPages / setActiveWorkflow / renameWorkflow / setWorkflowPages / removeWorkflowdoc-store.ts
Workflow modelGdsPageWorkflow pe project.workflowsgds-core blocks.ts
Agent workflow opscreate_workflow / update_workflow / rename_workflow / set_workflow_pages / select_workflow / remove_workflowai-agent-actions.ts + shadow
Orchestrate paralelmode:'orchestrate' / POST /api/ai/orchestrateai-orchestrate.mjs