Theme

Corporate Travel Hub

Travel booking, approvals, and policy tracking in one DryUI surface.

Total travel spend
$613M
12.4% vs last quarter
Policy compliance
98.2%
+2.1 points this month
Open approvals
14
3 escalations pending
Blocked bookings
4
2 above weekly target

Search results

Use SegmentedControl, ChipGroup, AvatarGroup, and StatCard together for travel-specific booking flows.

SFO → JFK

In policy Direct

United 1942

$612 per traveler
Departure
08:10
Terminal 2
Arrival
16:42
Local time
Duration
5h 32m
12 kg CO2 saved

LAX → BOS

Needs justification 1 stop

Delta 827

$548 per traveler
Departure
09:45
Terminal 2
Arrival
18:25
Local time
Duration
5h 40m
12 kg CO2 saved

Duty of care

Travelers in transit
28
6 red-eye arrivals tonight
Risk zone alerts
3
Weather disruption in the northeast

Traveler cluster

Five travelers arriving in New York before 7pm local time.

Source

This page is rendered from the real demo file shown below.

svelte
<script lang="ts">
  import {
    Avatar,
    Badge,
    Button,
    Card,
    Flex,
    Grid,
    Stack,
    Tabs,
  } from '@dryui/ui';
  import { AvatarGroup } from '@dryui/ui/avatar-group';
  import { ChipGroup } from '@dryui/ui/chip-group';
  import { SegmentedControl } from '@dryui/ui/segmented-control';
  import { StatCard } from '@dryui/ui/stat-card';

  let tripType = $state('round-trip');
  let activeTab = $state('results');
  let filters = $state(['in-policy', 'direct']);

  const flights = [
    {
      route: 'SFO → JFK',
      airline: 'United 1942',
      departure: '08:10',
      arrival: '16:42',
      duration: '5h 32m',
      stops: 'Direct',
      price: '$612',
      policy: { label: 'In policy', color: 'green' as const },
      travelers: ['ER', 'MC', 'JL', 'PP', 'SR'],
    },
    {
      route: 'LAX → BOS',
      airline: 'Delta 827',
      departure: '09:45',
      arrival: '18:25',
      duration: '5h 40m',
      stops: '1 stop',
      price: '$548',
      policy: { label: 'Needs justification', color: 'yellow' as const },
      travelers: ['NB', 'CD', 'AV'],
    },
  ];

  const approvals = [
    { traveler: 'Elena Rossi', trip: 'San Francisco to New York', reason: 'Customer summit', eta: '42m' },
    { traveler: 'Maya Chen', trip: 'Los Angeles to Boston', reason: 'Board workshop', eta: '1h 18m' },
    { traveler: 'Jordan Lee', trip: 'Austin to Seattle', reason: 'Q2 kickoff', eta: '3h 05m' },
  ];
</script>

