Skip to content
Merged
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
28 changes: 28 additions & 0 deletions public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ To get the higher limit, authenticate with an API key (recommended for any autom

---

## Web URLs (for linking humans to a view)

The API paths above are for fetching data. When you need to point a person at
something in the browser, use these page URLs. The rule: a version is a path
prefix, a view is a path segment, and omitting the version prefix means "latest".
Semver appears WITHOUT the leading "v" in page URLs (/v/1.2.0), unlike API paths
(/versions/v1.2.0). Both forms resolve, but the bare form is canonical.

https://underlay.org/:owner → account/organization profile
https://underlay.org/:owner/:slug → collection overview (latest): README, record types, stats
https://underlay.org/:owner/:slug/records → browse records (latest) — add ?type=TypeName to pick a type
https://underlay.org/:owner/:slug/schemas → schemas (latest)
https://underlay.org/:owner/:slug/files → files (latest)
https://underlay.org/:owner/:slug/versions → version history
https://underlay.org/:owner/:slug/versions/compare?from=v1.0.0&to=v1.1.0 → diff two versions
https://underlay.org/:owner/:slug/v/1.2.0 → overview pinned to v1.2.0
https://underlay.org/:owner/:slug/v/1.2.0/records → records in v1.2.0 (?type=TypeName)
https://underlay.org/:owner/:slug/v/1.2.0/schemas → schemas as pinned in v1.2.0
https://underlay.org/:owner/:slug/v/1.2.0/files → files in v1.2.0
https://underlay.org/records/:hash → a record's content-addressed permalink + provenance
https://underlay.org/schemas/:id → a schema's detail page + which collections use it

Prefer a version-pinned URL when citing data, since /records (no prefix) follows
the latest version and its contents will change on the next push. ARK identifiers
(ark:NAAN/id.v1.2.0) also resolve to these pages and are the most durable option.

---

## Reading Data

### Browse collections
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LoaderFunctionArgs, RouteObject } from 'react-router'

import { RouteErrorBoundary } from '~/components/NotFound'
import Root from '~/components/Root'
import { fetchBase } from '~/lib/fetch-base'
import { buildDataRoutes } from '~/route-gen'
Expand Down Expand Up @@ -32,6 +33,7 @@ export const routes: RouteObject[] = [
{
id: 'root',
Component: Root,
ErrorBoundary: RouteErrorBoundary,
loader: rootLoader,
children: [...buildDataRoutes(components, dataModules), { path: '*', lazy: NotFound }],
},
Expand Down
3 changes: 2 additions & 1 deletion src/api/ark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ export async function resolve(c: Context<AuthEnv>) {
}

if (version !== undefined && versionRow) {
const url = `/${ownerSlug}/${collectionSlug}/v/${versionRow.semver}`
// Page URLs use the bare semver form (/v/1.0.0); stored semver is "v1.0.0".
const url = `/${ownerSlug}/${collectionSlug}/v/${versionRow.semver.replace(/^v/, '')}`
return c.json({
type: 'redirect' as const,
url,
Expand Down
2 changes: 1 addition & 1 deletion src/api/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ const app = new Hono<AuthEnv>()
})
.from(schema.collections)
.where(and(...conditions))
.orderBy(schema.collections.updatedAt)
.orderBy(desc(schema.collections.updatedAt))

return c.json(results)
},
Expand Down
23 changes: 11 additions & 12 deletions src/components/ApiPlayground.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useCallback, useState } from 'react'

import { Button } from '~/components/ui'

