# SaaS workspace

> Drawer panel for a SaaS workspace: logo and name with a close button, an Accent Colour onboarding item with a 5 Steps Badge, main items, a bottom group (Notifications with a count, Settings, Docs, Help) and a footer with a team switcher and the user's avatar.

Block · navigation. Registry item `drawer-02`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/drawer-02.json

## Installation

```sh
npx nativecn-cli@latest add drawer-02
```

Also installs: [drawer](https://nativecn.dev/docs/components/drawer), [avatar](https://nativecn.dev/docs/components/avatar), [badge](https://nativecn.dev/docs/components/badge), [button](https://nativecn.dev/docs/components/button), [icon](https://nativecn.dev/docs/components/icon), [pressable](https://nativecn.dev/docs/primitives/pressable), [text](https://nativecn.dev/docs/components/text), [theme](https://nativecn.dev/docs/theme/theme).

Packages: `lucide-react-native@^1.51.0`, `react-native-svg`.

## What makes it different

A product workspace: an Accent-tinted onboarding Checklist on top, a second group pushed to the bottom, and a team switcher and account button in the footer instead of a profile header.

## Block Variants

- [drawer-01](https://nativecn.dev/docs/blocks/drawer-01) – Profile header + grouped sections: The familiar Gmail/Slack drawer: who is signed in on top, items grouped under small headings, Log out at the bottom; the active item sits on a muted background.
- **drawer-02** (this page) – SaaS workspace: A product workspace: an Accent-tinted onboarding Checklist on top, a second group pushed to the bottom, and a team switcher and account button in the footer instead of a profile header.
- [drawer-03](https://nativecn.dev/docs/blocks/drawer-03) – Cover header: A consumer or commerce app drawer: a dark profile cover that stays dark in both Schemes (with a light status bar over it while the drawer is open), and items that show the current route by Accent Colour text and icon, with no background.

## Accessibility

- Each item is a DrawerItem: role link with aria-selected while its route is active, or role button for an action without href.
- An item's visible label is its accessible name.
- Items are 48 high in Vega and 40 in Nova, with the tap area extended to 48.
- The close button is an icon-only Button named "Close menu"; the logo icon is decorative.
- The footer buttons are named "Switch team, current team Acme Inc" and "Account menu", with their tap areas extended to 48.

## Notes

In app/(drawer)/_layout.tsx, with Expo Router's drawer from expo-router/drawer (never @react-navigation/drawer or the npm package expo-drawer): <Drawer drawerContent={(props) => <Drawer02 {...props} />} />. Spread every drawerContent prop in, so items can close the drawer. Tapping an item navigates and closes the drawer; the backdrop and the system back gesture also close it. Names, avatars, counts, items and routes are placeholders: rename them and point each href at a route in your drawer Layout. Item heights follow the drawer.item Style Slot (Vega 48, Nova 40), and the panel keeps clear of the status bar and home indicator.

## Source

### {components}/drawer-02/index.tsx

```tsx
// drawer-02: SaaS workspace. The panel of a drawer Layout, built from the drawer Components on
// Expo Router's own drawer (`expo-router/drawer`; never install `@react-navigation/drawer` or
// the unrelated npm package `expo-drawer`).
//
// In app/(drawer)/_layout.tsx:
//   <Drawer
//     drawerContent={(props) => (
//       <Drawer02 {...props} onSwitchTeam={openTeams} onAccountPress={openAccount} />
//     )}
//     screenOptions={{ drawerStyle: { width: 290 } }}
//   />
//
// The logo, app and team names, items, counts and routes are placeholders: rename them to your
// app's.
import {
  Bell,
  ChevronsUpDown,
  CreditCard,
  FileText,
  Hexagon,
  House,
  LifeBuoy,
  ListChecks,
  Package,
  Settings,
  TicketPercent,
  Users,
  Wallet,
  X,
} from "lucide-react-native";
import { View } from "react-native";

import { Avatar } from "@/components/avatar";
import { Badge } from "@/components/badge";
import { Button } from "@/components/button";
import {
  DrawerContent,
  DrawerFooter,
  DrawerHeader,
  DrawerItem,
  DrawerSection,
  type DrawerContentProps,
} from "@/components/drawer";
import { Icon } from "@/components/icon";
import { Pressable } from "@/components/primitives/pressable";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";

export type Drawer02Props = DrawerContentProps & {
  /** Runs when the team switcher in the footer is pressed. */
  onSwitchTeam?: () => void;
  /** Runs when the user's avatar in the footer is pressed. */
  onAccountPress?: () => void;
};

export function Drawer02({ onSwitchTeam, onAccountPress, ...props }: Drawer02Props) {
  const styles = useStyles();
  return (
    <DrawerContent {...props}>
      <DrawerHeader style={styles.header}>
        <View style={styles.brand}>
          <Icon icon={Hexagon} size="lg" color="primary" />
          <Text variant="h4" numberOfLines={1} style={styles.shrink}>
            Acme
          </Text>
        </View>
        <Button
          variant="ghost"
          size="sm"
          icon={X}
          aria-label="Close menu"
          onPress={() => props.navigation?.closeDrawer()}
        />
      </DrawerHeader>

      <DrawerSection>
        <DrawerItem
          label="Checklist"
          icon={ListChecks}
          href="/checklist"
          tone="accent"
          badge={<Badge label="5 Steps" />}
        />
        <DrawerItem label="Home" icon={House} href="/" />
        <DrawerItem label="Products" icon={Package} href="/products" />
        <DrawerItem label="Payment page" icon={CreditCard} href="/payment-page" />
        <DrawerItem label="Promo codes" icon={TicketPercent} href="/promo-codes" />
        <DrawerItem label="Customers" icon={Users} href="/customers" />
        <DrawerItem label="Payments" icon={Wallet} href="/payments" />
      </DrawerSection>

      {/* Pushed to the bottom of the scrolling area, above the footer. */}
      <DrawerSection style={styles.bottom}>
        <DrawerItem
          label="Notifications"
          icon={Bell}
          href="/notifications"
          badge={<Badge label="+9" />}
        />
        <DrawerItem label="Settings" icon={Settings} href="/settings" />
        <DrawerItem label="Docs" icon={FileText} href="/docs" />
        <DrawerItem label="Help" icon={LifeBuoy} href="/help" />
      </DrawerSection>

      <DrawerFooter style={styles.footer}>
        <Pressable
          role="button"
          aria-label="Switch team, current team Acme Inc"
          onPress={onSwitchTeam}
          pressedStyle={styles.pressed}
          style={[styles.control, styles.team]}
        >
          <View style={styles.teamLogo} />
          <Text variant="label" numberOfLines={1} style={styles.shrink}>
            Acme Inc
          </Text>
          <Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
        </Pressable>
        <Pressable
          role="button"
          aria-label="Account menu"
          onPress={onAccountPress}
          pressedStyle={styles.pressed}
          style={styles.control}
        >
          <Avatar fallback="JD" size="sm" />
          <Icon icon={ChevronsUpDown} size="sm" color="mutedForeground" />
        </Pressable>
      </DrawerFooter>
    </DrawerContent>
  );
}

const useStyles = createStyles((t) => {
  const item = {
    height: t.scaleValue(48),
    borderRadius: t.radius.md,
    paddingHorizontal: t.spacing[3],
    gap: t.spacing[3],
  };
  return {
    header: {
      flexDirection: "row",
      alignItems: "center",
      justifyContent: "space-between",
      borderBottomWidth: t.borderWidth.default,
      borderBottomColor: t.colors.border,
    },
    brand: { flex: 1, flexDirection: "row", alignItems: "center", gap: t.spacing[2] },
    shrink: { flexShrink: 1 },
    bottom: { marginTop: "auto" },
    footer: {
      flexDirection: "row",
      alignItems: "center",
      justifyContent: "space-between",
      gap: t.spacing[2],
      borderTopWidth: t.borderWidth.default,
      borderTopColor: t.colors.border,
    },
    // The footer controls share a DrawerItem's height, radius and padding (drawer.item Slot).
    control: {
      ...item,
      flexDirection: "row",
      alignItems: "center",
      gap: t.spacing[2],
      borderCurve: "continuous",
    },
    team: { flexShrink: 1 },
    teamLogo: {
      width: t.iconSize.lg,
      height: t.iconSize.lg,
      borderRadius: t.radius.sm,
      backgroundColor: t.colors.primary,
    },
    pressed: { backgroundColor: t.colors.muted },
  };
});
```
