Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/site/src/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ import { FormUxGallery } from '@/components/site/form-ux-gallery'
import { LatexUnitsDemo } from '@/components/site/latex-units-demo'
import { ParsePlayground } from '@/components/site/parse-playground'
import { PerformanceSection } from '@/components/site/performance-section'
import { RemindMePopoverBlock } from '@/components/site/remind-me-popover'
import { SemanticTokenHighlighter } from '@/components/site/semantic-token-highlighter'
import {
StrictnessVariantWall,
SystemNumberFormatVariantWall,
} from '@/components/site/variant-walls'
import { WorkflowTriggerBlock } from '@/components/site/workflow-trigger-block'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import {
Expand Down Expand Up @@ -864,6 +867,17 @@ export default async function Home() {
</div>
<CodeBlock code={gridColumnSnippet} filename="columns.ts" lang="ts" />
<DataGridDemo />

<div className="flex min-w-0 flex-col gap-3 pt-6">
<SubHeading id="one-schema-workflow">Thresholds out of prose</SubHeading>
<p className="text-muted-foreground text-sm">
Automation prompts mix nouns a rules engine already knows with thresholds it does
not. <Code>findQuantities</Code> pulls the bounds (<Code>below $10k</Code>,{' '}
<Code>over 15 minutes</Code>) out of the sentence with spans into the original
string and the same issues a form field would see.
</p>
</div>
<WorkflowTriggerBlock />
</Section>

<Section
Expand Down Expand Up @@ -943,6 +957,27 @@ export default async function Home() {
</p>
</div>
<CalendarFieldDemo />

<div className="flex min-w-0 flex-col gap-3 pt-6">
<SubHeading id="dates-semantic-tokens">Color only where it read</SubHeading>
<p className="text-muted-foreground text-sm">
Lingo reads one expression at a time and reports one span per result, so a sentence
has to be pre-segmented before it can be colored. The demo proposes slices with
regexes and lets <Code>parseDate</Code>, <Code>parseDateRange</Code>, and{' '}
<Code>parseDuration</Code> confirm each one — a slice lingo declines stays plain.
</p>
</div>
<SemanticTokenHighlighter />

<div className="flex min-w-0 flex-col gap-3 pt-6">
<SubHeading id="dates-remind-me">Presets and prose, one reader</SubHeading>
<p className="text-muted-foreground text-sm">
Quick presets and a free-text field share one reader, so the row a preset shows is
the same reading typing its phrase would give. A phrase lingo cannot read shows
lingo&apos;s own message instead of a made-up time.
</p>
</div>
<RemindMePopoverBlock />
</Section>

<Section
Expand Down
45 changes: 45 additions & 0 deletions apps/site/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@
--motion-moderate: 280ms;
--motion-slow: 380ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
/* Token-category tints: the one place the demos leave the achromatic scale.
Four hues, one per category the highlighter can confirm through lingo. */
--token-date: oklch(0.55 0.16 255);
--token-time: oklch(0.6 0.15 65);
--token-duration: oklch(0.55 0.14 155);
--token-quantity: oklch(0.55 0.17 305);
color-scheme: light;
}

Expand Down Expand Up @@ -452,6 +458,10 @@
--command-surface-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.035), var(--surface-ring),
0 16px 36px rgb(0 0 0 / 0.22);
--token-date: oklch(0.76 0.12 255);
--token-time: oklch(0.8 0.13 75);
--token-duration: oklch(0.78 0.12 155);
--token-quantity: oklch(0.78 0.13 305);
color-scheme: dark;
}

Expand Down Expand Up @@ -649,6 +659,41 @@ p {
font-variant-numeric: tabular-nums slashed-zero;
}

.token-chip,
.token-dot {
--token: var(--muted-foreground);
}

.token-chip {
color: color-mix(in oklch, var(--token), var(--foreground) 40%);
background: color-mix(in oklch, var(--token), transparent 88%);
border-color: color-mix(in oklch, var(--token), transparent 64%);
}

.token-dot {
background: var(--token);
}

.token-chip[data-token="date"],
.token-dot[data-token="date"] {
--token: var(--token-date);
}

.token-chip[data-token="time"],
.token-dot[data-token="time"] {
--token: var(--token-time);
}

.token-chip[data-token="duration"],
.token-dot[data-token="duration"] {
--token: var(--token-duration);
}

.token-chip[data-token="quantity"],
.token-dot[data-token="quantity"] {
--token: var(--token-quantity);
}

