Settings Page
blocklayoutLinear-style settings shell: a nav rail of icon sections (collapses to a horizontal tab strip on narrow screens) switching between fully composed panels — profile form with sticky save bar, notification switches, theme picker, workspace identity, billing summary, and a type-to-confirm danger zone. Swap the panel internals for your own forms.
Installation
$pnpm dlx uipkge-ng@latest add settings-page$npx uipkge-ng@latest add settings-page$yarn dlx uipkge-ng@latest add settings-page$bunx uipkge-ng@latest add settings-pageManifest:
https://uipkge.dev/r/angular/settings-page.jsonInstalls to:src/app/components/blocks/settings-page/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SettingsSectioninterface SettingsSection {
id: SectionId
label: string
icon: LucideIconData
}npm dependencies
Files installed (2)
src/app/components/blocks/settings-page/settings-page.component.ts14.6 kB
// Linear-style settings shell: a nav rail of icon sections (collapses to a horizontal // tab strip on narrow screens) switching between fully composed panels — profile form with // sticky save bar, notification switches, theme picker, workspace identity, billing summary, // and a type-to-confirm danger zone. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Bell, Building2, CreditCard, LucideAngularModule, Palette, TriangleAlert, User, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiLabelComponent } from '@/ui/label/label.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import { UiTextareaComponent } from '@/ui/textarea/textarea.component' export type SectionId = 'profile' | 'notifications' | 'appearance' | 'workspace' | 'billing' | 'danger' export interface SettingsSection { id: SectionId label: string icon: LucideIconData } const defaultSections: SettingsSection[] = [ { id: 'profile', label: 'Profile', icon: User }, { id: 'notifications', label: 'Notifications', icon: Bell }, { id: 'appearance', label: 'Appearance', icon: Palette }, { id: 'workspace', label: 'Workspace', icon: Building2 }, { id: 'billing', label: 'Billing', icon: CreditCard }, { id: 'danger', label: 'Danger zone', icon: TriangleAlert }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-settings-page, [ui-settings-page]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiButtonComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiInputComponent, UiLabelComponent, UiSectionCardComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiSwitchComponent, UiTextareaComponent, ], template: ` <div data-slot="settings-page" [class]="rootClass"> <!-- Nav rail --> <nav class="border-border shrink-0 border-b p-3 lg:w-56 lg:border-r lg:border-b-0" aria-label="Settings sections"> <div class="flex [scrollbar-width:none] gap-1 overflow-x-auto lg:flex-col lg:overflow-visible [&::-webkit-scrollbar]:hidden" > @for (section of sections(); track section.id) { <button type="button" [class]="navButtonClass(section.id)" [attr.aria-current]="active() === section.id ? 'page' : null" (click)="switchPanel(section.id)" > <lucide-icon [img]="section.icon" class="size-4" aria-hidden="true" /> {{ section.label }} </button> } </div> </nav> <!-- Content pane --> <div class="flex min-w-0 flex-1 flex-col"> @if (active() === 'profile') { <section class="flex flex-1 flex-col"> <div class="flex-1 space-y-6 p-6"> <ui-section-card title="Public profile" description="How teammates see you across the workspace."> <div class="space-y-4"> <div class="space-y-2"> <label ui-label for="sp-name">Display name</label> <ui-input id="sp-name" defaultValue="Amara Osei" /> </div> <div class="space-y-2"> <label ui-label for="sp-email">Email</label> <ui-input id="sp-email" defaultValue="amara@acme.com" type="email" /> </div> <div class="space-y-2"> <label ui-label for="sp-bio">Bio</label> <ui-textarea id="sp-bio" defaultValue="Design engineer. Builds systems, breaks assumptions." [rows]="3" /> </div> <div class="space-y-2"> <label ui-label for="sp-role">Role</label> <ui-select [value]="role()" (valueChange)="role.set($event)"> <button ui-select-trigger id="sp-role" class="w-full sm:w-56"> <ui-select-value /> </button> <ui-select-content> <ui-select-item value="admin">Admin</ui-select-item> <ui-select-item value="editor">Editor</ui-select-item> <ui-select-item value="viewer">Viewer</ui-select-item> </ui-select-content> </ui-select> </div> </div> </ui-section-card> </div> <div class="border-border flex items-center justify-end gap-2 border-t p-4"> <button ui-button variant="ghost" size="sm">Discard</button> <button ui-button size="sm">Save changes</button> </div> </section> } @else if (active() === 'notifications') { <section class="flex-1 p-6"> <ui-section-card title="Notifications" description="Choose what reaches you and where."> <ul class="-my-4 divide-y"> <li class="flex items-center justify-between gap-4 py-4"> <div> <p class="text-sm font-medium">Email notifications</p> <p class="text-muted-foreground text-xs">Mentions, assignments and approvals.</p> </div> <button ui-switch [checked]="notifyEmail()" (checkedChange)="notifyEmail.set($event)" aria-label="Toggle email notifications" ></button> </li> <li class="flex items-center justify-between gap-4 py-4"> <div> <p class="text-sm font-medium">Push notifications</p> <p class="text-muted-foreground text-xs">Real-time browser alerts while you work.</p> </div> <button ui-switch [checked]="notifyPush()" (checkedChange)="notifyPush.set($event)" aria-label="Toggle push notifications" ></button> </li> <li class="flex items-center justify-between gap-4 py-4"> <div> <p class="text-sm font-medium">Weekly digest</p> <p class="text-muted-foreground text-xs">A summary of everything, every Monday.</p> </div> <button ui-switch [checked]="notifyDigest()" (checkedChange)="notifyDigest.set($event)" aria-label="Toggle weekly digest" ></button> </li> </ul> </ui-section-card> </section> } @else if (active() === 'appearance') { <section class="flex-1 p-6"> <ui-section-card title="Appearance" description="Theme follows your system by default."> <div class="space-y-5"> <div class="grid grid-cols-3 gap-3"> @for (theme of themeChoices; track theme) { <button type="button" [class]="themeButtonClass(theme)" (click)="themeChoice.set(theme)" > <span class="block text-sm font-medium capitalize">{{ theme }}</span> <span class="text-muted-foreground block text-xs">{{ themeHint(theme) }}</span> </button> } </div> <ui-separator /> <div class="flex items-center justify-between"> <div> <p class="text-sm font-medium">Reduced motion</p> <p class="text-muted-foreground text-xs">Collapse animations to instant transitions.</p> </div> <button ui-switch [checked]="false" aria-label="Toggle reduced motion"></button> </div> </div> </ui-section-card> </section> } @else if (active() === 'workspace') { <section class="flex-1 p-6"> <ui-section-card title="Workspace" description="Identity and defaults for everyone here."> <div class="space-y-4"> <div class="space-y-2"> <label ui-label for="sp-ws-name">Workspace name</label> <ui-input id="sp-ws-name" defaultValue="Acme Inc" /> </div> <div class="space-y-2"> <label ui-label for="sp-ws-url">URL slug</label> <div class="flex"> <span class="border-border bg-muted text-muted-foreground flex items-center rounded-l-md border px-3 font-mono text-xs" > acme.uipkge.app </span> <ui-input id="sp-ws-url" defaultValue="acme" class="rounded-l-none font-mono text-xs" /> </div> </div> <p class="text-muted-foreground text-xs">16 members · 4 pending invites</p> </div> </ui-section-card> </section> } @else if (active() === 'billing') { <section class="flex-1 p-6"> <ui-section-card title="Billing" description="Plan, seats and invoices."> <div class="flex items-center justify-between gap-4"> <div> <p class="text-sm font-medium">Team plan · $12 / seat / month</p> <p class="text-muted-foreground mt-1 text-xs">Next invoice Sep 1, 2026 · 18 seats</p> </div> <button ui-button variant="outline" size="sm">Manage billing</button> </div> </ui-section-card> </section> } @else { <section class="flex-1 p-6"> <div class="border-destructive/40 rounded-xl border p-6"> <div class="flex items-center justify-between gap-4"> <div> <p class="text-destructive text-sm font-medium">Delete this workspace</p> <p class="text-muted-foreground mt-1 text-xs"> All projects, members and data will be permanently removed. </p> </div> <button ui-button variant="destructive" size="sm" (click)="deleteOpen.set(true)">Delete…</button> </div> </div> </section> } </div> <ui-dialog [open]="deleteOpen()" (openChange)="deleteOpen.set($event)"> <ui-dialog-content class="max-w-sm"> <div ui-dialog-header> <h2 ui-dialog-title>Delete workspace?</h2> <p ui-dialog-description> This removes Acme Inc and all of its data for every member. This cannot be undone. </p> </div> <ui-input [value]="deleteConfirm()" (valueChange)="deleteConfirm.set($event)" placeholder='Type "DELETE" to confirm' /> <div ui-dialog-footer> <button ui-button variant="outline" size="sm" (click)="deleteOpen.set(false)">Cancel</button> <button ui-button variant="destructive" size="sm" [disabled]="deleteConfirm() !== 'DELETE'" (click)="deleteOpen.set(false)" > Delete forever </button> </div> </ui-dialog-content> </ui-dialog> </div> `, }) export class UiSettingsPageComponent { @Input('sections') set sectionsInput(v: SettingsSection[] | undefined) { if (v) this.sectionsOverride.set([...v]) } @Input('initialSection') set initialSectionInput(v: SectionId | undefined) { if (v) this.active.set(v) } @Input('class') className?: string readonly sectionsOverride = signal<SettingsSection[] | null>(null) readonly sections = computed(() => this.sectionsOverride() ?? defaultSections) readonly active = signal<SectionId>('profile') readonly notifyEmail = signal(true) readonly notifyPush = signal(false) readonly notifyDigest = signal(true) readonly role = signal('editor') readonly themeChoice = signal<'light' | 'dark' | 'system'>('system') readonly themeChoices: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'] readonly deleteOpen = signal(false) readonly deleteConfirm = signal('') get rootClass(): string { return cn('bg-background border-border flex min-h-svh flex-col rounded-xl border lg:flex-row', this.className) } navButtonClass(id: SectionId): string { // [class] overwrites the whole class attribute: the base layout/focus classes must be merged here. return cn( 'focus-visible:ring-ring flex shrink-0 items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none', this.active() === id ? id === 'danger' ? 'text-destructive bg-destructive/10' : 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground', ) } themeButtonClass(theme: 'light' | 'dark' | 'system'): string { return cn( 'focus-visible:ring-ring rounded-lg border p-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none', this.themeChoice() === theme ? 'border-primary ring-primary/20 ring-2' : 'hover:bg-accent/50', ) } themeHint(theme: 'light' | 'dark' | 'system'): string { return theme === 'light' ? 'Bright surfaces' : theme === 'dark' ? 'Low-light friendly' : 'Match device' } switchPanel(id: SectionId): void { this.active.set(id) } }src/app/components/blocks/settings-page/index.ts0.1 kB
export { UiSettingsPageComponent, type SectionId, type SettingsSection } from './settings-page.component'
Raw manifest:https://uipkge.dev/r/angular/settings-page.json