Skip to main content
Neural Nativev0.1.0-beta.1Documentation

These examples run under NeuTheme and a safe-area provider. State stays in memory: connect your own persistence for a production application. The code is compiled against the distributed package.

First-run onboarding

Complete a profile, choose a team, set preferences and review the simulated account confirmation. No account is created on a server.

Stepper · Card · Input · Select · Switch · Checkbox · Badge

Device captures shown in English.

First-run onboarding · android · native capture
AndroidNative screenshot
First-run onboarding · ios · native capture
iOSNative screenshot
onboarding.ts
import { Component, computed, signal } from '@angular/core';
import { Text, View } from '@ng-native/components';
import { NeuBadgeComponent, NeuCardComponent, NeuCheckboxComponent, NeuInputComponent,
  NeuSelectComponent, NeuStepperComponent, NeuSwitchComponent, type NeuOption } from '@neural-ui/native';
import { validEmail, validProfile } from './logic.js';

@Component({
  imports: [Text, View, NeuBadgeComponent, NeuCardComponent, NeuCheckboxComponent, NeuInputComponent,
    NeuSelectComponent, NeuStepperComponent, NeuSwitchComponent],
  template: `
    @if (finished()) {
      <neu-card title="Your account is ready" description="You can start working with your team.">
        <neu-badge label="All set" />
        <text class="body">{{ name() }} · {{ email() }}</text>
      </neu-card>
    } @else {
      <neu-stepper [steps]="steps" [(activeStep)]="step" [linear]="true" [completed]="completed()"
        previousLabel="Back" nextLabel="Continue" finishLabel="Create account" (finished)="finish()">
        @switch (step()) {
          @case (0) {
            <neu-card title="Let’s get to know you" description="A profile to collaborate from day one.">
              <neu-input label="Full name" [(value)]="name" [invalid]="name().trim().length < 2" error="Enter your name" />
              <neu-input label="Email address" keyboardType="email-address" [(value)]="email"
                [invalid]="!emailValid()" error="Enter a valid email address" />
              <neu-select label="Team" [options]="teams" [(value)]="team" />
            </neu-card>
          }
          @case (1) {
            <neu-card title="Your way" description="Choose how to stay up to date.">
              <neu-switch label="Team updates" [(checked)]="updates" />
              <neu-checkbox label="I accept the terms" [(checked)]="accepted" />
              <text class="hint">You can change notifications at any time.</text>
            </neu-card>
          }
          @case (2) {
            <neu-card title="Review your profile" description="Check your details before getting started.">
              <view class="summary"><text class="body">{{ name() }}</text><text class="hint">{{ email() }}</text></view>
              <neu-badge [label]="teamLabel()" />
              <text class="body">{{ updates() ? 'You will receive team updates.' : 'Notifications are off.' }}</text>
            </neu-card>
          }
        }
      </neu-stepper>
    }
  `,
  styles: `
    .summary { gap: 4px; }
    .body { color: var(--neu-text, #172033); font-size: 16px; }
    .hint { color: var(--neu-muted, #596477); font-size: 14px; }
  `,
})
export class OnboardingRecipe {
  readonly steps: readonly NeuOption[] = [{ id: 'profile', label: 'Profile' }, { id: 'preferences', label: 'Preferences' }, { id: 'review', label: 'Summary' }];
  readonly teams: readonly NeuOption[] = [{ id: 'design', label: 'Design' }, { id: 'product', label: 'Product' }, { id: 'engineering', label: 'Engineering' }];
  readonly name = signal('Ada Lovelace');
  readonly email = signal('ada@neuralui.org');
  readonly team = signal('design');
  readonly updates = signal(true);
  readonly accepted = signal(false);
  readonly step = signal(0);
  readonly finished = signal(false);
  readonly emailValid = computed(() => validEmail(this.email()));
  readonly profileValid = computed(() => validProfile(this.name(), this.email(), this.team()));
  readonly teamLabel = computed(() => this.teams.find(team => team.id === this.team())?.label ?? 'Team');
  readonly completed = computed(() => [
    ...(this.profileValid() ? ['profile'] : []),
    ...(this.accepted() ? ['preferences'] : []),
    ...(this.profileValid() && this.accepted() ? ['review'] : []),
  ]);
  finish(): void { if (this.profileValid() && this.accepted()) this.finished.set(true); }
}
logic.ts
export interface Project { readonly id: string; readonly label: string; readonly description: string; readonly status: 'active' | 'review' | 'done'; }
export const PROJECTS: readonly Project[] = [
  { id: 'native', label: 'Neural Launch', description: 'A mobile experience for your team', status: 'active' },
  { id: 'studio', label: 'Design studio', description: 'Ideas ready for the next review', status: 'review' },
  { id: 'guide', label: 'Welcome guide', description: 'The first step for every new member', status: 'done' },
];
const normalize = (value: string) => value.trim().normalize('NFD').replace(/\p{Diacritic}/gu, '').toLocaleLowerCase('en');
export function validEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); }
export function validProfile(name: string, email: string, team: string): boolean {
  return name.trim().length >= 2 && validEmail(email) && ['design', 'product', 'engineering'].includes(team);
}
export function filterProjects(query: string, statuses: readonly string[]): readonly Project[] {
  const needle = normalize(query);
  return PROJECTS.filter(project => (!statuses.length || statuses.includes(project.status)) &&
    (!needle || normalize(`${project.label} ${project.description}`).includes(needle)));
}
export function projectById(id: string): Project | null { return PROJECTS.find(project => project.id === id) ?? null; }

