# Stop Rebuilding Auth, Onboarding, and Dashboards: DesignFoundationPro

> Source: <https://dev.to/nerd_snipe_dev/stop-rebuilding-auth-onboarding-and-dashboards-designfoundationpro-39oj>
> Published: 2026-07-07 06:26:00+00:00

Part 2 of 2.This tutorial builds on[Part 1 — DesignFoundation core]. If you haven't added the base package and theme yet, start there.

The core package gives you tokens and primitives. **DesignFoundationPro** adds what comes next — 29 blocks, 47 screens, 18 navigation shells, and 9 runnable composition examples across auth, onboarding, charts, data tables, and full product verticals. The docs claim ~87% fewer lines of code versus building from scratch. That's the bet.

This matters even more if an AI coding agent is doing the building. Ask an agent for a CRM screen and a settings screen in the same session and, without guardrails, you'll get two different takes on spacing, two different sidebar behaviors, and a table that's native on Mac in one screen and a scroll view pretending to be a table in the other. Pro ships with that guardrail already in place — more on that in Where to go from here.

Pro sits on top of Foundation and re-exports it. Import only `DesignFoundationPro`

and you get everything from both:

```
YourApp                    your models, data, routing
DesignFoundationPro        29 blocks · 47 screens · 18 shells · 9 examples
DesignFoundation           tokens · primitives · validation · theme engine · MIT
```

**Access:** Foundation is MIT and public. Pro is a commercial add-on — repo access is granted after purchase. Licenses are **lifetime** (no subscription); annual updates are $39/year and entirely optional. Pricing: **$149** individual · **$449** team (up to 5 devs).

Before buying, browse everything in **DFPlayground** — a free macOS app that lets you preview all 29 blocks, 47 screens, and 18 shells with live theming.

Pro declares Foundation as its own dependency and re-exports it via `@_exported import DesignFoundation`