.code-surface {
--code-surface-background: color-mix(in oklch, var(--card), var(--muted) 48%);
--syntax-comment: #6a737d;
Expand Down
6 changes: 5 additions & 1 deletion apps/site/src/components/site/calendar-field-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ export function CalendarFieldDemo() {
const reading = useMemo(() => read(value, now), [value, now])
const { mode, start, end } = reading
const twoUp = mode === 'range'
// The JSON prints UTC instants, and SSR_NOW is a local wall-clock time: the
// same wall clock is a different instant on the server and in the visitor's
// zone, so the panel waits for hydration instead of tripping React #418.
const output = hydrated ? JSON.stringify(reading.result, null, 2) : ''

// The calendar follows the parse unless the reader has paged away from it.
const anchor = useMemo(
Expand Down Expand Up @@ -287,7 +291,7 @@ export function CalendarFieldDemo() {
return (
<DemoFrame
caption="One field. The reading picks the widget — a day, a span, or a slot."
details={<JsonView label="Output" value={JSON.stringify(reading.result, null, 2)} />}
details={<JsonView label="Output" value={output} />}
detailsLabel="Output"
stageClassName="min-h-[34rem] justify-start"
title="Adaptive date field"
Expand Down
16 changes: 12 additions & 4 deletions apps/site/src/components/site/coverage-explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DemoFrame } from '@/components/site/demo-frame'
import { DocsPane, DocsSplitPane } from '@/components/site/docs-split-pane'
import { JsonView } from '@/components/site/json-view'
import { Readout } from '@/components/site/readout'
import { useHydrated } from '@/components/site/use-hydrated'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Expand Down Expand Up @@ -65,24 +66,31 @@ function clockLabel(date: Date): string {
}

export function CoverageExplorer() {
const hydrated = useHydrated()
const [index, setIndex] = useState(0)
const current = aliasExamples[index] ?? aliasExamples[0]
const result = useMemo(() => lingo(current.text, { kind: current.kind }), [current])
const aliasCopyText = useMemo(() => JSON.stringify(resultToPlain(result), null, 2), [result])
const fuzzyCopyText = useMemo(() => JSON.stringify(temperatureVocabs, null, 2), [])
// Day words resolve in the visitor's zone ("today" at 12:00Z is already
// tomorrow in Auckland), so the instants cannot match between the server
// and the browser; the column fills in after hydration.
const dateRows = useMemo(
() =>
dateExamples.map((example) => {
const parsed = parseDate(example, {
now: referenceNow,
dayFirst: true,
})
const value = parsed.ok
? parsed.date.toISOString().replace('.000Z', 'Z')
: (parsed.issues[0]?.code ?? 'UNSUPPORTED_DATE')
let value = ''
if (!parsed.ok) {
value = parsed.issues[0]?.code ?? 'UNSUPPORTED_DATE'
} else if (hydrated) {
value = parsed.date.toISOString().replace('.000Z', 'Z')
}
return { example, parsed, value }
}),
[],
[hydrated],
)
const dateCopyText = useMemo(
() =>
Expand Down
24 changes: 24 additions & 0 deletions apps/site/src/components/site/landing-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { ChevronDownIcon } from 'lucide-react'
import Link from 'next/link'

import { CodeBlock } from '@/components/site/code-block'
import { RemindMePopoverBlock } from '@/components/site/remind-me-popover'
import { SemanticTokenHighlighter } from '@/components/site/semantic-token-highlighter'
import { WorkflowTriggerBlock } from '@/components/site/workflow-trigger-block'
import { Badge } from '@/components/ui/badge'
import {
Table,
Expand Down Expand Up @@ -259,6 +262,27 @@ export function LandingSections() {
</p>
</SectionShell>

<SectionShell
explainer="Regexes propose slices of a sentence; parseDate, parseDateRange, and parseDuration confirm each one. A color only appears where lingo actually read something."
title="Tokens you can trust"
>
<SemanticTokenHighlighter />
</SectionShell>

<SectionShell
explainer="A scheduling popover where the presets and the free-text field go through the same reader — so 'tomorrow' and 'in 3 days at 8am' resolve the same way."
title="Remind me, in plain words"
>
<RemindMePopoverBlock />
</SectionShell>

<SectionShell
explainer="Automation rules mix nouns a rules engine knows with thresholds it does not. findQuantities pulls the bounds out of the sentence with spans and issues attached."
title="Thresholds out of prose"
>
<WorkflowTriggerBlock />
</SectionShell>

<SectionShell
explainer="One parser powers both sides: the same schema validates a human form and an LLM tool call."
title="For the web. For LLMs."
Expand Down
Loading
Loading