Find the next project

Search and filter a native list, select a project and inspect its detail. Empty results hide the previous detail.

Filter Bar · Virtual List · Card · Badge

Device captures shown in English.

Find the next project · android · native capture
AndroidNative screenshot
Find the next project · ios · native capture
iOSNative screenshot
collections.ts
import { Component, computed, signal } from '@angular/core';
import { Text, View } from '@ng-native/components';
import { NeuBadgeComponent, NeuCardComponent, NeuFilterBarComponent, NeuVirtualListComponent, type NeuOption } from '@neural-ui/native';
import { filterProjects, projectById } from './logic.js';

@Component({
  imports: [Text, View, NeuBadgeComponent, NeuCardComponent, NeuFilterBarComponent, NeuVirtualListComponent],
  template: `
    <neu-filter-bar label="Project status" searchLabel="Search projects" [options]="statuses"
      [(query)]="query" [(value)]="filters" clearLabel="Clear filters" />
    <view class="count"><neu-badge [label]="countLabel()" /></view>
    <neu-virtual-list [items]="projects()" [height]="240" emptyLabel="No projects match these filters" (selected)="select($event)" />
    @if (selectedProject(); as project) {
      <neu-card [title]="project.label" description="Project details">
        <neu-badge [label]="statusLabel(project.status)" />
        <text class="body">{{ project.description }}</text>
      </neu-card>
    }
  `,
  styles: `:host { gap: 16px; } .count { align-items: flex-start; } .body { color: var(--neu-text, #172033); font-size: 16px; }`,
})
export class CollectionsRecipe {
  readonly statuses: readonly NeuOption[] = [{ id: 'active', label: 'In progress' }, { id: 'review', label: 'Review' }, { id: 'done', label: 'Done' }];
  readonly query = signal('');
  readonly filters = signal<readonly string[]>([]);
  readonly selected = signal('native');
  readonly projects = computed(() => filterProjects(this.query(), this.filters()));
  readonly countLabel = computed(() => this.projects().length === 1 ? '1 project' : `${this.projects().length} projects`);
  // Filtering hides a stale detail; selecting a visible row establishes the next detail explicitly.
  readonly selectedProject = computed(() => this.projects().some(project => project.id === this.selected()) ? projectById(this.selected()) : null);
  select(id: string): void { if (this.projects().some(project => project.id === id)) this.selected.set(id); }
  statusLabel(status: string): string { return this.statuses.find(option => option.id === status)?.label ?? status; }
}
logic.ts
export interface Project { readonly id: string; readonly label: string; readonly description: string; readonly status: 'active' | 'review' | 'done'; }
export const PROJECTS: readonly Project[] = [
  { id: 'native', label: 'Neural Launch', description: 'A mobile experience for your team', status: 'active' },
  { id: 'studio', label: 'Design studio', description: 'Ideas ready for the next review', status: 'review' },
  { id: 'guide', label: 'Welcome guide', description: 'The first step for every new member', status: 'done' },
];
const normalize = (value: string) => value.trim().normalize('NFD').replace(/\p{Diacritic}/gu, '').toLocaleLowerCase('en');
export function validEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); }
export function validProfile(name: string, email: string, team: string): boolean {
  return name.trim().length >= 2 && validEmail(email) && ['design', 'product', 'engineering'].includes(team);
}
export function filterProjects(query: string, statuses: readonly string[]): readonly Project[] {
  const needle = normalize(query);
  return PROJECTS.filter(project => (!statuses.length || statuses.includes(project.status)) &&
    (!needle || normalize(`${project.label} ${project.description}`).includes(needle)));
}
export function projectById(id: string): Project | null { return PROJECTS.find(project => project.id === id) ?? null; }