<Stack gap="lg">
  <Flex justify="between" align="center">
    <Stack gap="sm">
      <h2 class="title">Corporate Travel Hub</h2>
      <p class="subtitle">Travel booking, approvals, and policy tracking in one DryUI surface.</p>
    </Stack>
    <Flex gap="sm" align="center">
      <Button variant="outline" size="sm">Export report</Button>
      <Button size="sm">Create trip</Button>
    </Flex>
  </Flex>

  <Grid columns={4} gap="md">
    <StatCard.Root tone="info">
      <StatCard.Label>Total travel spend</StatCard.Label>
      <StatCard.Value>$613M</StatCard.Value>
      <StatCard.Trend direction="up">12.4% vs last quarter</StatCard.Trend>
    </StatCard.Root>
    <StatCard.Root tone="success">
      <StatCard.Label>Policy compliance</StatCard.Label>
      <StatCard.Value>98.2%</StatCard.Value>
      <StatCard.Trend direction="up">+2.1 points this month</StatCard.Trend>
    </StatCard.Root>
    <StatCard.Root tone="warning">
      <StatCard.Label>Open approvals</StatCard.Label>
      <StatCard.Value>14</StatCard.Value>
      <StatCard.Trend direction="flat">3 escalations pending</StatCard.Trend>
    </StatCard.Root>
    <StatCard.Root tone="danger">
      <StatCard.Label>Blocked bookings</StatCard.Label>
      <StatCard.Value>4</StatCard.Value>
      <StatCard.Trend direction="down">2 above weekly target</StatCard.Trend>
    </StatCard.Root>
  </Grid>

  <Grid columns={3} gap="lg">
    <div class="main-column">
      <Tabs.Root bind:value={activeTab}>
        <Tabs.List>
          <Tabs.Trigger value="results">Travel Results</Tabs.Trigger>
          <Tabs.Trigger value="approvals">Approval Flow</Tabs.Trigger>
        </Tabs.List>

        <Tabs.Content value="results">
          <Card.Root>
            <Card.Header>
              <Stack gap="md">
                <Flex justify="between" align="center" wrap="wrap">
                  <Stack gap="sm">
                    <h3 class="card-title">Search results</h3>
                    <p class="section-copy">Use SegmentedControl, ChipGroup, AvatarGroup, and StatCard together for travel-specific booking flows.</p>
                  </Stack>
                  <SegmentedControl.Root bind:value={tripType}>
                    <SegmentedControl.Item value="one-way">One way</SegmentedControl.Item>
                    <SegmentedControl.Item value="round-trip">Round trip</SegmentedControl.Item>
                    <SegmentedControl.Item value="multi-city">Multi-city</SegmentedControl.Item>
                  </SegmentedControl.Root>
                </Flex>
                <ChipGroup.Root type="multiple" bind:value={filters}>
                  <ChipGroup.Item value="in-policy">In policy</ChipGroup.Item>
                  <ChipGroup.Item value="direct">Direct</ChipGroup.Item>
                  <ChipGroup.Item value="wifi">Wi-fi</ChipGroup.Item>
                  <ChipGroup.Item value="lounge">Lounge access</ChipGroup.Item>
                </ChipGroup.Root>
              </Stack>
            </Card.Header>
            <Card.Content>
              <Stack gap="md">
                {#each flights as flight (flight.route)}
                  <Card.Root>
                    <Card.Content>
                      <Stack gap="md">
                        <Flex justify="between" align="start" wrap="wrap">
                          <Stack gap="sm">
                            <Flex gap="sm" align="center" wrap="wrap">
                              <h4 class="flight-route">{flight.route}</h4>
                              <Badge variant="soft" color={flight.policy.color} size="sm">{flight.policy.label}</Badge>
                              <Badge variant="soft" color="blue" size="sm">{flight.stops}</Badge>
                            </Flex>
                            <p class="section-copy">{flight.airline}</p>
                          </Stack>
                          <div class="price-block">
                            <Stack gap="sm">
                              <span class="price">{flight.price}</span>
                              <span class="section-copy">per traveler</span>
                            </Stack>
                          </div>
                        </Flex>

                        <Grid columns={3} gap="md">
                          <StatCard.Root density="compact">
                            <StatCard.Label>Departure</StatCard.Label>
                            <StatCard.Value>{flight.departure}</StatCard.Value>
                            <StatCard.Trend direction="flat">Terminal 2</StatCard.Trend>
                          </StatCard.Root>
                          <StatCard.Root density="compact">
                            <StatCard.Label>Arrival</StatCard.Label>
                            <StatCard.Value>{flight.arrival}</StatCard.Value>
                            <StatCard.Trend direction="flat">Local time</StatCard.Trend>
                          </StatCard.Root>
                          <StatCard.Root density="compact">
                            <StatCard.Label>Duration</StatCard.Label>
                            <StatCard.Value>{flight.duration}</StatCard.Value>
                            <StatCard.Trend direction="up">12 kg CO2 saved</StatCard.Trend>
                          </StatCard.Root>
                        </Grid>

                        <Flex justify="between" align="center" wrap="wrap">
                          <AvatarGroup count={flight.travelers.length} maxVisible={4} label="Travelers">
                            {#each flight.travelers.slice(0, 4) as traveler (traveler)}
                              <Avatar fallback={traveler} size="sm" />
                            {/each}
                          </AvatarGroup>
                          <Flex gap="sm" align="center">
                            <Button variant="outline" size="sm">Compare fares</Button>
                            <Button size="sm">Select flight</Button>
                          </Flex>
                        </Flex>
                      </Stack>
                    </Card.Content>
                  </Card.Root>
                {/each}
              </Stack>
            </Card.Content>
          </Card.Root>
        </Tabs.Content>

        <Tabs.Content value="approvals">
          <Card.Root>
            <Card.Header>
              <h3 class="card-title">Approval chain</h3>
            </Card.Header>
            <Card.Content>
              <Stack gap="md">
                {#each approvals as approval (approval.traveler)}
                  <Card.Root>
                    <Card.Content>
                      <Flex justify="between" align="start" wrap="wrap">
                        <Stack gap="sm">
                          <div class="approval-name">{approval.traveler}</div>
                          <div class="section-copy">{approval.trip}</div>
                          <Badge variant="soft" color="yellow" size="sm">{approval.reason}</Badge>
                        </Stack>
                        <div class="eta-block">
                          <Stack gap="sm">
                            <Badge variant="outline" color="gray" size="sm">Escalates in {approval.eta}</Badge>
                            <Flex gap="sm">
                              <Button variant="outline" size="sm">Request info</Button>
                              <Button size="sm">Approve</Button>
                            </Flex>
                          </Stack>
                        </div>
                      </Flex>
                    </Card.Content>
                  </Card.Root>
                {/each}
              </Stack>
            </Card.Content>
          </Card.Root>
        </Tabs.Content>
      </Tabs.Root>
    </div>

    <div class="side-column">
      <Card.Root>
        <Card.Header>
          <h3 class="card-title">Duty of care</h3>
        </Card.Header>
        <Card.Content>
          <Stack gap="md">
            <StatCard.Root tone="warning" density="compact">
              <StatCard.Label>Travelers in transit</StatCard.Label>
              <StatCard.Value>28</StatCard.Value>
              <StatCard.Trend direction="flat">6 red-eye arrivals tonight</StatCard.Trend>
            </StatCard.Root>
            <StatCard.Root tone="danger" density="compact">
              <StatCard.Label>Risk zone alerts</StatCard.Label>
              <StatCard.Value>3</StatCard.Value>
              <StatCard.Trend direction="down">Weather disruption in the northeast</StatCard.Trend>
            </StatCard.Root>
            <Card.Root>
              <Card.Content>
                <Stack gap="sm">
                  <h4 class="flight-route">Traveler cluster</h4>
                  <AvatarGroup count={6} maxVisible={5} label="Travelers in New York">
                    <Avatar fallback="ER" size="sm" />
                    <Avatar fallback="MC" size="sm" />
                    <Avatar fallback="JL" size="sm" />
                    <Avatar fallback="PP" size="sm" />
                    <Avatar fallback="SR" size="sm" />
                  </AvatarGroup>
                  <p class="section-copy">Five travelers arriving in New York before 7pm local time.</p>
                </Stack>
              </Card.Content>
            </Card.Root>
          </Stack>
        </Card.Content>
      </Card.Root>
    </div>
  </Grid>
</Stack>

<style>
  .title {
    margin: 0;
    font-size: var(--dry-text-2xl-size);
    font-weight: 700;
  }

  .subtitle,
  .section-copy {
    margin: 0;
    color: var(--dry-color-text-secondary);
    line-height: 1.5;
  }

  .card-title {
    margin: 0;
    font-size: var(--dry-text-lg-size);
    font-weight: 600;
  }

  .flight-route,
  .approval-name {
    margin: 0;
    font-size: var(--dry-text-base-size);
    font-weight: 600;
  }

  .price {
    font-size: var(--dry-text-xl-size);
    font-weight: 700;
    color: var(--dry-color-text);
  }

  .price-block,
  .eta-block {
    text-align: right;
  }

  .main-column {
    grid-column: span 2;
  }

  .side-column {
    grid-column: span 1;
  }

  @media (max-width: 900px) {
    .main-column,
    .side-column {
      grid-column: span 3;
    }

    .price-block,
    .eta-block {
      text-align: left;
    }
  }
</style>