interface Collection {
id: string
slug: string
Expand Down Expand Up @@ -152,7 +154,7 @@ export function ApiPlayground({ slug, collections }: ApiPlaygroundProps) {
setSelectedEndpoint(0)
setResponse(null)
}}
className="bg-parchment border-rule focus:border-ink border px-2 py-1 text-sm focus:outline-none"
className="bg-parchment border-rule focus:border-ink rounded-control cursor-pointer border px-2 py-1 text-sm focus:outline-none"
>
{collections.map((c) => (
<option key={c.id} value={c.slug}>
Expand All @@ -169,7 +171,7 @@ export function ApiPlayground({ slug, collections }: ApiPlaygroundProps) {
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Paste key to test it (optional)"
className="bg-parchment border-rule focus:border-ink w-52 border px-2 py-1 font-mono text-xs focus:outline-none"
className="bg-parchment border-rule focus:border-ink rounded-control w-52 border px-2 py-1 font-mono text-xs focus:outline-none"
/>
</div>
</div>
Expand All @@ -186,7 +188,7 @@ export function ApiPlayground({ slug, collections }: ApiPlaygroundProps) {
setSelectedEndpoint(i)
setResponse(null)
}}
className={`block w-full rounded px-2 py-1.5 text-left text-xs transition-colors ${
className={`rounded-control block w-full cursor-pointer px-2 py-1.5 text-left text-xs transition-colors ${
selectedEndpoint === i
? 'bg-parchment-dark font-medium'
: 'hover:bg-parchment-dark'
Expand Down Expand Up @@ -244,20 +246,17 @@ export function ApiPlayground({ slug, collections }: ApiPlaygroundProps) {

{/* Action bar */}
<div className="border-rule flex items-center gap-2 border-b px-3 py-2">
<button
onClick={sendRequest}
disabled={loading}
className="bg-ink text-parchment px-3 py-1 text-xs font-medium hover:opacity-90 disabled:opacity-50"
>
<Button size="sm" onClick={sendRequest} disabled={loading}>
{loading ? 'Sending…' : 'Send'}
</button>
<button
</Button>
<Button
variant="secondary"
size="sm"
onClick={copyAsCurl}
className="border-rule text-ink-muted hover:text-ink border px-2 py-1 text-xs"
title="Copy as cURL (with Bearer token placeholder)"
>
{copied ? 'Copied!' : 'Copy cURL'}
</button>
</Button>
<span className="text-ink-muted ml-auto hidden text-[10px] sm:inline">
{token.trim() ? 'Using API key' : 'Using your session'}
</span>
Expand Down
5 changes: 4 additions & 1 deletion src/components/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export default function BaseLayout({ children }: { children: React.ReactNode })
</a>
</div>
<div>
<a href="https://github.com/knowledgefutures/underlay" className="hover:text-ink">
<a
href="https://github.com/knowledgefutures/underlay"
className="hover:text-ink underline"
>
GitHub
</a>
</div>
Expand Down
20 changes: 10 additions & 10 deletions src/components/CollectionExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export default function CollectionExplorer() {
<li>
<button
onClick={() => handleTagClick(null)}
className={`w-full rounded-sm px-2.5 py-1.5 text-left text-sm transition-colors ${
className={`rounded-control w-full px-2.5 py-1.5 text-left text-sm transition-colors ${
!selectedTag
? 'bg-parchment-dark text-ink font-medium'
: 'text-ink-muted hover:bg-parchment-dark/50 hover:text-ink'
Expand All @@ -187,7 +187,7 @@ export default function CollectionExplorer() {
<li key={tag}>
<button
onClick={() => handleTagClick(tag)}
className={`w-full rounded-sm px-2.5 py-1.5 text-left text-sm transition-colors ${
className={`rounded-control w-full px-2.5 py-1.5 text-left text-sm transition-colors ${
selectedTag === tag
? 'bg-parchment-dark text-ink font-medium'
: 'text-ink-muted hover:bg-parchment-dark/50 hover:text-ink'
Expand Down Expand Up @@ -221,15 +221,15 @@ export default function CollectionExplorer() {
<input
type="search"
placeholder="Search collections..."
className="bg-parchment border-rule placeholder:text-ink-muted focus:border-ink w-full rounded-sm border py-2 pr-3 pl-10 text-sm focus:outline-none"
className="bg-parchment border-rule placeholder:text-ink-muted focus:border-ink rounded-control w-full border py-2 pr-3 pl-10 text-sm focus:outline-none"
value={query}
onChange={(e) => handleInput(e.target.value)}
/>
</div>
<select
value={sort}
onChange={(e) => handleSortChange(e.target.value)}
className="bg-parchment border-rule text-ink-muted rounded-sm border px-3 py-2 text-sm focus:outline-none"
className="bg-parchment border-rule text-ink-muted focus:border-ink rounded-control cursor-pointer border px-3 py-2 text-sm focus:outline-none"
>
<option value="featured">Featured</option>
<option value="records">Most records</option>
Expand All @@ -249,7 +249,7 @@ export default function CollectionExplorer() {
<Link
key={`feat-${c.ownerSlug}/${c.slug}`}
to={`/${c.ownerSlug}/${c.slug}`}
className="border-rule hover:border-ink-muted/50 group rounded-sm border p-4 transition-all hover:shadow-sm"
className="border-rule hover:border-ink-muted/50 rounded-surface group border p-4 transition-all hover:shadow-sm"
>
<div className="flex items-baseline gap-1.5">
<span className="text-ink-muted text-xs">{c.ownerSlug}/</span>
Expand All @@ -263,7 +263,7 @@ export default function CollectionExplorer() {
<div className="text-ink-muted mt-3 flex items-center gap-3 text-xs tabular-nums">
{c.recordCount != null && <span>{formatCount(c.recordCount)} records</span>}
{c.tags && c.tags.length > 0 && (
<span className="bg-parchment-dark rounded px-1.5 py-0.5 text-[10px] leading-none">
<span className="bg-parchment-dark rounded-control px-1.5 py-0.5 text-[10px] leading-none">
{c.tags[0]}
</span>
)}
Expand All @@ -279,7 +279,7 @@ export default function CollectionExplorer() {
<div className="mb-4 flex flex-wrap gap-1.5 md:hidden">
<button
onClick={() => handleOwnerClick(null)}
className={`rounded-full px-3 py-1 text-xs transition-colors ${
className={`rounded-control px-3 py-1 text-xs transition-colors ${
!selectedOwner
? 'bg-ink text-parchment'
: 'bg-parchment-dark text-ink-muted hover:text-ink'
Expand All @@ -291,7 +291,7 @@ export default function CollectionExplorer() {
<button
key={o.slug}
onClick={() => handleOwnerClick(o.slug)}
className={`rounded-full px-3 py-1 text-xs transition-colors ${
className={`rounded-control px-3 py-1 text-xs transition-colors ${
selectedOwner === o.slug
? 'bg-ink text-parchment'
: 'bg-parchment-dark text-ink-muted hover:text-ink'
Expand Down Expand Up @@ -326,7 +326,7 @@ export default function CollectionExplorer() {
<Link
key={`${c.ownerSlug}/${c.slug}`}
to={`/${c.ownerSlug}/${c.slug}`}
className="border-rule hover:border-ink-muted/50 group flex items-start gap-4 rounded-sm border px-3 py-2.5 transition-all hover:shadow-sm"
className="border-rule hover:border-ink-muted/50 rounded-surface group flex items-start gap-4 border px-3 py-2.5 transition-all hover:shadow-sm"
>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-1.5">
Expand All @@ -337,7 +337,7 @@ export default function CollectionExplorer() {
{c.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="bg-parchment-dark text-ink-muted rounded px-1.5 py-0.5 text-[10px] leading-none"
className="bg-parchment-dark text-ink-muted rounded-control px-1.5 py-0.5 text-[10px] leading-none"
>
{tag}
</span>
Expand Down
23 changes: 12 additions & 11 deletions src/components/CreateMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import { useEffect, useRef, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { Link } from 'react-router'

import { useDismissable } from '~/lib/use-dismissable'

export default function CreateMenu() {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)

useEffect(() => {
if (!open) return
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [open])
useDismissable(
open,
useCallback(() => setOpen(false), []),
ref,
)

return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="border-rule bg-parchment-dark hover:bg-rule/30 cursor-pointer rounded border px-2.5 py-1 text-xs transition-colors"
aria-expanded={open}
aria-haspopup="menu"
className="border-rule bg-parchment-dark hover:bg-rule/30 rounded-control cursor-pointer border px-2.5 py-1 text-xs transition-colors"
>
New +
</button>
{open && (
<div className="bg-parchment border-rule absolute top-full right-0 z-50 mt-1.5 min-w-[10rem] overflow-hidden rounded border shadow-sm">
<div className="bg-parchment border-rule rounded-control absolute top-full right-0 z-50 mt-1.5 min-w-[10rem] overflow-hidden border shadow-sm">
<Link
to="/new"
onClick={() => setOpen(false)}
Expand Down
Loading
Loading