Navigate, edit and save

Switch sections, edit a project in a native sheet, commit the draft and confirm the update.

Nav · Card · Badge · Input · Bottom Sheet · Button · Toast · Timeline · Switch

Device captures shown in English.

Navigate, edit and save · android · native capture
AndroidNative screenshot
Navigate, edit and save · ios · native capture
iOSNative screenshot
actions.ts
import { Component, signal } from '@angular/core';
import { Text } from '@ng-native/components';
import { NeuBadgeComponent, NeuBottomSheetComponent, NeuButtonComponent, NeuCardComponent,
  NeuInputComponent, NeuNavComponent, NeuSwitchComponent, NeuTimelineComponent, NeuToastComponent,
  type NeuOption } from '@neural-ui/native';

@Component({
  imports: [Text, NeuBadgeComponent, NeuBottomSheetComponent, NeuButtonComponent, NeuCardComponent,
    NeuInputComponent, NeuNavComponent, NeuSwitchComponent, NeuTimelineComponent, NeuToastComponent],
  template: `
    <neu-nav [items]="navigation" [(value)]="tab" />
    @switch (tab()) {
      @case ('home') {
        <neu-card [title]="title()" description="A shared idea. A team ready to build.">
          <neu-badge label="In progress" />
          <text class="body">Organize the next launch and keep details up to date.</text>
          <neu-button label="Edit project" (pressed)="beginEdit()" />
        </neu-card>
      }
      @case ('activity') {
        <neu-card title="Team activity"><neu-timeline [items]="activity" /></neu-card>
      }
      @case ('settings') {
        <neu-card title="Preferences"><neu-switch label="Project notifications" [(checked)]="notifications" /></neu-card>
      }
    }
    <neu-bottom-sheet title="Edit project" [(open)]="editing" closeLabel="Close editor">
      <neu-input label="Project name" [(value)]="draft" [invalid]="!draft().trim()" error="Enter a name" />
      <neu-button label="Save project" [disabled]="!draft().trim()" (pressed)="save()" />
    </neu-bottom-sheet>
    <neu-toast message="Project updated" [(open)]="saved" [duration]="0" closeLabel="Dismiss notification" />
  `,
  styles: `.body { color: var(--neu-text, #172033); font-size: 16px; }`,
})
export class ActionsRecipe {
  readonly navigation: readonly NeuOption[] = [{ id: 'home', label: 'Home' }, { id: 'activity', label: 'Activity' }, { id: 'settings', label: 'Settings' }];
  readonly activity = [{ id: 'created', label: 'Project created', date: 'Today', done: true }, { id: 'review', label: 'Next review', date: 'Tomorrow' }];
  readonly tab = signal('home');
  readonly title = signal('Mobile launch');
  readonly draft = signal('');
  readonly notifications = signal(true);
  readonly editing = signal(false);
  readonly saved = signal(false);
  beginEdit(): void { this.draft.set(this.title()); this.saved.set(false); this.editing.set(true); }
  save(): void {
    const title = this.draft().trim();
    if (!title) return;
    this.title.set(title);
    this.editing.set(false);
    this.saved.set(true);
  }
}