Blocks · Application
Application 6
In-product surfaces: settings, members, empty states, utilities, and support chat.
Notifications
Workspace
PlanPro
---
import { Card, Switch, Input, Tag, Button } from "astro-ui";
---
<div class="block-canvas-grid">
<Card>
<h3 slot="header">Notifications</h3>
<div class="block-settings">
<Switch name="product-updates" checked label="Product updates" />
<Switch name="weekly-digest" label="Weekly digest" />
<Switch name="security-alerts" checked label="Security alerts" />
</div>
</Card>
<Card>
<h3 slot="header">Workspace</h3>
<div class="block-form">
<div class="block-field">
<label for="workspace">Workspace name</label>
<Input name="workspace" id="workspace" value="Acme Inc" />
</div>
<div class="block-form-row">
<span class="block-plan-label">Plan</span>
<Tag variant="primary">Pro</Tag>
</div>
<Button variant="outline">Save changes</Button>
</div>
</Card>
</div>
<style is:global>
.block-canvas-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--ui-space-4); }
.block-settings { display: grid; gap: var(--ui-space-3); }
.block-form { display: grid; gap: var(--ui-space-3); }
.block-field { display: grid; gap: var(--ui-space-1); }
.block-field label { font-size: var(--ui-text-sm); font-weight: var(--ui-font-medium); }
.block-form-row { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-space-3); }
.block-plan-label { font-size: var(--ui-text-sm); color: var(--ui-muted-foreground); }
</style>Team members
Member list with avatars, role badges, and invite action.
Members
- Parsa MownerOwner
- Devin VmaintainerMaintainer
- Robin HmemberContributor
---
import { Card, Avatar, Badge, Button } from "astro-ui";
---
<Card>
<h3 slot="header">Members</h3>
<ul class="block-team">
{[
{ name: "Parsa M", role: "Owner", badge: "owner" },
{ name: "Devin V", role: "Maintainer", badge: "maintainer" },
{ name: "Robin H", role: "Contributor", badge: "member" },
].map((member) => (
<li class="block-team-row">
<Avatar name={member.name} size="sm" />
<span class="block-team-name">{member.name}</span>
<Badge
variant={member.badge === "owner" ? "primary" : member.badge === "maintainer" ? "info" : "muted"}
size="sm"
>
{member.badge}
</Badge>
<span class="block-team-role">{member.role}</span>
</li>
))}
</ul>
<Button slot="footer" variant="outline" size="sm">Invite member</Button>
</Card>
<style is:global>
.block-team { display: grid; gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
.block-team-row { display: flex; align-items: center; gap: var(--ui-space-3); padding: var(--ui-space-2) 0; }
.block-team-name { font-size: var(--ui-text-sm); font-weight: var(--ui-font-medium); }
.block-team-role { margin-inline-start: auto; font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
</style>Empty state
Zero-data panel with icon, copy, and next-step actions.
No projects yet
Create your first project to see analytics, deploys, and team activity here.
---
import { Empty, Button } from "astro-ui";
---
<Empty>
<span slot="media" class="block-empty-icon" aria-hidden="true">◈</span>
<span slot="title">No projects yet</span>
<span slot="description">
Create your first project to see analytics, deploys, and team activity here.
</span>
<Button size="sm">New project</Button>
<Button size="sm" variant="ghost" href="/docs/get-started/">Read the guide</Button>
</Empty>
<style is:global>
.block-empty-icon { display: grid; place-items: center; font-size: 1.75rem; color: var(--ui-primary); }
</style>Empty state — with avatar
Empty state that leads with a single avatar and a next-step CTA.
No team members yet
Invite your first teammate to start collaborating on projects, deploys, and analytics.
---
import { Empty, Avatar, Button } from "astro-ui";
---
<Empty size="lg" ariaLabel="No team members yet">
<span slot="media" class="block-empty-avatar" aria-hidden="true">
<Avatar name="Your team" size="xl" variant="primary" ring="primary" />
</span>
<span slot="title">No team members yet</span>
<span slot="description">Invite your first teammate to start collaborating on projects, deploys, and analytics.</span>
<Button size="sm">Invite member</Button>
<Button size="sm" variant="ghost" href="/docs/get-started/">Read the guide</Button>
</Empty>
<style is:global>
.block-empty-avatar { display: grid; place-items: center; }
/* Empty handles layout; Avatar sits in the media slot for a human-centered empty state. */
</style>Password generator
Generate a strong password: length slider, character-set switches, and a one-click generate action.
Generate Password
Length 16
Settings
---
import { Card, Slider, Switch, Button } from "astro-ui";
---
<div class="passgen">
<Card class="passgen__card">
<h3 slot="header" class="passgen__title">Generate Password</h3>
<output class="passgen__out" data-passgen-output aria-live="polite">CLICK GENERATE</output>
<div class="passgen__field">
<span class="passgen__lab">Length <b data-passgen-length>16</b></span>
<Slider name="passgen-length" id="passgen-length" min={4} max={32} value={16}
ariaStartLabel="Password length" />
<div class="passgen__bounds" aria-hidden="true">
<span>4</span><span>32</span>
</div>
</div>
<div class="passgen__field">
<span class="passgen__lab">Settings</span>
<div class="passgen__opts">
<Switch name="passgen-upper" checked label="Include Uppercase" />
<Switch name="passgen-lower" checked label="Include Lowercase" />
<Switch name="passgen-num" checked label="Include Numbers" />
<Switch name="passgen-sym" label="Include Symbols" />
</div>
</div>
<Button variant="primary" data-passgen-generate class="passgen__btn">
Generate Password
</Button>
</Card>
</div>
<script>
// Wire every generator on the page: length readout + generate click.
document.querySelectorAll("[data-passgen-generate]").forEach((btn) => {
const root = btn.closest(".passgen__card");
if (!root) return;
const output = root.querySelector("[data-passgen-output]");
const lenOut = root.querySelector("[data-passgen-length]");
const range = root.querySelector("input[type='range']");
const boxes = Array.from(root.querySelectorAll("input[type='checkbox']"));
const SETS = [
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"abcdefghijklmnopqrstuvwxyz",
"0123456789",
"!@#$%^&*()_+-=[]{};:,.<>?/",
];
btn.addEventListener("click", () => {
if (!output || !range) return;
const len = Number(range.value) || 16;
const pool = boxes.map((b, i) => (b.checked ? SETS[i] : "")).join("");
if (!pool) {
output.textContent = "SELECT AT LEAST ONE SET";
return;
}
const bytes = crypto.getRandomValues(new Uint32Array(len));
let pass = "";
for (let i = 0; i < len; i++) pass += pool[bytes[i] % pool.length];
output.textContent = pass;
});
range.addEventListener("input", () => {
if (lenOut) lenOut.textContent = range.value;
});
});
</script>
<style is:global>
.passgen { display: grid; justify-items: center; }
.passgen__card { inline-size: min(24rem, 100%); }
.passgen__title { margin: 0; color: var(--ui-foreground); font-size: var(--ui-text-lg); font-weight: var(--ui-font-semibold); }
.passgen__out { display: block; padding: var(--ui-space-3) var(--ui-space-4); margin-block-end: var(--ui-space-4); background-color: var(--ui-muted); border-radius: var(--ui-radius-md); color: var(--ui-foreground); font-size: var(--ui-text-md); letter-spacing: 0.08em; text-align: center; overflow-wrap: anywhere; }
.passgen__field { display: grid; gap: var(--ui-space-2); margin-block-end: var(--ui-space-4); }
.passgen__lab { color: var(--ui-muted-foreground); font-size: var(--ui-text-xs); font-weight: var(--ui-font-semibold); letter-spacing: 0.08em; text-transform: uppercase; }
.passgen__lab b { color: var(--ui-foreground); }
.passgen__bounds { display: flex; justify-content: space-between; color: var(--ui-muted-foreground); font-size: var(--ui-text-xs); }
.passgen__opts { display: grid; gap: var(--ui-space-2); }
.passgen__btn { inline-size: 100%; margin-block-start: var(--ui-space-2); }
</style>Support chat thread
Operator + customer conversation: grouped bubbles, reactions, quick-reply buttons, and a composer.
Mira · SupportTypically replies in minutes
Online👋 Hi! I'm Mira from support. I see ticket #4821 opened on your account — the double charge on
last month's invoice, right?
I've already pulled up the billing log.
Yes! My card was charged twice for the renewal 🙁
(and the renewal email never arrived either)
Confirmed — duplicate transaction at 14:02. Refund approved, lands in 3–5 business days.
Amazing, thank you! Will I get a confirmation email?
You will — and here's the refund reference for your records:
RFND-2026-09482All set on my side! I'll stay in this thread if anything else comes up. Quick actions before
you go:
Ticket #4821 marked as resolved · Mira · 09:21
---
import {
Bubble,
BubbleContent,
BubbleGroup,
BubbleReactions,
Avatar,
Badge,
Button,
Input,
} from "astro-ui";
---
<div class="chat">
<div class="chat__head">
<Avatar name="Mira Chen" size="sm" status="online" />
<div class="chat__who">
<span class="chat__name">Mira · Support</span>
<span class="chat__sub">Typically replies in minutes</span>
</div>
<Badge variant="success" size="sm">Online</Badge>
</div>
<div class="chat__thread">
<p class="chat__meta">Today · 09:12</p>
<!-- Operator turn: consecutive messages collapse into one group -->
<div class="chat__turn">
<Avatar name="Mira Chen" size="sm" />
<BubbleGroup ariaLabel="Messages from Mira" class="chat__group">
<Bubble align="start" variant="muted">
<BubbleContent>👋 Hi! I'm Mira from support. I see ticket #4821 opened on your account — the double charge on last month's invoice, right?</BubbleContent>
</Bubble>
<Bubble align="start" variant="muted">
<BubbleContent>I've already pulled up the billing log.</BubbleContent>
</Bubble>
</BubbleGroup>
</div>
<p class="chat__meta chat__meta--end">You · 09:14</p>
<div class="chat__turn chat__turn--end">
<BubbleGroup ariaLabel="Messages from you" class="chat__group">
<Bubble align="end" variant="primary">
<BubbleContent>Yes! My card was charged twice for the renewal 🙁</BubbleContent>
</Bubble>
<Bubble align="end" variant="primary">
<BubbleContent>(and the renewal email never arrived either)</BubbleContent>
<BubbleReactions side="bottom" align="end" ariaLabel="Reactions">
<button type="button" aria-pressed="true" aria-label="Heart, 1 reaction">
<span aria-hidden="true">❤️</span> 1
</button>
</BubbleReactions>
</Bubble>
</BubbleGroup>
<Avatar name="Sam Visitor" size="sm" />
</div>
<!-- Keep alternating turns; add row-gap so reaction pills have room -->
<p class="chat__meta">Mira · 09:15</p>
<div class="chat__turn">
<Avatar name="Mira Chen" size="sm" />
<Bubble align="start" variant="muted">
<BubbleContent>Confirmed — duplicate transaction at 14:02. Refund approved, lands in 3–5 business days.</BubbleContent>
</Bubble>
</div>
<p class="chat__meta chat__meta--end">You · 09:16</p>
<div class="chat__turn chat__turn--end">
<Bubble align="end" variant="primary">
<BubbleContent>Amazing, thank you! Will I get a confirmation email?</BubbleContent>
<BubbleReactions side="bottom" align="start" ariaLabel="Reactions">
<span aria-hidden="true">🙏 2</span>
</BubbleReactions>
</Bubble>
</div>
<p class="chat__meta">Mira · 09:17</p>
<div class="chat__turn">
<Avatar name="Mira Chen" size="sm" />
<BubbleGroup ariaLabel="Messages from Mira" class="chat__group">
<Bubble align="start" variant="muted">
<BubbleContent>You will — and here's the refund reference for your records:</BubbleContent>
</Bubble>
<Bubble align="start" variant="outline">
<BubbleContent><code>RFND-2026-09482</code></BubbleContent>
</Bubble>
</BubbleGroup>
</div>
<!-- Interactive bubble: a real button the user can pick -->
<p class="chat__meta chat__meta--end">You · 09:18</p>
<div class="chat__turn chat__turn--end">
<Bubble align="end" variant="tinted">
<button type="button" class="aui-bubble__content">Perfect — anything else I should know?</button>
</Bubble>
</div>
<p class="chat__meta">Mira · 09:19</p>
<div class="chat__turn">
<Avatar name="Mira Chen" size="sm" />
<Bubble align="start" variant="muted">
<BubbleContent>All set on my side! I'll stay in this thread if anything else comes up. Quick actions before you go:</BubbleContent>
</Bubble>
</div>
<!-- Suggested user actions -->
<div class="chat__quick">
<Button variant="outline" size="sm" shape="rounded">Email me the transcript</Button>
<Button variant="outline" size="sm" shape="rounded">Rate this chat</Button>
<Button variant="primary" size="sm" shape="rounded">Open new ticket</Button>
</div>
<!-- Ghost system event closes the thread -->
<div class="chat__resolved">
<Bubble align="start" variant="ghost">
<BubbleContent>Ticket #4821 marked as <strong>resolved</strong> · Mira · 09:21</BubbleContent>
</Bubble>
</div>
</div>
<div class="chat__composer">
<Input name="chat-message" placeholder="Type and press enter…" ariaLabel="Message" />
<Button variant="primary" size="icon" shape="rounded" ariaLabel="Send message">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
</Button>
</div>
</div>
<style is:global>
.chat { display: grid; gap: var(--ui-space-3); max-inline-size: 34rem; }
.chat__head { display: flex; align-items: center; gap: var(--ui-space-2); padding-bottom: var(--ui-space-2); border-bottom: 1px solid var(--ui-border); }
.chat__who { display: grid; gap: 2px; margin-inline-end: auto; }
.chat__name { font-size: var(--ui-text-sm); font-weight: var(--ui-font-semibold); color: var(--ui-foreground); }
.chat__sub { font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
.chat__thread { display: flex; flex-direction: column; gap: 1.75rem; max-block-size: 30rem; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.chat__meta { margin: 0; font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
.chat__meta--end { text-align: end; }
.chat__turn { display: flex; align-items: flex-end; gap: var(--ui-space-2); }
.chat__turn--end { flex-direction: row-reverse; }
.chat__group { gap: var(--ui-space-1); }
.chat__thread :is(.aui-bubble__content p) { margin: 0; }
.chat__thread :is(.aui-bubble__content code) { padding: 0.05em 0.35em; background: color-mix(in srgb, currentcolor 10%, transparent); border-radius: var(--ui-radius-sm); font-family: var(--ui-font-mono); font-size: 0.85em; }
.chat__quick { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
.chat__resolved { padding-inline-start: var(--ui-space-2); }
.chat__composer { display: flex; align-items: center; gap: var(--ui-space-2); padding-top: var(--ui-space-2); border-top: 1px solid var(--ui-border); }
.chat__composer :is(input, .aui-input) { flex: 1; }
</style>