Build Page Layout: $ARGUMENTS
SkillDev toolsBuild a page layout using fd-dynamic-page or fdp-dynamic-page — collapsing header, subheader, content area, footer, and optional tabs
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Build Page Layout: $ARGUMENTS skill
What this skill tells your AI
The instructions your AI receives, as published by sap/fundamental-ngx in .claude/skills/build-page-layout/SKILL.md and read by ahel’s review.
If $ARGUMENTS is empty, ask the user: (1) what this page displays, (2) whether it needs tabs, a collapsible subheader, or a floating footer.
Phase 1: Determine Scope
Parse from $ARGUMENTS or ask:
- Project type:
nx(existing NX monorepo library/app) |standalone(fresh Angular CLI app — needs full setup) - Page name (PascalCase, e.g.,
ProductDetail,OrderList) - Base component:
fd-dynamic-page(core, sufficient for most cases) |fdp-dynamic-page(platform — needed only if usingfdp-icon-tab-baror thetabChangeoutput event) - Sections needed (check all that apply):
header— title, subtitle, breadcrumb, global/layout actions (default: yes)subheader— collapsible section below the header with metadata or filterstabs— multiple content sections, each on its own tabfooter— floating action bar (Save / Cancel buttons)fcl— page is a column insidefd-flexible-column-layout
- Header actions: global actions (left-aligned toolbar) and/or layout actions (right-aligned)
Default to fd-dynamic-page with header + one content area unless the user specifies otherwise.
Standalone app setup (skip for NX projects)
If project type is standalone, run these steps before generating the component:
# 1. Create the app (--standalone is accepted but is the default since Angular 19, so it's a no-op)
ng new <app-name> --routing=true --style=scss --skip-git --package-manager=npm
# 2. Resolve the exact @fundamental-ngx version and fundamental-styles peer version first
npm show @fundamental-ngx/core version # → e.g. 0.62.2
npm show @fundamental-ngx/core peerDependencies # → check fundamental-styles exact pin
# 3. Install @fundamental-ngx and peer deps (--legacy-peer-deps is required to resolve Angular CDK peer conflicts)
cd <app-name>
npm install --legacy-peer-deps \
@fundamental-ngx/core@<version> \
@fundamental-ngx/cdk@<version> \
@fundamental-ngx/i18n@<version> \
fundamental-styles@<peer-version> \
"@angular/cdk@^<angular-major>.0.0" \
@sap-theming/theming-base-content
Check the exact fundamental-styles peer version with:
npm show @fundamental-ngx/core peerDependencies
Bundle budget — the default Angular CLI budget (1 MB error) is too small for @fundamental-ngx/core (~2.4 MB initial). Raise both thresholds in angular.json:
{ "type": "initial", "maximumWarning": "2.5MB", "maximumError": "4MB" }
Theming — add the SAP theme files to the angular.json "styles" array. esbuild emits css-syntax-error warnings about JSON metadata embedded in css_variables.css, but these are non-blocking — the styles work correctly.
In angular.json, add to the "styles" array before src/styles.scss:
"styles": [
"node_modules/@sap-theming/theming-base-content/content/Base/baseLib/sap_horizon/css_variables.css",
"node_modules/fundamental-styles/dist/theming/sap_horizon.css",
"src/styles.scss"
]
Do NOT @import the SAP theming CSS in styles.scss — the SCSS bundler cannot process the embedded JSON metadata and will error.
Global styles (src/styles.scss) — import the fundamental-styles CSS files for the sections you are building. Always start with:
@import '@angular/cdk/overlay-prebuilt.css';
@import 'fundamental-styles/dist/dynamic-page.css';
@import 'fundamental-styles/dist/breadcrumb.css';
@import 'fundamental-styles/dist/button.css';
@import 'fundamental-styles/dist/toolbar.css';
@import 'fundamental-styles/dist/icon.css';
@import 'fundamental-styles/dist/link.css';
@import 'fundamental-styles/dist/title.css';
@import 'fundamental-styles/dist/scrollbar.css';
Add these only when the corresponding section is included:
- footer →
fundamental-styles/dist/bar.css - subheader → no extra file (covered by
dynamic-page.css)
Phase 2: Gather Component Context
Call the @fundamental-ngx/mcp MCP server:
get_usage_guide('fd-flexible-column-layout')— iffclflag is set, check composition rules for dynamic page inside FCL columnsget_component_api('fd-dynamic-page')— size, background, expandContent, positionRelative inputsget_component_api('fd-dynamic-page-header')— title, subtitle, headingLevel (signal inputs)- If
subheaderrequested:get_component_api('fd-dynamic-page-subheader')— collapsible, pinnable, collapsed - If
footerrequested:get_component_api('fd-bar')— barDesign="floating-footer" usage
If MCP is unavailable, read libs/mcp-server/src/data/usage-guides.ts.
Phase 3: Present Plan
Output this summary before writing any code:
## Page Layout Plan: [PageName]
**Component:** fd-dynamic-page
**Size:** extra-large (auto-responsive)
**Sections:**
- [x] Header — title, subtitle, breadcrumbs, global actions
- [x] Subheader — collapsible, pinnable
- [ ] Tabs — single content area
- [x] Footer — floating (Save / Cancel)
- [ ] FCL — standalone page
**Header actions:**
- Global: Edit, Delete buttons (left)
- Layout: Settings button (right)
Stop here and wait for approval before generating code.
Phase 4: Generate Component
Create three files in the target path (ask the user if not already known).
TypeScript (.component.ts)
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { DynamicPageModule } from '@fundamental-ngx/core/dynamic-page';
import { BreadcrumbModule } from '@fundamental-ngx/core/breadcrumb'; // include when header has breadcrumbs
import { LinkModule } from '@fundamental-ngx/core/link'; // include when breadcrumb anchors use fd-link
import { ToolbarModule } from '@fundamental-ngx/core/toolbar';
import { BarModule } from '@fundamental-ngx/core/bar'; // include only when footer is present
import { ButtonModule } from '@fundamental-ngx/core/button';
@Component({
selector: 'app-[kebab-name]-page',
templateUrl: './[kebab-name]-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
// RouterLink required when breadcrumb anchors use routerLink
// LinkModule required when breadcrumb anchors use fd-link directive
// BarModule required only when footer is present
imports: [DynamicPageModule, BreadcrumbModule, LinkModule, RouterLink, ToolbarModule, BarModule, ButtonModule]
})
export class [Name]PageComponent {
readonly isSubheaderCollapsed = signal(false);
onCollapseChange(collapsed: boolean): void {
this.isSubheaderCollapsed.set(collapsed);
}
onSave(): void {
// TODO: implement save
}
onCancel(): void {
// TODO: navigate back or reset state
}
}
HTML Template (.component.html)
Sections are composed strictly in this order inside <fd-dynamic-page>:
fd-dynamic-page-headerfd-dynamic-page-subheader(if needed)- Tab list or
fd-dynamic-page-content(one or more) fd-dynamic-page-footer(if needed)
Single content area (no tabs)
<fd-dynamic-page size="extra-large" background="solid" [expandContent]="true" ariaLabel="[Page Name]">
<fd-dynamic-page-header title="[Page Title]" subtitle="[Subtitle]" [headingLevel]="1">
<fd-dynamic-page-breadcrumb>
<fd-breadcrumb>
<fd-breadcrumb-item>
<a fd-link routerLink="/">Home</a>
</fd-breadcrumb-item>
<fd-breadcrumb-item>[Page Name]</fd-breadcrumb-item>
</fd-breadcrumb>
</fd-dynamic-page-breadcrumb>
<fd-dynamic-page-global-actions>
<fd-toolbar>
<button fd-button fdType="emphasized" (click)="onSave()">Edit</button>
<button fd-button fdType="negative">Delete</button>
</fd-toolbar>
</fd-dynamic-page-global-actions>
<fd-dynamic-page-layout-actions>
<fd-toolbar>
<button fd-button fdType="transparent">Settings</button>
</fd-toolbar>
</fd-dynamic-page-layout-actions>
</fd-dynamic-page-header>
<fd-dynamic-page-subheader
[collapsible]="true"
[pinnable]="true"
[collapsed]="isSubheaderCollapsed()"
(collapsedChange)="onCollapseChange($event)"
>
<!--
IMPORTANT: put actual content here (key facts, filters, metadata).
The collapse button hides/shows this section via CSS [aria-hidden=true]{display:none}.
An empty subheader makes the button appear broken — nothing visually changes.
-->
</fd-dynamic-page-subheader>
<fd-dynamic-page-content>
<!-- main scrollable content -->
</fd-dynamic-page-content>
<fd-dynamic-page-footer>
<div fd-bar barDesign="floating-footer">
<div fd-bar-right>
<button fd-bar-element fd-button fdType="emphasized" (click)="onSave()">Save</button>
<button fd-bar-element fd-button fdType="transparent" (click)="onCancel()">Cancel</button>
</div>
</div>
</fd-dynamic-page-footer>
</fd-dynamic-page>
Tabbed content area
When multiple content sections are needed, provide tabLabel and id on each fd-dynamic-page-content. Use fdp-dynamic-page if tabChange events are required.
<fd-dynamic-page size="extra-large" [expandContent]="true" ariaLabel="[Page Name]">
<fd-dynamic-page-header title="[Page Title]" [headingLevel]="1"> </fd-dynamic-page-header>
<fd-dynamic-page-content tabLabel="Overview" id="tab-overview">
<!-- tab 1 content -->
</fd-dynamic-page-content>
<fd-dynamic-page-content tabLabel="Details" id="tab-details">
<!-- tab 2 content -->
</fd-dynamic-page-content>
<fd-dynamic-page-content tabLabel="History" id="tab-history">
<!-- tab 3 content -->
</fd-dynamic-page-content>
</fd-dynamic-page>
Inside Flexible Column Layout (FCL)
When the page is a column inside fd-flexible-column-layout, add [positionRelative]="true" so the floating footer is constrained to the column width, not the full viewport:
<fd-dynamic-page size="large" [expandContent]="true" [positionRelative]="true" ariaLabel="[Page Name]">
<!-- same inner structure -->
</fd-dynamic-page>
Inside a dynamically-sized container
If fd-dynamic-page is inside a container whose height changes at runtime (e.g., a resizable panel or fd-split-layout), apply fdDynamicPageWrapper to the container element so the page recalculates its height on resize:
<div fdDynamicPageWrapper class="my-panel">
<fd-dynamic-page ...> </fd-dynamic-page>
</div>
fdDynamicPageWrapper is exported from DynamicPageModule. Without it, the page height will be wrong after the container resizes.
SCSS (.component.scss)
Leave empty — dynamic-page layout and spacing are handled by fundamental-styles. Add rules only if the host element needs explicit sizing (e.g., height: 100% when the page is inside a flex container).
Critical Rules
- Composition order is fixed inside
<fd-dynamic-page>: header → subheader → tab list or content(s) → footer. Angular throws if content is projected out of order. - Multiple
fd-dynamic-page-contentrequiretabLabelon every one — mixing labeled and unlabeled content sections throws a runtime error. Either all havetabLabel(tabs mode) or none do (single content mode). fd-dynamic-page-headeruses signal inputs (title,subtitle,headingLevel) — bind with property syntax[title]="'My Page'"or string literaltitle="My Page"(both work); do NOT use[(title)]two-way binding.fd-dynamic-page-subheaderuses@Input()decorators (not signals) —[collapsible],[pinnable],[collapsed]are standard@Input()bindings;(collapsedChange)is@Output().[positionRelative]="true"is required inside FCL — without it, the floating footer spans the full viewport width instead of the column.fdDynamicPageWrapperdirective on the outer container when the container height is dynamic — without it, the content area height is calculated once at init and never updated on resize.- Header collapse requires a subheader with content — the collapse button collapses the
fd-dynamic-page-subheadersection only. The mechanism is CSS-based:fundamental-stylessets.fd-dynamic-page__collapsible-header[aria-hidden=true] { display: none }when the collapsed state changes. If the subheader has no visible content (e.g., only a comment), clicking the button has no visible effect — the button's arrow icon changes but the page looks identical. Always put real content (key facts, filter chips, metadata) insidefd-dynamic-page-subheaderso the collapse is noticeable. To disable snap-on-scroll, set[disableSnapOnScroll]="true"onfd-dynamic-page. fd-barwithbarDesign="floating-footer"is the correct footer content —fd-dynamic-page-footeris a slot wrapper; the actual floating bar styling comes fromfd-bar.- Use
fdp-dynamic-pageonly if you needtabChangeevents orfdp-icon-tab-bar— otherwisefd-dynamic-pageis sufficient and has fewer dependencies. - Do NOT add
standalone: true— default since Angular 19. - Do NOT use
*ngIf/*ngFor— use@if/@for.
Phase 5: Validate
# NX monorepo
nx run <project>:build
# Standalone Angular CLI app
ng build # or: npm run build
Verify the page renders with correct header height, the footer floats at the bottom, and (if tabs are used) tab navigation works before reporting done.
Output
## Build Page Layout: [PageName]
**Files generated:**
- src/app/.../[kebab-name]-page.component.ts
- src/app/.../[kebab-name]-page.component.html
- src/app/.../[kebab-name]-page.component.scss
**Imports required in parent or router:**
- `import { [Name]PageComponent } from './[kebab-name]-page/[kebab-name]-page.component'`
**Sections implemented:** header ✓ | subheader(collapsible) ✓ | content ✓ | footer(floating) ✓
**Next steps:**
- [ ] Replace breadcrumb items with real route links
- [ ] Replace header action buttons with your actual actions
- [ ] Fill fd-dynamic-page-content with your feature content
- [ ] If page is a column in fd-flexible-column-layout, add [positionRelative]="true"
- [ ] If the page container resizes dynamically, add fdDynamicPageWrapper to the outer container
Signals
- GitHub stars
- 294
- Forks
- 147
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
build-page-layout- Source
- github.com/sap/fundamental-ngx