Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3dfb2da
chore(deps): bump kirchDev/workflows from v0.8.0 to v0.8.2
TitusKirch Sep 6, 2026
6a73f4b
feat: serve translated pages as collections of their own
TitusKirch Sep 6, 2026
8ff3639
feat: fall back to the nearest language a page exists in
TitusKirch Sep 6, 2026
282d8a2
feat: build the sidebar from the default language's tree
TitusKirch Sep 6, 2026
dfa63b5
feat: reject a default locale the collections do not share
TitusKirch Sep 6, 2026
e7927d0
docs: document translated pages and record the decision
TitusKirch Sep 6, 2026
22a77aa
docs: translate the getting-started and concepts pages
TitusKirch Sep 6, 2026
3d80516
docs: translate the guides
TitusKirch Sep 6, 2026
9ff4ed5
docs: translate the reference overview, sources and configuration
TitusKirch Sep 6, 2026
eab21ad
docs: translate the remaining reference pages, credits and decisions
TitusKirch Sep 7, 2026
7f72afd
fix: wait for content's parse cache instead of failing beside it
TitusKirch Sep 7, 2026
c119015
feat: draw the devtools panels from plain data
TitusKirch Sep 7, 2026
6fdcbc2
feat: render the devtools panels into the documentation
TitusKirch Sep 7, 2026
9935e7c
docs: document every devtools panel
TitusKirch Sep 7, 2026
849e3b2
fix: ship the devtools panel previews from the layer
TitusKirch Sep 7, 2026
1f36b85
fix: tell two languages apart when picking a collection
TitusKirch Sep 7, 2026
4004037
fix: read a translated partial instead of the original
TitusKirch Sep 7, 2026
e0f72df
fix: resolve a link and its anchor in the linking page's own language
TitusKirch Sep 7, 2026
19cc17e
feat: report what each language carries and what has stood still
TitusKirch Sep 7, 2026
69ebd86
fix: drop allow-same-origin from the devtools preview frame
TitusKirch Sep 7, 2026
d637ffe
fix: emit a devtools tag class without a trailing space
TitusKirch Sep 7, 2026
9f45be6
fix: open content's cache database in wal mode
TitusKirch Sep 7, 2026
ee3057f
docs: document translated partials, the translation report and the sq…
TitusKirch Sep 7, 2026
c4b9ce5
chore: point the community aside at duxt.app
TitusKirch Sep 7, 2026
cd48807
feat: let a site put its own wordmark in the header and footer
TitusKirch Sep 7, 2026
b10e611
chore: give the development site its own icons
TitusKirch Sep 7, 2026
b425646
chore: point the development site's header at its own wordmark
TitusKirch Sep 7, 2026
daebb9d
docs: give the readme a wordmark and an assets section
TitusKirch Sep 7, 2026
d8085d6
docs: hand off the i18n ownership audit
TitusKirch Sep 7, 2026
70d4c63
docs: replace the audit handoff with the verified migration plan
TitusKirch Sep 7, 2026
d1531a1
fix: make a documentation heading its own anchor link
TitusKirch Sep 7, 2026
752a45e
fix: keep the table of contents on the heading being read
TitusKirch Sep 7, 2026
94a3407
fix: mark the docs entry active on every section
TitusKirch Sep 7, 2026
9593eeb
chore: give the development site the brand accent colour
TitusKirch Sep 7, 2026
3b77fde
chore: put the release badge in the accent colour
TitusKirch Sep 7, 2026
d4dd558
chore: use the bracketed mark for the site icons
TitusKirch Sep 7, 2026
3b085c4
docs: record why the icon's brackets are redrawn
TitusKirch Sep 7, 2026
fc94cee
chore: ignore dotenv files
TitusKirch Sep 7, 2026
cc6bdc4
chore: document the development site's environment variables
TitusKirch Sep 7, 2026
8f9095f
chore: give the development site its own name, copy and navigation
TitusKirch Sep 7, 2026
c40677b
fix: stop shipping duxt's own content as layer defaults
TitusKirch Sep 7, 2026
8ac6c11
fix: drop the product name from the devtools preview label
TitusKirch Sep 7, 2026
6160133
fix: name the mcp server from the site's own title
TitusKirch Sep 7, 2026
3d79f34
fix: drop the unreachable heading-anchor label
TitusKirch Sep 7, 2026
5074d80
test: assert the layer ships no site content
TitusKirch Sep 7, 2026
3360ed1
docs: record that the layer ships no site content
TitusKirch Sep 7, 2026
b72af9d
fix: keep the devtools fixtures out of a consumer's index
TitusKirch Sep 7, 2026
aad9986
fix: reject a browser fallback locale the site does not serve
TitusKirch Sep 7, 2026
c1ea266
fix: give the docs tree one navigation landmark, not three
TitusKirch Sep 7, 2026
2f394a5
chore: declare the docs render target and locale trees
TitusKirch Sep 7, 2026
95680a8
docs: document theming and how a site sets its own brand
TitusKirch Sep 7, 2026
e0dde2e
fix: keep the section row still and opaque while the page scrolls
TitusKirch Sep 7, 2026
396b29e
chore: run the pre-commit hook over .vue files too
TitusKirch Sep 7, 2026
51b3bbd
docs: record the theme decision as ADR-0008
TitusKirch Sep 7, 2026
cff48f0
fix: name a sidebar folder after its index page, not its directory
TitusKirch Sep 7, 2026
f9b3c3c
fix: give a page outside every section one branch, not the whole tree
TitusKirch Sep 7, 2026
003bd8d
docs: point the conventions section at the decision log
TitusKirch Sep 7, 2026
ef25d9d
chore: give the decision log a row of its own
TitusKirch Sep 8, 2026
c5846e7
fix: make the brand tokens outrank the layer's on specificity
TitusKirch Sep 8, 2026
4238b52
feat: let a section stand in with an icon for pages that carry none
TitusKirch Sep 8, 2026
67587eb
docs: document pageIcon and give the decision log its own
TitusKirch Sep 8, 2026
7590241
chore: give the decision records a document icon, not a second gavel
TitusKirch Sep 8, 2026
c99e4e7
feat: take the SEO stack from the Nuxt SEO bundle
TitusKirch Sep 8, 2026
3740212
feat: complete the page metadata the SEO modules leave open
TitusKirch Sep 8, 2026
501d0ab
test: assert the built pages carry the SEO tags they must
TitusKirch Sep 8, 2026
b0edb32
fix: drop a link to a heading anchor that does not exist
TitusKirch Sep 8, 2026
97dd3f3
chore: give credits a navbar entry instead of a dropdown item
TitusKirch Sep 8, 2026
48dc902
fix: put the palette in a cascade layer so an override cannot lose a …
TitusKirch Sep 8, 2026
f915dc0
fix: name the devtools panel without describing the tab
TitusKirch Sep 8, 2026
bbd4ad2
fix: stop creating the content database before Content does
TitusKirch Sep 8, 2026
9d7328c
fix: check the origin before a framed panel takes a theme from a message
TitusKirch Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .github/assets/wordmark-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions .github/assets/wordmark-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ permissions:
jobs:
check:
name: CI
uses: kirchDev/workflows/.github/workflows/_ci-check.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_ci-check.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ jobs:
packages: read
actions: read
contents: read
uses: kirchDev/workflows/.github/workflows/_codeql.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_codeql.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
2 changes: 1 addition & 1 deletion .github/workflows/fast-forward-queue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
pull-requests: read
checks: read
statuses: read
uses: kirchDev/workflows/.github/workflows/_fast-forward-queue.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_fast-forward-queue.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
with:
pr: ${{ inputs.pr }}
secrets:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/promotion-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ permissions:
jobs:
promotion-pr:
name: Promotion PR
uses: kirchDev/workflows/.github/workflows/_promotion-pr.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_promotion-pr.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
2 changes: 1 addition & 1 deletion .github/workflows/queue-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ jobs:
permissions:
contents: read
pull-requests: write
uses: kirchDev/workflows/.github/workflows/_queue-branch.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_queue-branch.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
secrets:
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
permissions:
contents: write
pull-requests: write
uses: kirchDev/workflows/.github/workflows/_release-please.yml@94cbcbd676b989ac6c6327055a240132f5cd76a5 # v0.8.0
uses: kirchDev/workflows/.github/workflows/_release-please.yml@1e57db95ff601f03ae87b78e329ef238f9d0cbf6 # v0.8.2
secrets:
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# region gitignore-sync
# stacks: core, agents, git, node, nuxt, vscode, intellij, macos, windows, linux
# stacks: core, agents, git, node, nuxt, dotenv, vscode, intellij, macos, windows, linux
# ─────────────────────────────────────────

