Blocks and templates
Copy-ready page sections built only with Texuli. Each one shows the live result, the texuli add command for the components it uses, and its exact source. Fictional Harbor Notes copy; static HTML with no runtime network.
Hero
An announcement badge, the headline, a short lead and two actions.
Open the hero templateNotes your whole harbor crew can trust
Harbor Notes keeps shift handovers, checklists and incident logs in one place, so the next crew starts where the last one stopped.
Install
texuli add badge buttonSource: HeroSection.svelte
<script lang="ts">
import { Badge } from 'texuli/badge';
import { Button } from 'texuli/button';
/**
* Hero: an announcement badge, the headline, a short lead and two actions.
* Copy-ready: `texuli add badge button`, then adjust the copy, the links and
* the heading level to your page.
*/
let {
primaryHref = '#get-started',
secondaryHref = '#features',
}: { primaryHref?: string; secondaryHref?: string } = $props();
</script>
<section class="hero" aria-labelledby="hero-title">
<Badge variant="secondary">New: shared notebooks</Badge>
<h3 id="hero-title" class="hero__title">Notes your whole harbor crew can trust</h3>
<p class="hero__lead">
Harbor Notes keeps shift handovers, checklists and incident logs in one place, so the next
crew starts where the last one stopped.
</p>
<div class="hero__actions">
<Button href={primaryHref}>Start free</Button>
<Button href={secondaryHref} variant="outline">See features</Button>
</div>
</section>
<style>
.hero {
display: grid;
justify-items: start;
gap: 1rem;
padding-block: clamp(1.5rem, 5vw, 3rem);
}
.hero__title {
margin: 0;
max-inline-size: 22ch;
font-size: clamp(1.75rem, 5vw, 2.75rem);
line-height: 1.15;
}
.hero__lead {
margin: 0;
max-inline-size: 40rem;
color: var(--tx-muted-foreground, GrayText);
font-size: 1.125rem;
}
.hero__actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
</style>
Features
A responsive grid of cards, one per capability.
Open the features templateEverything a crew writes down
Shift handovers
A checklist each crew signs before leaving the dock.
Incident log
Timestamped entries with photos, kept in order.
Offline first
Write on the pier without signal; it syncs later.
Shared notebooks
One notebook per vessel, open to the whole crew.
Search
Find any note by vessel, crew member or date.
Exports
Hand a month of logs to the harbor master as a PDF.
Install
texuli add cardSource: FeaturesSection.svelte
<script lang="ts">
import { Card, CardDescription, CardGrid, CardHeader, CardTitle } from 'texuli/card';
/**
* Features: a responsive grid of cards, one per capability.
* Copy-ready: `texuli add card`, then replace the list.
*/
const features = [
{ title: 'Shift handovers', text: 'A checklist each crew signs before leaving the dock.' },
{ title: 'Incident log', text: 'Timestamped entries with photos, kept in order.' },
{ title: 'Offline first', text: 'Write on the pier without signal; it syncs later.' },
{ title: 'Shared notebooks', text: 'One notebook per vessel, open to the whole crew.' },
{ title: 'Search', text: 'Find any note by vessel, crew member or date.' },
{ title: 'Exports', text: 'Hand a month of logs to the harbor master as a PDF.' },
];
</script>
<section class="features" aria-labelledby="features-title">
<h3 id="features-title" class="features__title">Everything a crew writes down</h3>
<CardGrid minItemWidth="14rem">
{#each features as feature (feature.title)}
<Card>
<CardHeader>
<CardTitle level={4}>{feature.title}</CardTitle>
<CardDescription>{feature.text}</CardDescription>
</CardHeader>
</Card>
{/each}
</CardGrid>
</section>
<style>
.features {
display: grid;
gap: 1.25rem;
}
.features__title {
margin: 0;
font-size: 1.5rem;
}
</style>
Pricing
Three plans with the middle one featured; prices kept as integer cents.
Open the pricing templateSimple plans for every crew
Deckhand
One vessel, three crew.
$0 forever
- Shift handovers
- Incident log
- 30 days of history
Captain
Most popularFor a working fleet.
$12 per month
- Everything in Deckhand
- Unlimited crew
- Exports to PDF
- One year of history
Harbor
For the whole port.
$49 per month
- Everything in Captain
- Harbor master access
- Unlimited history
Install
texuli add badge button cardSource: PricingSection.svelte
<script lang="ts">
import { Badge } from 'texuli/badge';
import { Button } from 'texuli/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardGrid,
CardHeader,
CardTitle,
} from 'texuli/card';
/**
* Pricing: three plans with the middle one featured. Prices are integer
* cents, formatted for display. Copy-ready: `texuli add badge button card`.
*/
const plans = [
{
id: 'deckhand',
name: 'Deckhand',
cents: 0,
period: 'forever',
blurb: 'One vessel, three crew.',
features: ['Shift handovers', 'Incident log', '30 days of history'],
featured: false,
},
{
id: 'captain',
name: 'Captain',
cents: 1200,
period: 'per month',
blurb: 'For a working fleet.',
features: ['Everything in Deckhand', 'Unlimited crew', 'Exports to PDF', 'One year of history'],
featured: true,
},
{
id: 'harbor',
name: 'Harbor',
cents: 4900,
period: 'per month',
blurb: 'For the whole port.',
features: ['Everything in Captain', 'Harbor master access', 'Unlimited history'],
featured: false,
},
];
const money = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
});
</script>
<section class="pricing" aria-labelledby="pricing-title">
<h3 id="pricing-title" class="pricing__title">Simple plans for every crew</h3>
<CardGrid minItemWidth="15rem">
{#each plans as plan (plan.id)}
<Card variant={plan.featured ? 'elevated' : 'flat'} class="pricing__plan">
<CardHeader>
<div class="pricing__name">
<CardTitle level={4}>{plan.name}</CardTitle>
{#if plan.featured}<Badge>Most popular</Badge>{/if}
</div>
<CardDescription>{plan.blurb}</CardDescription>
</CardHeader>
<CardContent>
<p class="pricing__price">
<span class="pricing__amount">{money.format(plan.cents / 100)}</span>
<span class="pricing__period">{plan.period}</span>
</p>
<ul class="pricing__features">
{#each plan.features as feature (feature)}<li>{feature}</li>{/each}
</ul>
</CardContent>
<CardFooter>
<Button href={`#plan-${plan.id}`} variant={plan.featured ? 'default' : 'outline'}>
Choose {plan.name}
</Button>
</CardFooter>
</Card>
{/each}
</CardGrid>
</section>
<style>
.pricing {
display: grid;
gap: 1.25rem;
}
.pricing__title {
margin: 0;
font-size: 1.5rem;
}
.pricing__name {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.pricing__price {
display: flex;
align-items: baseline;
gap: 0.375rem;
margin: 0 0 0.75rem;
}
.pricing__amount {
font-size: 2rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.pricing__period {
color: var(--tx-muted-foreground, GrayText);
}
.pricing__features {
display: grid;
gap: 0.375rem;
margin: 0;
padding-inline-start: 1.25rem;
}
</style>
FAQ
Questions in a keyboard-operable accordion, one open at a time.
Open the faq templateFrequently asked questions
Install
texuli add tabs-accordionSource: FaqSection.svelte
<script lang="ts">
import { Accordion } from 'texuli/tabs-accordion';
/**
* FAQ: questions as an accordion (one open at a time, keyboard operable).
* Copy-ready: `texuli add tabs-accordion`, then replace the questions.
*/
const questions = [
{
value: 'offline',
title: 'Does Harbor Notes work without signal?',
content: 'Yes. Notes are saved on the device and sync when a connection returns.',
},
{
value: 'crew',
title: 'How many crew members can share a notebook?',
content: 'Deckhand allows three; Captain and Harbor have no limit.',
},
{
value: 'export',
title: 'Can I export the logs?',
content: 'Captain and Harbor export any date range to PDF or CSV.',
},
{
value: 'cancel',
title: 'Can I cancel at any time?',
content: 'Yes. Your notes stay readable on the free plan.',
},
];
</script>
<section class="faq" aria-labelledby="faq-title">
<h3 id="faq-title" class="faq__title">Frequently asked questions</h3>
<Accordion items={questions} type="single" headingLevel={4} />
</section>
<style>
.faq {
display: grid;
gap: 1rem;
max-inline-size: 48rem;
}
.faq__title {
margin: 0;
font-size: 1.5rem;
}
</style>
Sign in
An email and password form with inline validation messages.
Open the sign in templateInstall
texuli add button card field inputSource: AuthSection.svelte
<script lang="ts">
import { Button } from 'texuli/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'texuli/card';
import { Field } from 'texuli/field';
import { Input } from 'texuli/input';
/**
* Sign in: an email and password form in a card, with validation messages.
* Copy-ready: `texuli add button card field input`, then wire `onsubmit` to
* your auth service. This demo checks the fields and sends nothing.
*/
let email = $state('');
let password = $state('');
let submitted = $state(false);
let status = $state('');
const emailError = $derived(
submitted && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? 'Enter an email such as ada@harbor.example.'
: null
);
const passwordError = $derived(
submitted && password.length < 8 ? 'Use at least 8 characters.' : null
);
function submit(event: SubmitEvent): void {
event.preventDefault();
submitted = true;
status = emailError || passwordError ? '' : `Demo only: ${email} would be signed in now.`;
}
</script>
<section class="auth" aria-labelledby="auth-title">
<Card class="auth__card">
<form novalidate onsubmit={submit}>
<CardHeader>
<CardTitle level={3} id="auth-title">Sign in to Harbor Notes</CardTitle>
<CardDescription>Use the email your harbor master invited.</CardDescription>
</CardHeader>
<CardContent class="auth__fields">
<Field label="Email" required error={emailError}>
{#snippet children(control)}
<Input {...control.props} type="email" autocomplete="email" bind:value={email} />
{/snippet}
</Field>
<Field label="Password" required error={passwordError}>
{#snippet children(control)}
<Input {...control.props} type="password" autocomplete="current-password" bind:value={password} />
{/snippet}
</Field>
<p class="auth__status" role="status">{status}</p>
</CardContent>
<CardFooter>
<Button type="submit">Sign in</Button>
</CardFooter>
</form>
</Card>
</section>
<style>
.auth {
display: grid;
justify-items: center;
}
.auth :global(.auth__card) {
inline-size: min(100%, 26rem);
}
.auth :global(.auth__fields) {
display: grid;
gap: 1rem;
}
.auth__status {
margin: 0;
min-block-size: 1.25rem;
color: var(--tx-muted-foreground, GrayText);
font-size: 0.875rem;
}
</style>