. Add only the Pro package — Foundation comes with it automatically. The Pro repo URL is provided after purchase — contact [nerdsnipe.inc@gmail.com](mailto:nerdsnipe.inc@gmail.com) or visit the [Pro page](https://nerdsnipe-inc.github.io/design-foundation/pro/) to get access.

```
dependencies: [
    .package(url: "https://github.com/NerdSnipe-Inc/DesignFoundationPro", from: "1.0.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "DesignFoundationPro", package: "DesignFoundationPro"),
        ]
    ),
]
```

One package, one import: `import DesignFoundationPro`

gives you both layers with no separate Foundation entry needed.

Pro adds `DFTheme.workspace`

— a platform-adaptive preset tuned for data-dense product UIs. It uses tighter desktop typography and adjusted surface tokens on macOS. For most product apps, this is the right starting point.

``` python
import SwiftUI
import DesignFoundationPro

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            MyRootView()
                .dfTheme(.workspace)
        }
    }
}
```

Still want a custom brand?Build your`DFTheme`

from tokens as shown in Part 1 — the individual blocks (auth, charts, onboarding) genuinely read whatever theme is in the environment. The composition roots and shells in Step 8 are the exception: each one hard-codes`.environment(\.dfTheme, .workspace)`

internally, so mounting`DFCRMRootView()`

(or any other root, or`DFOnboardingFlow`

) always renders in`.workspace`

regardless of what theme you set upstream. If you need a branded CRM/PM/Analytics root, that's a real customization, not a one-line override — check the root's source for where it applies the theme before assuming an ambient override will reach it.

Every product app needs some variation of "sidebar on Mac and iPad, tabs on iPhone," and it's one of the most-rebuilt pieces of chrome in SwiftUI — split view state, collapse behavior, selection binding, all rewritten per project. Shells are layout-only, pre-solved versions of that problem. They give you navigation structure — sidebar, columns, tabs — and expose `@ViewBuilder`

slots for your own content. No data or business logic.

| Shell | Use for |
|---|---|
`DFStandardSidebarShell` |
Single sidebar + detail. macOS-native split view. |
`DFThreeColumnShell` |
Source list → content → inspector. Documents, mail. |
`DFWorkspaceSidebarShell` |
Collapsible sidebar with workspace density tokens. |
`DFIconRailShell` |
Narrow icon rail. Dashboard or tool-focused apps. |
`DFAdaptiveShell` |
Tabs on iPhone, sidebar on iPad and Mac. |
`DFSearchSidebarShell` |
Sidebar with integrated search bar at top. |

Each shell has its own init signature matching its structural role. (The table above lists 6 of the 18 general-purpose shells; the rest follow the same pattern.) There's also a 19th, vertical-specific shell — `DFSocialAppShell`

— built for feed/explore/notifications/profile apps specifically rather than generic navigation. It's the simplest to read, with four named `@ViewBuilder`

slots:

``` js
struct MyRootView: View {
    var body: some View {
        DFSocialAppShell {
            FeedView()
        } explore: {
            ExploreView()
        } notifications: {
            NotificationsView()
        } profile: {
            ProfileView()
        }
        .dfToastHost()
    }
}
```

The sidebar shells take typed nav data alongside a selection binding and a content `@ViewBuilder`

: `DFStandardSidebarShell`

takes `sections: [DFNavSection]`

(each section holds `[DFNavItem]`

), `DFAdaptiveShell`

takes `[DFAdaptiveTab]`

, `DFWorkspaceSidebarShell`

takes `[DFWorkspace]`

plus an icon rail and account switcher. The composition roots (Step 8) wire all of this for you when you're building a known vertical.

MountAfter that, trigger toasts from anywhere in the tree with`.dfToastHost()`

once on any root view.`DFToastQueue.shared.show(text: "Saved", severity: .success)`

.

Auth screens feel simple until you're deep into forgot-password states, social provider buttons, OTP resend timers, and inline validation — a week of work for something that isn't your product's differentiator.

The four Pro auth blocks — `DFWelcomeBlock`

, `DFSignInBlock`

, `DFSignUpBlock`

, `DFOTPBlock`

— each take a `Configuration`

struct with callbacks. Form validation is wired by default using `DFFormState`

from Foundation; set `usesFormValidation: false`

only when you own it yourself.

```
DFSignInBlock(configuration: .init(
    title: "Sign in to Acme",
    subtitle: "Welcome back",
    socialProviders: [
        .apple  { Task { await AuthService.signInWithApple()  } },
        .google { Task { await AuthService.signInWithGoogle() } },
    ],
    onSubmit: { email, password in
        // onSubmit is not async — wrap async work in a Task
        Task { await AuthService.signIn(email: email, password: password) }
    },
    onForgotPassword: { showForgotPassword() },
    onSignUp: { showSignUp() }
))
```

The block handles layout, field spacing, validation error display, and social auth button presentation across iOS, macOS, and visionOS. Desktop density is applied automatically from `@Environment(\.dfUseDesktopDensity)`

.

Pair `DFSignUpBlock`

with `DFOTPBlock`

for email verification. Each block is independent — you control navigation between them:

```
DFOTPBlock(configuration: .init(
    title: "Check your email",
    subtitle: registeredEmail,   // shown under the title
    onSubmit: { code in
        // onSubmit is not async — wrap async work in a Task
        Task { await AuthService.verify(otp: code) }
    },
    onResend: {
        Task { await AuthService.resendOTP() }
    }
))
```

Onboarding flows are where a lot of hand-rolled apps quietly go wrong — the "what screen comes next" logic ends up spread across seven view files with their own local state, and resuming mid-flow after the app is backgrounded is an afterthought nobody tests.

`DFOnboardingFlow`

is the highest-level Pro component: a multi-step flow that covers welcome, sign-up or sign-in, OTP verification, profile setup, permission requests, plan selection, personalisation, and a success screen. It manages its own step state (persisted across launches via `UserDefaults`

) and handles back navigation.

You configure the whole flow via `DFOnboardingConfiguration`

— one struct, no subclassing:

```
DFOnboardingFlow(configuration: .init(
    appName:         "Acme",
    welcomeTagline:  "Welcome to Acme",
    welcomeHeadline: "The best tool for your team.",
    enableAppleSignIn:  true,
    enableGoogleSignIn: true,

    featureHighlights: [
        .init(icon: "sparkles", title: "AI-Powered",
              description: "Smart suggestions across every workflow."),
        .init(icon: "person.3", title: "Built for Teams",
              description: "Real-time collaboration, no friction."),
    ],

    requestedPermissions: [.notifications],

    showPlanSelection: true,
    planTiers: [
        .init(name: "Free",  price: "$0",   period: "forever",
              features: ["3 projects", "1,000 API calls"], isPopular: false),
        .init(name: "Pro",   price: "$49",  period: "/ month",
              features: ["Unlimited projects", "Priority support"],
              isPopular: true, badge: "Most Popular"),
    ],

    personalisationTags: [
        .init(id: "startup",    label: "Startup"),
        .init(id: "enterprise", label: "Enterprise"),
        .init(id: "freelance",  label: "Freelance"),
    ],

    onSignUp:  { name, email, password in
        // Return Bool: true = success, false = failure (e.g. email taken)
        return await AuthService.signUp(name: name, email: email, password: password)
    },
    onSignIn:  { email, password in
        return await AuthService.signIn(email: email, password: password)
    },
    onOTPVerify:             { code in return await AuthService.verify(otp: code) },
    onOTPResend:             {           await AuthService.resendOTP()         },
    onProfileSave:           { name, avatar in await ProfileService.save(name, avatar) },
    onPermissionRequest:     { perm in  await PermissionService.request(perm) },
    onPlanSelected:          { tier in   BillingService.select(tier)          },
    onPersonalisationComplete: { tags in ProfileService.saveTags(tags)        },
    onComplete:              {           AppState.shared.isOnboarded = true   }
))
```

That one initialiser configures every step. If the user backgrounds the app mid-onboarding, they resume where they left off.

Swift Charts gives you the primitives, but every dashboard still needs the same surrounding scaffolding — a period picker, a legend, loading and empty states, consistent theme colors per series. That's the part that gets rebuilt per project.

Chart blocks are composables, not screen-locked. Drop them into your own views, not just the Analytics root. They use Swift Charts under the hood and inherit theme colors automatically.

| Block | Use for |
|---|---|
`DFLineChartBlock` |
Trends, time series, cumulative metrics |
`DFBarChartBlock` |
Category comparison, period-over-period |
`DFDonutChartBlock` |
Part-to-whole breakdown, proportions |
`DFStatCardBlock` |
Single metric with delta and trend sparkline |

Don't useIt's a loading/skeleton state only. For loading chart data, pass`DFChartPlaceholderBlock`

in shipping UI.`isLoading: true`

to the real chart block instead.

```
// Keyword arguments must appear in the same order as the initializer declares them —
// isLoading: comes right after title:, before data:.
DFLineChartBlock(configuration: .init(
    title: "Active Users",
    isLoading: isFetchingData,
    data: [
        ("Mon", 182), ("Tue", 195), ("Wed", 188),
        ("Thu", 212), ("Fri", 240), ("Sat", 198),
    ],
    periods: DFChartPeriodPreset.allCases,
    selectedPeriod: .sevenDays,
    onPeriodChange: { period in
        // onPeriodChange is not async — wrap async work in a Task
        Task { await loadTrend(for: period) }
    },
    height: isDesktop ? 140 : 220
))
```

The period toolbar is built in. Your domain period enum conforms to `DFChartPeriodRepresentable`

if you need custom ranges beyond the preset.

A data table that feels native on macOS (keyboard navigation, `Return`

to activate a row) and still works on a phone-width screen is two different UIs wearing the same data. Building both, and deciding which one renders where, is a recurring cross-platform tax.

`DFDataTable`

ships in Foundation but it's most useful in Pro-scale apps. On macOS it renders a native `Table`

with keyboard navigation and Return-to-activate. On iPhone it falls back to scrollable header + rows. Use it on regular-width layouts; keep `DFList`

with swipe actions on compact phone screens where that fits better.

``` js
@State private var selectedIDs: Set<Contact.ID> = []

private var columns: [DFDataTableColumn<Contact>] {
    [
        DFDataTableColumn(id: "name",    title: "Name")    { $0.name            },
        DFDataTableColumn(id: "company", title: "Company") { $0.company         },
        DFDataTableColumn(id: "status",  title: "Status")  { $0.status.rawValue },
    ]
}

DFDataTable(
    data:          filteredContacts,
    columns:       columns,
    selection:     $selectedIDs,
    selectionMode: .multiple,
    filterQuery:   searchText,
    onRowActivate: { openDetail($0) },
    emptyContent: {
        DFEmptyStateBlock(configuration: .init(
            icon:    "person.crop.circle.badge.questionmark",
            title:   "No contacts found",
            message: "Try a different search or filter."
        ))
    }
)
```

Starting a new product vertical from a blank `ContentView`

means weeks before it looks like a real app. If you're building a known vertical, Pro ships full composition roots instead: pre-wired screens, navigation, and blocks. Mount the root, replace the fixture data with your models, and you have something that looks like a real product immediately.

| Root | What it includes |
|---|---|
`DFCRMRootView` |
Home, pipeline, contacts table, analytics |
`DFPMRootView` |
Project board, list, timeline, team roster |
`DFAnalyticsRootView` |
Overview, events, revenue, users — all with charts |
`DFAIChatRootView` |
New chat, thread, compare view, settings sheet |
`DFEcommerceRootView` |
Store home, orders table, products, revenue |
`DFSettingsRootView` |
Account, security, billing, team, danger zone |
`DFSocialAppShell` |
Feed, explore, notifications, profile |

``` js
struct MyRootView: View {
    var body: some View {
        DFCRMRootView()
            .dfToastHost()
    }
}
```

The `.environment(\.dfTheme, .workspace)`

line from Step 2 isn't needed here — every composition root applies `.workspace`

internally, so it renders themed correctly with no setup at all. The root ships with preview fixtures (`CRMPreviewFixtures`

, etc.) so it renders immediately with realistic data. Replace those fixtures with your own domain types as you go. Keep the layout, the density helpers, and the theme wiring — those are the parts you don't want to rebuild.

Quality gate.Before shipping a screen built on Pro,`QUALITY-GATE.md`

in the repo has 15 review items including platform-native layout, non-happy-path previews, and correct root wiring. The bar is: would this hold up as a standalone app?

Every form in an app tends to reinvent its own field-to-validator plumbing — a slightly different `isValid`

computed property each time. Pro auth and settings screens wire `DFFormState`

by default so that plumbing exists once. If you need the same pattern in your own views:

``` js
private enum Field {
    static let email    = "email"
    static let password = "password"
}

@State private var form = DFFormState()

// Register fields and validators — e.g. in .task or a setup method:
form.register(field: Field.email,    validators: [DFRequiredValidator(), DFEmailValidator()])
form.register(field: Field.password, validators: [DFRequiredValidator(), DFMinLengthValidator(minLength: 8)])

// In your view body:
DFValidatedTextField("Email", field: Field.email, form: form)

DFSecureField(
    "Password",
    text: form.binding(for: Field.password),
    validationState: form.validationState(for: Field.password)
)

DFButton("Sign in") {
    guard form.validate() else { return }
    // DFButton action is () -> Void — wrap async work in a Task
    Task {
        await submit(
            form.values[Field.email,    default: ""],
            form.values[Field.password, default: ""]
        )
    }
}
```

Built-in validators: `DFRequiredValidator`

, `DFEmailValidator`

, `DFMinLengthValidator`

, `DFMaxLengthValidator`

, `DFRegexValidator`

. Conform to `DFFieldValidator`

to add your own.

Pro ships the same `CLAUDE.md`

/ `AGENTS.md`

/ `.cursor`

rule file as Foundation, plus more: a `.cursor/skills`

and `.cursor/references`

library covering Apple's Human Interface Guidelines platform-by-platform, Liquid Glass, Swift Charts patterns, accessibility, and desktop-app archetypes. Point Claude Code, Cursor, or Codex at a repo that depends on DesignFoundationPro and it isn't just told "use `DFButton`

" — it has design-pattern reference material to draw on when it's deciding how a screen should actually look.

That's the difference between an agent that produces a working screen and one that produces a *correct-looking, cross-platform* screen without a single `#if os(macOS)`

block — because the platform branching already happened inside the shell, the block, or the root it reached for. The practical result: when three different agent sessions build three different features of the same app, they come out looking like one product instead of three.

DesignFoundationPro is designed to be adopted incrementally — a single block takes about 5 minutes to drop in, a full screen 10 minutes, a composition root 15. You don't need the full vertical to get value from it.

Issues and feedback on the [GitHub repo](https://github.com/NerdSnipe-Inc/DesignFoundationPro). If you missed the foundation layer, start with [Part 1](https://dev.to/nerdsnipe/stop-rebuilding-the-same-swiftui-components-a-guide-to-designfoundation).