# region core@v1
Expand Down Expand Up @@ -43,6 +43,14 @@ coverage
.data
# endregion

# region dotenv@v1
.env
.env.*
!.env.example
!.env.*.example
!.env.testing
# endregion

# region vscode@v1
.vscode/*
!.vscode/extensions.json
Expand Down
3 changes: 2 additions & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"CLAUDE.md",
"AGENTS.md",
"docs/**/*.md",
"www/content/**/*.md"
"www/content/**/*.md",
"public/devtools/*.html"
]
}
4 changes: 3 additions & 1 deletion .tituskirch-skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
}
},
"docs": {
"preset": "package"
"preset": "package",
"render": "duxt",
"locales": ["de", "es", "fr", "pt"]
},
"work": {
"tracker": "github",
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<div align="center">

# 📚 duxt
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/wordmark-dark.svg">
<img src=".github/assets/wordmark-light.svg" alt="duxt" width="200" />
</picture>

**Versioned, multi-repo documentation for Nuxt — one line to extend, no collection boilerplate**

Expand Down Expand Up @@ -118,6 +121,18 @@ pnpm check # lint + format + typecheck + tests + policy parity + build + a11

The repo root **is** the layer — `nuxt.config.ts`, `content.config.ts`, `app/`, `modules/` and `server/` live there, and `package.json` points at them. `www/` beside it is the site that consumes the layer, and the development target: it deliberately carries the awkward cases — two repositories, four refs, one version of each lifecycle. It is not a template; the exemplary starting point lives in [`kirchDev/duxt-starter`](https://github.com/kirchDev/duxt-starter).

## 🎨 Assets & branding

The mark is the package name with its first letter bracketed — `[d]uxt` — because that is how the package is written where it is used: `extends: ['@kirchdev/duxt']`, an array with one entry. It is set in [IBM Plex Mono](https://github.com/IBM/plex) SemiBold and converted to outlines, so no asset depends on the font being installed anywhere.

> [!NOTE]
> The wordmark is **AI-assisted placeholder artwork** — a stand-in to be replaced at some point, with no fixed timeline. No image generator was involved: it is typeset, not drawn.

Every asset, the colour values, why the icon's brackets are redrawn and the font licensing are in [Conventions → Branding](https://duxt.app/conventions/branding).

> [!IMPORTANT]
> The layer ships **no** branding. `duxt.logo` is unset by default, so `DuxtBrand` falls back to the consumer's own `duxt.title` beside a generic icon: a site extending duxt shows its own name in the header and footer and its own icon in the tab, never this one. These assets belong to this repository and to `www/`, not to the published package.

## 🤝 Contributing

PRs welcome. Conventional Commits are enforced via commitlint, and husky runs the linters on `git commit`. Branch off `dev`.
Expand Down
50 changes: 50 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,56 @@ if (baseUrl) {
const localeHead = useLocaleHead({ dir: false, lang: false, seo: true });
useHead(() => localeHead.value);
}

/**
* `og:locale`, which nuxt-seo-utils does not derive from i18n.
*
* Open Graph spells a locale with an underscore — `de_DE`, not `de-DE` — and
* the alternates are the OTHER locales this site serves, so a share card comes
* back in the reader's language where the network offers the choice. Only where
* the site has more than one; a single-locale site listing no alternates is
* correct, not incomplete.
*/
const ogLocale = (code: string) => code.replace('-', '_');

useSeoMeta({
ogSiteName: () => duxt.title,
ogLocale: () => ogLocale(locale.value),
ogLocaleAlternate: () =>
locales.value
.map((entry) => (typeof entry === 'string' ? entry : entry.code))
.filter((code) => code !== locale.value)
.map(ogLocale)
});

/**
* WHAT THE SITE IS, once, for every page under it.
*
* nuxt-schema-org keeps one `@graph` per document and merges into it, so this
* node and the per-page `Article` in `[...slug].vue` end up in the same script
* tag with the references between them resolved — which is the whole reason the
* hand-written JSON-LD that used to live there could go.
*
* The `Organization` is conditional and stays that way: duxt renders somebody
* else's documentation and must not name a publisher it invented. A consumer
* fills in `duxt.organization`; until then the site describes itself and no
* more.
*/
useSchemaOrg([
defineWebSite({
name: duxt.title,
inLanguage: locale.value
}),
...(duxt.organization?.name
? [
defineOrganization({
name: duxt.organization.name,
url: duxt.organization.url,
logo: duxt.organization.logo
})
]
: [])
]);
</script>

<template>
Expand Down
182 changes: 109 additions & 73 deletions app/assets/css/duxt.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,78 +15,101 @@
sets it with classSuffix: '' so the two agree. */
@custom-variant dark (&:is(.dark *));

/* The neutral base palette, in oklch. A consumer overrides a token by
redefining it in its own stylesheet — no fork of this file needed. */
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
/* shadcn's own value is oklch(0.556 0 0). Measured, that is 4.73:1 on the
page background — and 4.34:1 on --muted / --accent, which is where this
token actually lands: a hovered sidebar link, a badge, the sheet. So the
default fails AA on exactly the surfaces it is used on. 0.52 gives 5.51:1
and 5.05:1, and this token carries the page descriptions, the table of
contents and the breadcrumb — too much text to leave borderline. */
--muted-foreground: oklch(0.52 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
/* THE FOREGROUND FOLLOWS THE BACKGROUND'S LIGHTNESS, not the mode's. A pale
green with white on it reads as a badge and measures 3.12:1, which is a
badge nobody with low vision can read. Above roughly L 0.6 the pair takes
dark text, below it white — `--warning` already did, and success, info and
dark-mode destructive were simply missed. Enforced in
`tests/contrast.test.ts`; see the note on `--muted-foreground` above for
why the browser check cannot answer this. */
--success: oklch(0.63 0.17 149);
--success-foreground: oklch(0.145 0 0);
--warning: oklch(0.75 0.16 70);
--warning-foreground: oklch(0.145 0 0);
--info: oklch(0.62 0.16 244);
--info-foreground: oklch(0.145 0 0);
}
/*
* The neutral base palette, in oklch. A consumer overrides a token by
* redefining it in its own stylesheet — no fork of this file needed.
*
* IN A CASCADE LAYER, so that override cannot lose a race. Unlayered CSS beats
* layered CSS whatever the source order — and source order is not something
* this file controls. Vite resolves the stylesheet's absolute path along two
* routes at once, `/_nuxt/@fs/<abs>` and `/_nuxt/<abs>`, and deduplicates
* neither, so a second copy of this file lands AFTER the consumer's own `css`
* entry. At equal specificity the values below then won on order alone, and
* which one painted first came down to which 313 kB response arrived first: a
* consumer's buttons and badges flashed neutral before turning branded, and
* differently on every reload.
*
* The layer is ANONYMOUS on purpose. A named one can be reordered by a consumer
* writing `@layer` itself, which would hand the race straight back; an
* anonymous layer cannot be named again, so nothing downstream can move its own
* rules below these.
*
* Only the tokens go in here. Everything further down this file styles the
* layer's own components, where a consumer overriding by name is meant to need
* the specificity it already takes.
*/
@layer {
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
/* shadcn's own value is oklch(0.556 0 0). Measured, that is 4.73:1 on the
page background — and 4.34:1 on --muted / --accent, which is where this
token actually lands: a hovered sidebar link, a badge, the sheet. So the
default fails AA on exactly the surfaces it is used on. 0.52 gives 5.51:1
and 5.05:1, and this token carries the page descriptions, the table of
contents and the breadcrumb — too much text to leave borderline. */
--muted-foreground: oklch(0.52 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
/* THE FOREGROUND FOLLOWS THE BACKGROUND'S LIGHTNESS, not the mode's. A pale
green with white on it reads as a badge and measures 3.12:1, which is a
badge nobody with low vision can read. Above roughly L 0.6 the pair takes
dark text, below it white — `--warning` already did, and success, info and
dark-mode destructive were simply missed. Enforced in
`tests/contrast.test.ts`; see the note on `--muted-foreground` above for
why the browser check cannot answer this. */
--success: oklch(0.63 0.17 149);
--success-foreground: oklch(0.145 0 0);
--warning: oklch(0.75 0.16 70);
--warning-foreground: oklch(0.145 0 0);
--info: oklch(0.62 0.16 244);
--info-foreground: oklch(0.145 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
/* Same measurement in the dark: 7.63:1 on the background but 5.83:1 on
--muted. Lifted for the same reason, by less, because it starts higher. */
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.145 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--success: oklch(0.7 0.16 149);
--success-foreground: oklch(0.145 0 0);
--warning: oklch(0.8 0.15 70);
--warning-foreground: oklch(0.145 0 0);
--info: oklch(0.7 0.14 244);
--info-foreground: oklch(0.145 0 0);
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
/* Same measurement in the dark: 7.63:1 on the background but 5.83:1 on
--muted. Lifted for the same reason, by less, because it starts higher. */
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.145 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--success: oklch(0.7 0.16 149);
--success-foreground: oklch(0.145 0 0);
--warning: oklch(0.8 0.15 70);
--warning-foreground: oklch(0.145 0 0);
--info: oklch(0.7 0.14 244);
--info-foreground: oklch(0.145 0 0);
}
}

@theme inline {
Expand Down Expand Up @@ -192,12 +215,25 @@
surfaces, and in dark mode the inner one reads as a highlighted row. Not
scoped to .typeset, because the package-manager block renders the same
markup outside a rendered page. */
/* The heading link wraps the words, so it has to shed what `typeset` gives
every other link — the underline would draw a rule under every H2 on the
site. `relative` is what the marker below positions against: anchored to the
text rather than to the heading box, it lands the same distance out whatever
the heading's own margins are. */
.typeset :where(.duxt-heading-link) {
@apply relative no-underline;
text-decoration-line: none;
}

.typeset :where(.duxt-anchor) {
@apply absolute -left-5 font-normal text-muted-foreground no-underline opacity-0 transition-opacity;
@apply absolute -left-5 font-normal text-muted-foreground opacity-0 transition-opacity;
}

/* Revealed by hovering the heading, not the marker: the marker is 1.25rem
outside the text and nobody hunts for it. Focus does the same for the
keyboard, where the link is reached without a pointer at all. */
.typeset :where(h2:hover, h3:hover, h4:hover) .duxt-anchor,
.typeset :where(.duxt-anchor:focus-visible) {
.typeset :where(.duxt-heading-link:focus-visible) .duxt-anchor {
@apply opacity-100;
}

Expand Down
Loading
Loading