@kirchdev/duxt 0.4.0 → 0.4.1-dev-main.46-fcc33a3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/app/assets/css/duxt.css +18 -0
- package/app/components/DuxtHeader.vue +1 -3
- package/app/components/content/ChangelogReleases.vue +56 -39
- package/app/components/content/ProseImg.vue +9 -2
- package/app/components/ui/dialog/DialogContent.vue +9 -2
- package/app/composables/useActiveHeading.ts +14 -1
- package/app/composables/useDuxtThemeToggle.ts +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
<img src=".github/assets/wordmark-light.svg" alt="duxt" width="200" />
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
**Versioned,
|
|
8
|
+
**Versioned docs, from the repositories you already have**
|
|
9
|
+
|
|
10
|
+
duxt is a Nuxt layer: extend it and your docs/ folder becomes a site, with theme, search, API reference and llms.txt included. Point it at other repositories, or at tags of the same one, and each becomes a version.
|
|
9
11
|
|
|
10
12
|
[](https://www.npmjs.com/package/@kirchdev/duxt)
|
|
11
13
|
[](https://www.npmjs.com/package/@kirchdev/duxt)
|
package/app/assets/css/duxt.css
CHANGED
|
@@ -596,3 +596,21 @@ pre.shiki,
|
|
|
596
596
|
main:has(.duxt-flush-footer) + footer {
|
|
597
597
|
margin-top: 0;
|
|
598
598
|
}
|
|
599
|
+
|
|
600
|
+
/* The theme toggle reveals the new theme as a growing circle — see
|
|
601
|
+
`useDuxtThemeToggle`. The browser's default cross-fade is switched off so the
|
|
602
|
+
clip is the only animation, and the new snapshot is lifted above the old one
|
|
603
|
+
it opens over. */
|
|
604
|
+
::view-transition-old(root),
|
|
605
|
+
::view-transition-new(root) {
|
|
606
|
+
animation: none;
|
|
607
|
+
mix-blend-mode: normal;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
::view-transition-old(root) {
|
|
611
|
+
z-index: 1;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
::view-transition-new(root) {
|
|
615
|
+
z-index: 9999;
|
|
616
|
+
}
|
|
@@ -22,9 +22,7 @@ if (import.meta.prerender) {
|
|
|
22
22
|
// The mobile sheet shows the same branch the sidebar does.
|
|
23
23
|
const { items } = useDuxtSection(navigation);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
|
|
27
|
-
}
|
|
25
|
+
const { toggle: toggleTheme } = useDuxtThemeToggle();
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* An entry with neither `to` nor children means "the documentation".
|
|
@@ -242,7 +242,7 @@ const rows = computed(() =>
|
|
|
242
242
|
dropped re-laid the row it shared, so the button a reader was aiming at
|
|
243
243
|
slid out from under the pointer between two clicks. On a row of its own
|
|
244
244
|
the header is fixed and only the row that is actually changing moves. -->
|
|
245
|
-
<div class="mt-12 mb-8
|
|
245
|
+
<div class="mt-12 mb-8">
|
|
246
246
|
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-3">
|
|
247
247
|
<h2 class="text-lg font-semibold tracking-tight">
|
|
248
248
|
{{ $t('duxt.changelog.history') }}
|
|
@@ -334,45 +334,62 @@ const rows = computed(() =>
|
|
|
334
334
|
The row is ALWAYS RENDERED and hidden while empty (`empty:hidden`),
|
|
335
335
|
rather than held behind a `v-if` that would take the leaving chips
|
|
336
336
|
with it before they could leave. During a leave it is no longer
|
|
337
|
-
empty, so it stays laid out until the last chip is gone.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
337
|
+
empty, so it stays laid out until the last chip is gone.
|
|
338
|
+
|
|
339
|
+
ITS HEIGHT ANIMATES TOO. Appearing and vanishing in one step, the row
|
|
340
|
+
pushed the figures and the whole list down by its own height on the
|
|
341
|
+
first choice and pulled them back up on the last — the one jump left
|
|
342
|
+
in a gesture that otherwise moves. `grid-template-rows` from `0fr` to
|
|
343
|
+
`1fr` is the one height transition CSS can run without knowing the
|
|
344
|
+
height, and the spacing lives INSIDE the clipped track so it
|
|
345
|
+
collapses with it. The padding sits on the INNER box because a
|
|
346
|
+
grid item cannot shrink below its own padding, and it keeps a chip's
|
|
347
|
+
focus ring clear of the clip. -->
|
|
348
|
+
<div
|
|
349
|
+
class="grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
|
|
350
|
+
:class="selected.length ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
|
348
351
|
>
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
class="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
352
|
+
<div class="-mx-1 min-h-0 overflow-hidden">
|
|
353
|
+
<TransitionGroup
|
|
354
|
+
tag="div"
|
|
355
|
+
role="group"
|
|
356
|
+
:aria-label="$t('duxt.changelog.selected')"
|
|
357
|
+
class="flex flex-wrap items-center gap-2 px-1 pt-3 pb-1"
|
|
358
|
+
enter-active-class="transition duration-200 ease-out motion-reduce:transition-none"
|
|
359
|
+
enter-from-class="scale-95 opacity-0"
|
|
360
|
+
leave-active-class="transition duration-150 ease-in motion-reduce:transition-none"
|
|
361
|
+
leave-to-class="scale-95 opacity-0"
|
|
362
|
+
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
|
363
|
+
>
|
|
364
|
+
<button
|
|
365
|
+
v-for="name in selected"
|
|
366
|
+
:key="name"
|
|
367
|
+
type="button"
|
|
368
|
+
class="inline-flex cursor-pointer items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-inset focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
|
369
|
+
:class="changelogTone(name).chip"
|
|
370
|
+
@click="toggle(name)"
|
|
371
|
+
>
|
|
372
|
+
<span
|
|
373
|
+
aria-hidden="true"
|
|
374
|
+
class="size-1.5 rounded-full"
|
|
375
|
+
:class="changelogTone(name).dot"
|
|
376
|
+
/>
|
|
377
|
+
{{ changelogLabel(name) }}
|
|
378
|
+
<Icon name="lucide:x" class="size-3 opacity-60" />
|
|
379
|
+
</button>
|
|
380
|
+
|
|
381
|
+
<button
|
|
382
|
+
v-if="selected.length"
|
|
383
|
+
key="reset"
|
|
384
|
+
type="button"
|
|
385
|
+
class="cursor-pointer text-xs text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
|
386
|
+
@click="selected = []"
|
|
387
|
+
>
|
|
388
|
+
{{ $t('duxt.changelog.reset') }}
|
|
389
|
+
</button>
|
|
390
|
+
</TransitionGroup>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
376
393
|
</div>
|
|
377
394
|
|
|
378
395
|
<!-- WHAT THE LIST CANNOT SAY: how much there is. Two figures set the way
|
|
@@ -525,11 +525,18 @@ if (import.meta.server && import.meta.prerender) {
|
|
|
525
525
|
|
|
526
526
|
<UiDialog v-if="zoomable" v-model:open="open">
|
|
527
527
|
<!-- The whole viewport, transparent over the overlay: the blur is what
|
|
528
|
-
separates the picture from the page, not a border.
|
|
528
|
+
separates the picture from the page, not a border.
|
|
529
|
+
|
|
530
|
+
THE DARKENING AND THE BLUR BELONG TO THE OVERLAY, not to this box.
|
|
531
|
+
The content scales in from 95%, so a surface painted here reached
|
|
532
|
+
the viewport's edges only once that animation ended — and for 200ms
|
|
533
|
+
the page's text showed unblurred along the bottom. The overlay only
|
|
534
|
+
fades, so it covers the whole screen from the first frame. -->
|
|
529
535
|
<UiDialogContent
|
|
530
536
|
:show-close-button="false"
|
|
531
537
|
:aria-describedby="undefined"
|
|
532
|
-
class="
|
|
538
|
+
overlay-class="bg-neutral-950/90 backdrop-blur-md"
|
|
539
|
+
class="inset-0 grid h-dvh w-screen max-w-none translate-none grid-rows-[auto_minmax(0,1fr)_auto] gap-0 rounded-none border-0 bg-transparent p-0 shadow-none sm:max-w-none"
|
|
533
540
|
@click.self="open = false"
|
|
534
541
|
@keydown="onKeydown"
|
|
535
542
|
>
|
|
@@ -19,6 +19,8 @@ const props = withDefaults(
|
|
|
19
19
|
defineProps<
|
|
20
20
|
DialogContentProps & {
|
|
21
21
|
class?: HTMLAttributes['class'];
|
|
22
|
+
/** Classes for the backdrop, which does not scale in with the content. */
|
|
23
|
+
overlayClass?: HTMLAttributes['class'];
|
|
22
24
|
showCloseButton?: boolean;
|
|
23
25
|
}
|
|
24
26
|
>(),
|
|
@@ -28,14 +30,19 @@ const props = withDefaults(
|
|
|
28
30
|
);
|
|
29
31
|
const emits = defineEmits<DialogContentEmits>();
|
|
30
32
|
|
|
31
|
-
const delegatedProps = reactiveOmit(
|
|
33
|
+
const delegatedProps = reactiveOmit(
|
|
34
|
+
props,
|
|
35
|
+
'class',
|
|
36
|
+
'overlayClass',
|
|
37
|
+
'showCloseButton'
|
|
38
|
+
);
|
|
32
39
|
|
|
33
40
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
34
41
|
</script>
|
|
35
42
|
|
|
36
43
|
<template>
|
|
37
44
|
<DialogPortal>
|
|
38
|
-
<DialogOverlay />
|
|
45
|
+
<DialogOverlay :class="overlayClass" />
|
|
39
46
|
<DialogContent
|
|
40
47
|
data-slot="dialog-content"
|
|
41
48
|
v-bind="{ ...$attrs, ...forwarded }"
|
|
@@ -49,9 +49,22 @@ export function useActiveHeading(ids: Ref<string[]>, scrollOffset = 96) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// THE LINE IS NEVER ABOVE WHERE A JUMP PARKS THE HEADING. A click in the
|
|
53
|
+
// outline scrolls the heading to its own `scroll-margin-top` — 112px, to
|
|
54
|
+
// clear the header AND the section row — and a fixed 96px line sat above
|
|
55
|
+
// that, so the heading just jumped to was not yet "passed" and the entry
|
|
56
|
+
// before it stayed marked. Reading the margin off the heading follows
|
|
57
|
+
// whatever the sticky stack is on this page and at this width; the
|
|
58
|
+
// configured offset remains the floor. The pixel absorbs sub-pixel scroll
|
|
59
|
+
// positions.
|
|
52
60
|
let current = headings[0]!;
|
|
53
61
|
for (const heading of headings) {
|
|
54
|
-
|
|
62
|
+
const margin = Number.parseFloat(
|
|
63
|
+
getComputedStyle(heading).scrollMarginTop
|
|
64
|
+
);
|
|
65
|
+
const line =
|
|
66
|
+
Math.max(scrollOffset, Number.isNaN(margin) ? 0 : margin) + 1;
|
|
67
|
+
if (heading.getBoundingClientRect().top > line) break;
|
|
55
68
|
current = heading;
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switch between light and dark, revealing the new theme as a circle that grows
|
|
3
|
+
* out of the button that was pressed.
|
|
4
|
+
*
|
|
5
|
+
* THE VIEW TRANSITIONS API, not a CSS transition on every colour. The browser
|
|
6
|
+
* snapshots the page as it is, the switch happens underneath, and the new page
|
|
7
|
+
* is clipped open on top of the old snapshot — one animation for the whole
|
|
8
|
+
* document, whatever each component paints. Where the API does not exist, or
|
|
9
|
+
* the reader asked for less motion, the theme simply switches.
|
|
10
|
+
*
|
|
11
|
+
* `@nuxtjs/color-mode` applies the class from a watcher, so the callback waits
|
|
12
|
+
* a tick: a snapshot taken before the class moved would reveal the old theme.
|
|
13
|
+
*
|
|
14
|
+
* Both the origin and the radius are PERCENTAGES. The clip resolves against the
|
|
15
|
+
* transition's snapshot box, which on a phone also spans the area behind the
|
|
16
|
+
* hideable address bar and is therefore taller than `innerHeight` — pixels
|
|
17
|
+
* computed from the viewport opened the circle away from the button and
|
|
18
|
+
* stopped it short of the far corner. 150% covers the box from any point in
|
|
19
|
+
* it: a corner needs 100%·√2 ≈ 141%.
|
|
20
|
+
*
|
|
21
|
+
* A transition the browser abandons — a second click, the tab going to the
|
|
22
|
+
* background, a snapshot that takes too long on a long page — rejects its
|
|
23
|
+
* promises. That is a normal outcome, and the theme has switched either way.
|
|
24
|
+
*/
|
|
25
|
+
export function useDuxtThemeToggle() {
|
|
26
|
+
const colorMode = useColorMode();
|
|
27
|
+
|
|
28
|
+
function apply() {
|
|
29
|
+
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function toggle(event?: MouseEvent) {
|
|
33
|
+
if (
|
|
34
|
+
!document.startViewTransition ||
|
|
35
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
36
|
+
) {
|
|
37
|
+
apply();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A keyboard press reports no pointer position; open from the centre then.
|
|
42
|
+
const x = event?.clientX ? (event.clientX / window.innerWidth) * 100 : 50;
|
|
43
|
+
const y = event?.clientY ? (event.clientY / window.innerHeight) * 100 : 50;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const transition = document.startViewTransition(async () => {
|
|
47
|
+
apply();
|
|
48
|
+
await nextTick();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await transition.ready;
|
|
52
|
+
|
|
53
|
+
document.documentElement.animate(
|
|
54
|
+
{
|
|
55
|
+
clipPath: [`circle(0% at ${x}% ${y}%)`, `circle(150% at ${x}% ${y}%)`]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
duration: 450,
|
|
59
|
+
easing: 'ease-out',
|
|
60
|
+
pseudoElement: '::view-transition-new(root)'
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
// Abandoned transition — see above.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { toggle };
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@kirchdev/duxt",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1-dev-main.46-fcc33a3",
|
|
5
5
|
"description": "duxt is a Nuxt layer: extend it and your docs/ folder becomes a site, with theme, search, API reference and llms.txt included. Point it at other repositories, or at tags of the same one, and each becomes a version.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|