@moldea.ai/website-ui 1.2.2 → 1.6.1
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 +164 -3
- package/dist/evaluation-replay/types.d.ts +1 -1
- package/dist/evaluation-replay/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts +7 -0
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown.js +33 -6
- package/dist/markdown.js.map +1 -1
- package/package.json +22 -1
- package/src/components/accordion/accordion.component.astro +103 -0
- package/src/components/action-link/action-link.component.astro +6 -1
- package/src/components/breadcrumbs/breadcrumbs.component.astro +1 -1
- package/src/components/code-block/code-block.component.astro +22 -0
- package/src/components/connection-label/connection-label.component.astro +26 -0
- package/src/components/dialog/dialog.component.astro +266 -0
- package/src/components/documentation-shell/documentation-outline.component.astro +1 -4
- package/src/components/documentation-shell/documentation-shell.component.astro +2 -12
- package/src/components/evaluation-replay/evaluation-replay-markdown.component.astro +1 -1
- package/src/components/file-preview/file-preview.component.astro +55 -0
- package/src/components/hero-backdrop/hero-backdrop.component.astro +9 -0
- package/src/components/inline-brand-text/inline-brand-text.component.astro +1 -1
- package/src/components/local-search/local-search.component.astro +37 -17
- package/src/components/result-summary/result-summary.component.astro +64 -0
- package/src/components/site-header/site-header.component.astro +39 -32
- package/src/components/status-badge/status-badge.component.astro +8 -2
- package/src/styles.css +52 -6
- package/src/tokens.css +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moldea.ai/website-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Shared Astro foundations for moldea public websites.",
|
|
5
5
|
"homepage": "https://github.com/moldea-ai/packages/tree/main/projects/website-ui#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
"types": "./dist/theme/index.d.ts",
|
|
48
48
|
"import": "./dist/theme.js"
|
|
49
49
|
},
|
|
50
|
+
"./accordion": {
|
|
51
|
+
"import": "./src/components/accordion/accordion.component.astro"
|
|
52
|
+
},
|
|
50
53
|
"./action-button": {
|
|
51
54
|
"import": "./src/components/action-button/action-button.component.astro"
|
|
52
55
|
},
|
|
@@ -59,9 +62,18 @@
|
|
|
59
62
|
"./breadcrumbs": {
|
|
60
63
|
"import": "./src/components/breadcrumbs/breadcrumbs.component.astro"
|
|
61
64
|
},
|
|
65
|
+
"./code-block": {
|
|
66
|
+
"import": "./src/components/code-block/code-block.component.astro"
|
|
67
|
+
},
|
|
68
|
+
"./connection-label": {
|
|
69
|
+
"import": "./src/components/connection-label/connection-label.component.astro"
|
|
70
|
+
},
|
|
62
71
|
"./documentation-shell": {
|
|
63
72
|
"import": "./src/components/documentation-shell/documentation-shell.component.astro"
|
|
64
73
|
},
|
|
74
|
+
"./dialog": {
|
|
75
|
+
"import": "./src/components/dialog/dialog.component.astro"
|
|
76
|
+
},
|
|
65
77
|
"./evaluation-replay": {
|
|
66
78
|
"import": "./src/components/evaluation-replay/evaluation-replay.component.astro"
|
|
67
79
|
},
|
|
@@ -69,6 +81,12 @@
|
|
|
69
81
|
"types": "./dist/evaluation-replay/index.d.ts",
|
|
70
82
|
"import": "./dist/evaluation-replay.js"
|
|
71
83
|
},
|
|
84
|
+
"./file-preview": {
|
|
85
|
+
"import": "./src/components/file-preview/file-preview.component.astro"
|
|
86
|
+
},
|
|
87
|
+
"./hero-backdrop": {
|
|
88
|
+
"import": "./src/components/hero-backdrop/hero-backdrop.component.astro"
|
|
89
|
+
},
|
|
72
90
|
"./inline-brand-text": {
|
|
73
91
|
"import": "./src/components/inline-brand-text/inline-brand-text.component.astro"
|
|
74
92
|
},
|
|
@@ -85,6 +103,9 @@
|
|
|
85
103
|
"./site-footer": {
|
|
86
104
|
"import": "./src/components/site-footer/site-footer.component.astro"
|
|
87
105
|
},
|
|
106
|
+
"./result-summary": {
|
|
107
|
+
"import": "./src/components/result-summary/result-summary.component.astro"
|
|
108
|
+
},
|
|
88
109
|
"./site-header": {
|
|
89
110
|
"import": "./src/components/site-header/site-header.component.astro"
|
|
90
111
|
},
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ChevronDown } from '@lucide/astro';
|
|
3
|
+
import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
|
|
4
|
+
|
|
5
|
+
// sibling items share a document-unique group name; at most one may be initially open
|
|
6
|
+
export interface Props {
|
|
7
|
+
id: string;
|
|
8
|
+
group: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
isOpen?: boolean;
|
|
12
|
+
}
|
|
13
|
+
const { id, group, title, description, isOpen = false } = Astro.props;
|
|
14
|
+
if (!id.trim() || !group.trim() || !title.trim())
|
|
15
|
+
throw new Error('Accordion requires an ID, group, and title.');
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<details
|
|
19
|
+
id={id}
|
|
20
|
+
name={group}
|
|
21
|
+
open={isOpen}
|
|
22
|
+
class="group/accordion min-w-0 scroll-mt-24 border-y sm:rounded-xl sm:border sm:bg-card"
|
|
23
|
+
data-accordion-item
|
|
24
|
+
>
|
|
25
|
+
<summary
|
|
26
|
+
class="flex min-h-12 list-none items-center gap-3 px-1 py-4 transition-colors outline-none hover:bg-secondary/60 focus-visible:ring-inset active:bg-secondary motion-reduce:transition-none sm:rounded-xl sm:px-5 [&::-webkit-details-marker]:hidden"
|
|
27
|
+
aria-labelledby={`${id}-title`}
|
|
28
|
+
aria-describedby={description ? `${id}-description` : undefined}
|
|
29
|
+
>
|
|
30
|
+
<span class="min-w-0 flex-1">
|
|
31
|
+
<span
|
|
32
|
+
id={`${id}-title`}
|
|
33
|
+
class="block text-sm leading-6 font-semibold sm:text-base"
|
|
34
|
+
><InlineBrandText text={title} /></span
|
|
35
|
+
>
|
|
36
|
+
{
|
|
37
|
+
description && (
|
|
38
|
+
<span
|
|
39
|
+
id={`${id}-description`}
|
|
40
|
+
class="mt-1 block text-xs leading-5 text-muted-foreground"
|
|
41
|
+
>
|
|
42
|
+
<InlineBrandText text={description} />
|
|
43
|
+
</span>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
</span>
|
|
47
|
+
<slot name="status" />
|
|
48
|
+
<ChevronDown
|
|
49
|
+
class="size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-open/accordion:rotate-180 motion-reduce:transition-none"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
/>
|
|
52
|
+
</summary>
|
|
53
|
+
<div
|
|
54
|
+
class="accordion-panel min-w-0 px-1 pt-1 pb-5 sm:px-5"
|
|
55
|
+
data-accordion-panel
|
|
56
|
+
>
|
|
57
|
+
<slot />
|
|
58
|
+
</div>
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
/* Variable-height content fades without a height transition or page-wide movement. */
|
|
63
|
+
details[open] > .accordion-panel {
|
|
64
|
+
animation: accordion-reveal 160ms ease-out;
|
|
65
|
+
}
|
|
66
|
+
@keyframes accordion-reveal {
|
|
67
|
+
from {
|
|
68
|
+
opacity: 0;
|
|
69
|
+
}
|
|
70
|
+
to {
|
|
71
|
+
opacity: 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
@media (prefers-reduced-motion: reduce) {
|
|
75
|
+
details[open] > .accordion-panel {
|
|
76
|
+
animation: none;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
/** Reveals hash-linked content after direct, history, and Astro client navigation. */
|
|
83
|
+
const revealAccordionAnchor = (): void => {
|
|
84
|
+
let anchorId: string;
|
|
85
|
+
try {
|
|
86
|
+
anchorId = decodeURIComponent(window.location.hash.slice(1));
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!anchorId) return;
|
|
91
|
+
const target = document.getElementById(anchorId);
|
|
92
|
+
const item = target?.closest<HTMLDetailsElement>('details[data-accordion-item]');
|
|
93
|
+
if (!item) return;
|
|
94
|
+
item.open = true;
|
|
95
|
+
// opening an earlier item can change the anchor position after native hash scrolling
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
target?.scrollIntoView({ block: 'start', behavior: 'instant' });
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
revealAccordionAnchor();
|
|
101
|
+
window.addEventListener('hashchange', revealAccordionAnchor);
|
|
102
|
+
document.addEventListener('astro:page-load', revealAccordionAnchor);
|
|
103
|
+
</script>
|
|
@@ -11,7 +11,12 @@ const { class: className, size = 'md', variant = 'primary', ...attributes } = As
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<a
|
|
14
|
-
class:list={[
|
|
14
|
+
class:list={[
|
|
15
|
+
'action-control',
|
|
16
|
+
variant === 'link' ? 'text-action-link' : `action-${variant}`,
|
|
17
|
+
`action-size-${size}`,
|
|
18
|
+
className,
|
|
19
|
+
]}
|
|
15
20
|
{...attributes}
|
|
16
21
|
>
|
|
17
22
|
<slot />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { renderCodeBlock } from '@moldea.ai/website-ui/markdown';
|
|
3
|
+
|
|
4
|
+
// highlighted source, with a panel by default or a flat surface for composition inside another panel
|
|
5
|
+
export interface Props {
|
|
6
|
+
source: string;
|
|
7
|
+
language?: string;
|
|
8
|
+
variant?: 'panel' | 'plain';
|
|
9
|
+
}
|
|
10
|
+
const { source, language, variant = 'panel' } = Astro.props;
|
|
11
|
+
const html = await renderCodeBlock(source, language);
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class:list={[
|
|
16
|
+
'prose-moldea min-w-0 text-sm [&_pre]:m-0 [&_pre]:text-xs [&_pre]:leading-6',
|
|
17
|
+
variant === 'panel'
|
|
18
|
+
? '[&_pre]:p-4'
|
|
19
|
+
: '[&_pre]:rounded-none [&_pre]:border-0 [&_pre]:bg-transparent [&_pre]:p-0 [&_pre]:pb-1',
|
|
20
|
+
]}
|
|
21
|
+
set:html={html}
|
|
22
|
+
/>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
// visual relationship between examples; consumers supply its meaning and optional decorative icon
|
|
3
|
+
export interface Props {
|
|
4
|
+
tone?: 'danger' | 'neutral';
|
|
5
|
+
}
|
|
6
|
+
const { tone = 'neutral' } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<p
|
|
10
|
+
class:list={[
|
|
11
|
+
'flex items-center justify-center gap-2 py-3 text-xs',
|
|
12
|
+
tone === 'danger' ? 'text-destructive' : 'text-muted-foreground',
|
|
13
|
+
]}
|
|
14
|
+
>
|
|
15
|
+
{
|
|
16
|
+
Astro.slots.has('icon') && (
|
|
17
|
+
<span
|
|
18
|
+
class="shrink-0"
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
>
|
|
21
|
+
<slot name="icon" />
|
|
22
|
+
</span>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
<span class="min-w-0"><slot /></span>
|
|
26
|
+
</p>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ArrowLeft, X } from '@lucide/astro';
|
|
3
|
+
|
|
4
|
+
import ActionButton, {
|
|
5
|
+
type Props as IActionButtonProps,
|
|
6
|
+
} from '../action-button/action-button.component.astro';
|
|
7
|
+
|
|
8
|
+
// consumers own the unique document ID, trigger copy, and slotted dialog content
|
|
9
|
+
export interface Props {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
triggerLabel: string;
|
|
14
|
+
triggerAriaLabel?: string;
|
|
15
|
+
triggerVariant?: IActionButtonProps['variant'];
|
|
16
|
+
triggerSize?: 'compact' | Exclude<IActionButtonProps['size'], 'icon'>;
|
|
17
|
+
size?: 'medium' | 'large';
|
|
18
|
+
closeLabel?: string;
|
|
19
|
+
// matches the platform's opt-in dismissal for read-only content
|
|
20
|
+
isOverlayCloseEnabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
id,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
triggerLabel,
|
|
28
|
+
triggerAriaLabel,
|
|
29
|
+
triggerVariant = 'outline',
|
|
30
|
+
triggerSize = 'compact',
|
|
31
|
+
size = 'medium',
|
|
32
|
+
closeLabel = 'Close dialog',
|
|
33
|
+
isOverlayCloseEnabled = false,
|
|
34
|
+
} = Astro.props;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<div data-dialog-root>
|
|
38
|
+
<ActionButton
|
|
39
|
+
variant={triggerVariant}
|
|
40
|
+
size={triggerSize === 'compact' ? 'sm' : triggerSize}
|
|
41
|
+
class={triggerSize === 'compact' ? 'py-1 text-xs [hidden]:hidden' : '[hidden]:hidden'}
|
|
42
|
+
aria-label={triggerAriaLabel}
|
|
43
|
+
aria-controls={id}
|
|
44
|
+
aria-haspopup="dialog"
|
|
45
|
+
data-dialog-trigger
|
|
46
|
+
hidden
|
|
47
|
+
>
|
|
48
|
+
{triggerLabel}
|
|
49
|
+
</ActionButton>
|
|
50
|
+
<dialog
|
|
51
|
+
id={id}
|
|
52
|
+
class:list={[
|
|
53
|
+
'fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none grid-rows-[auto_minmax(0,1fr)] overflow-hidden border-0 bg-background p-0 text-foreground outline-none [--dialog-exit-duration:300ms] [--dialog-motion-transform:translateY(0.25rem)] backdrop:bg-black/45 open:grid sm:m-auto sm:h-fit sm:max-h-[calc(100dvh-2rem)] sm:w-[calc(100vw-2rem)] sm:rounded-xl sm:border sm:shadow-raised sm:[--dialog-exit-duration:200ms] sm:[--dialog-motion-transform:translateY(0.5rem)_scale(0.95)] sm:backdrop:backdrop-blur-xs dark:backdrop:bg-black/70',
|
|
54
|
+
size === 'large' ? 'sm:max-w-5xl' : 'sm:max-w-2xl',
|
|
55
|
+
]}
|
|
56
|
+
aria-labelledby={`${id}-title`}
|
|
57
|
+
aria-describedby={description ? `${id}-description` : undefined}
|
|
58
|
+
data-website-ui-dialog
|
|
59
|
+
data-overlay-close-enabled={isOverlayCloseEnabled ? '' : undefined}
|
|
60
|
+
>
|
|
61
|
+
<header
|
|
62
|
+
class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 border-b pt-[max(0.5rem,env(safe-area-inset-top))] pr-[max(0.5rem,env(safe-area-inset-right))] pb-4 pl-[max(0.5rem,env(safe-area-inset-left))] sm:grid-cols-[minmax(0,1fr)_auto] sm:px-6 sm:py-4"
|
|
63
|
+
>
|
|
64
|
+
<div class="col-start-2 row-start-1 flex min-h-9 min-w-0 items-center sm:col-start-1">
|
|
65
|
+
<slot name="heading">
|
|
66
|
+
<h2
|
|
67
|
+
id={`${id}-title`}
|
|
68
|
+
class="min-w-0 text-lg font-semibold"
|
|
69
|
+
>
|
|
70
|
+
{title}
|
|
71
|
+
</h2>
|
|
72
|
+
</slot>
|
|
73
|
+
</div>
|
|
74
|
+
<form
|
|
75
|
+
method="dialog"
|
|
76
|
+
class="col-start-1 row-start-1 sm:col-start-2"
|
|
77
|
+
data-dialog-close-form
|
|
78
|
+
>
|
|
79
|
+
<ActionButton
|
|
80
|
+
type="submit"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="icon"
|
|
83
|
+
class="size-9 sm:size-7 sm:rounded-md"
|
|
84
|
+
aria-label={closeLabel}
|
|
85
|
+
autofocus
|
|
86
|
+
>
|
|
87
|
+
<ArrowLeft
|
|
88
|
+
class="size-4 sm:hidden"
|
|
89
|
+
aria-hidden="true"
|
|
90
|
+
/>
|
|
91
|
+
<X
|
|
92
|
+
class="hidden size-4 sm:block"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
/>
|
|
95
|
+
</ActionButton>
|
|
96
|
+
</form>
|
|
97
|
+
{
|
|
98
|
+
description && (
|
|
99
|
+
<p
|
|
100
|
+
id={`${id}-description`}
|
|
101
|
+
class="col-span-2 mt-1 px-2 text-sm leading-6 text-muted-foreground sm:px-0"
|
|
102
|
+
>
|
|
103
|
+
{description}
|
|
104
|
+
</p>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
</header>
|
|
108
|
+
<div
|
|
109
|
+
class="min-h-0 min-w-0 overflow-y-auto overscroll-contain p-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:p-6"
|
|
110
|
+
>
|
|
111
|
+
<slot />
|
|
112
|
+
</div>
|
|
113
|
+
</dialog>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
/** Enhances optional detail triggers while native dialogs own modality and keyboard focus. */
|
|
118
|
+
const initializeDialogs = (): void => {
|
|
119
|
+
for (const root of document.querySelectorAll<HTMLElement>('[data-dialog-root]')) {
|
|
120
|
+
if (root.dataset['enhanced'] === 'true') continue;
|
|
121
|
+
const trigger = root.querySelector<HTMLButtonElement>(':scope > [data-dialog-trigger]');
|
|
122
|
+
const dialog = root.querySelector<HTMLDialogElement>(':scope > dialog');
|
|
123
|
+
if (trigger === null || dialog === null || typeof dialog.showModal !== 'function') continue;
|
|
124
|
+
|
|
125
|
+
let closeRequest: symbol | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Keeps native modality until exit completes; navigation and reopening invalidate old exits.
|
|
128
|
+
* @returns A promise that resolves after dismissal or invalidation by a newer session.
|
|
129
|
+
*/
|
|
130
|
+
const requestClose = async (): Promise<void> => {
|
|
131
|
+
if (!dialog.open || closeRequest !== undefined) return;
|
|
132
|
+
const request = Symbol();
|
|
133
|
+
closeRequest = request;
|
|
134
|
+
dialog.dataset['closing'] = '';
|
|
135
|
+
// cancellation (including reduced-motion changes) must still finish dismissal
|
|
136
|
+
await Promise.allSettled(dialog.getAnimations().map((animation) => animation.finished));
|
|
137
|
+
if (closeRequest === request && dialog.isConnected && dialog.open) dialog.close();
|
|
138
|
+
};
|
|
139
|
+
trigger.addEventListener('click', () => {
|
|
140
|
+
if (dialog.open) return;
|
|
141
|
+
closeRequest = undefined;
|
|
142
|
+
delete dialog.dataset['closing'];
|
|
143
|
+
dialog.showModal();
|
|
144
|
+
});
|
|
145
|
+
dialog.addEventListener('cancel', (event) => {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
void requestClose();
|
|
148
|
+
});
|
|
149
|
+
dialog.querySelector('[data-dialog-close-form]')?.addEventListener('submit', (event) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
void requestClose();
|
|
152
|
+
});
|
|
153
|
+
dialog.addEventListener('close', () => {
|
|
154
|
+
if (dialog.open) return;
|
|
155
|
+
closeRequest = undefined;
|
|
156
|
+
delete dialog.dataset['closing'];
|
|
157
|
+
});
|
|
158
|
+
if (dialog.hasAttribute('data-overlay-close-enabled')) {
|
|
159
|
+
let didPressOverlay = false;
|
|
160
|
+
/** Native backdrop events target the dialog, so exclude its panel bounds. */
|
|
161
|
+
const isOverlayEvent = (event: MouseEvent): boolean => {
|
|
162
|
+
if (event.target !== dialog) return false;
|
|
163
|
+
const bounds = dialog.getBoundingClientRect();
|
|
164
|
+
return (
|
|
165
|
+
event.clientX < bounds.left ||
|
|
166
|
+
event.clientX > bounds.right ||
|
|
167
|
+
event.clientY < bounds.top ||
|
|
168
|
+
event.clientY > bounds.bottom
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
dialog.addEventListener('pointerdown', (event) => {
|
|
172
|
+
didPressOverlay = event.isPrimary && event.button === 0 && isOverlayEvent(event);
|
|
173
|
+
});
|
|
174
|
+
dialog.addEventListener('pointercancel', () => {
|
|
175
|
+
didPressOverlay = false;
|
|
176
|
+
});
|
|
177
|
+
dialog.addEventListener('click', (event) => {
|
|
178
|
+
const shouldClose = didPressOverlay && event.button === 0 && isOverlayEvent(event);
|
|
179
|
+
didPressOverlay = false;
|
|
180
|
+
if (shouldClose) void requestClose();
|
|
181
|
+
});
|
|
182
|
+
dialog.addEventListener('close', () => {
|
|
183
|
+
didPressOverlay = false;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
root.dataset['enhanced'] = 'true';
|
|
187
|
+
trigger.hidden = false;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
initializeDialogs();
|
|
192
|
+
document.addEventListener('astro:page-load', initializeDialogs);
|
|
193
|
+
document.addEventListener('astro:before-swap', () => {
|
|
194
|
+
for (const dialog of document.querySelectorAll<HTMLDialogElement>(
|
|
195
|
+
'[data-website-ui-dialog]:modal',
|
|
196
|
+
)) {
|
|
197
|
+
dialog.close();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
</script>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
/* Native modality makes the page inert; also prevent background scrolling. */
|
|
204
|
+
:global(html:has([data-website-ui-dialog]:modal)) {
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Match platform dialog motion without animating content dimensions. */
|
|
209
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
210
|
+
dialog:modal {
|
|
211
|
+
animation: dialog-enter 300ms ease-out both;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
dialog:modal::backdrop {
|
|
215
|
+
animation: dialog-backdrop-enter 300ms ease-out both;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
dialog[data-closing]:modal {
|
|
219
|
+
animation: dialog-exit var(--dialog-exit-duration) ease-in both;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
dialog[data-closing]:modal::backdrop {
|
|
223
|
+
animation: dialog-backdrop-exit 200ms ease-in both;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@keyframes dialog-enter {
|
|
228
|
+
from {
|
|
229
|
+
opacity: 0;
|
|
230
|
+
transform: var(--dialog-motion-transform);
|
|
231
|
+
}
|
|
232
|
+
to {
|
|
233
|
+
opacity: 1;
|
|
234
|
+
transform: none;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@keyframes dialog-exit {
|
|
239
|
+
from {
|
|
240
|
+
opacity: 1;
|
|
241
|
+
transform: none;
|
|
242
|
+
}
|
|
243
|
+
to {
|
|
244
|
+
opacity: 0;
|
|
245
|
+
transform: var(--dialog-motion-transform);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@keyframes dialog-backdrop-enter {
|
|
250
|
+
from {
|
|
251
|
+
opacity: 0;
|
|
252
|
+
}
|
|
253
|
+
to {
|
|
254
|
+
opacity: 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@keyframes dialog-backdrop-exit {
|
|
259
|
+
from {
|
|
260
|
+
opacity: 1;
|
|
261
|
+
}
|
|
262
|
+
to {
|
|
263
|
+
opacity: 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
</style>
|
|
@@ -11,10 +11,7 @@ const { headings } = Astro.props;
|
|
|
11
11
|
{
|
|
12
12
|
headings.map((heading) => (
|
|
13
13
|
<a
|
|
14
|
-
class:list={[
|
|
15
|
-
'text-sm leading-5 text-muted-foreground hover:text-foreground',
|
|
16
|
-
heading.depth === 3 && 'ps-3',
|
|
17
|
-
]}
|
|
14
|
+
class:list={['plain-link text-sm leading-5', heading.depth === 3 && 'ps-3']}
|
|
18
15
|
href={`#${heading.id}`}
|
|
19
16
|
>
|
|
20
17
|
{heading.text}
|
|
@@ -89,12 +89,7 @@ const contentGridClass = hasNavigation
|
|
|
89
89
|
<div class="grid gap-1">
|
|
90
90
|
{group.items.map((item) => (
|
|
91
91
|
<a
|
|
92
|
-
class
|
|
93
|
-
'rounded-lg px-3 py-2.5 text-sm font-medium',
|
|
94
|
-
item.href === currentRoute
|
|
95
|
-
? 'bg-accent text-accent-foreground'
|
|
96
|
-
: 'hover:bg-muted',
|
|
97
|
-
]}
|
|
92
|
+
class="rounded-lg navigation-link px-3 py-2.5 text-sm"
|
|
98
93
|
href={withBase(item.href, basePath)}
|
|
99
94
|
aria-current={item.href === currentRoute ? 'page' : undefined}
|
|
100
95
|
>
|
|
@@ -144,12 +139,7 @@ const contentGridClass = hasNavigation
|
|
|
144
139
|
<div class="grid gap-1">
|
|
145
140
|
{group.items.map((item) => (
|
|
146
141
|
<a
|
|
147
|
-
class
|
|
148
|
-
'rounded-lg px-3 py-2 text-sm font-medium',
|
|
149
|
-
item.href === currentRoute
|
|
150
|
-
? 'bg-accent text-accent-foreground'
|
|
151
|
-
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
152
|
-
]}
|
|
142
|
+
class="rounded-lg navigation-link px-3 py-2 text-sm"
|
|
153
143
|
href={withBase(item.href, basePath)}
|
|
154
144
|
aria-current={item.href === currentRoute ? 'page' : undefined}
|
|
155
145
|
>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { FileText } from '@lucide/astro';
|
|
3
|
+
|
|
4
|
+
import InlineBrandText from '../inline-brand-text/inline-brand-text.component.astro';
|
|
5
|
+
import type { IStatusBadgeTone } from '../status-badge/status-badge.component.astro';
|
|
6
|
+
|
|
7
|
+
// file presentation only; consumers own source selection, status, and body content
|
|
8
|
+
export interface Props {
|
|
9
|
+
path: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
tone?: IStatusBadgeTone;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { path, label, tone = 'neutral' } = Astro.props;
|
|
15
|
+
const filenameStart = path.lastIndexOf('/');
|
|
16
|
+
const directory = filenameStart < 0 ? '' : path.slice(0, filenameStart);
|
|
17
|
+
const filename = filenameStart < 0 ? path : path.slice(filenameStart);
|
|
18
|
+
const toneClasses = {
|
|
19
|
+
danger: 'border-destructive/40 bg-destructive/10',
|
|
20
|
+
info: 'border-info/40 bg-info/10',
|
|
21
|
+
neutral: 'border-border bg-muted/30',
|
|
22
|
+
success: 'border-border bg-secondary',
|
|
23
|
+
warning: 'border-warning/60 bg-warning/15',
|
|
24
|
+
} as const;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div class="min-w-0 overflow-hidden rounded-lg border">
|
|
28
|
+
<div class:list={['flex items-center gap-3 border-b px-3 py-3', toneClasses[tone]]}>
|
|
29
|
+
<span
|
|
30
|
+
class="shrink-0 text-muted-foreground"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
<slot name="icon"><FileText class="size-5" /></slot>
|
|
34
|
+
</span>
|
|
35
|
+
<div class="min-w-0 flex-1">
|
|
36
|
+
{
|
|
37
|
+
label && (
|
|
38
|
+
<p class="mb-1 text-xs text-muted-foreground">
|
|
39
|
+
<InlineBrandText text={label} />
|
|
40
|
+
</p>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
<code
|
|
44
|
+
class="inline-code flex w-fit max-w-full text-xs whitespace-nowrap"
|
|
45
|
+
title={path}
|
|
46
|
+
>
|
|
47
|
+
<span class="min-w-0 truncate">{directory}</span><span
|
|
48
|
+
class="max-w-full shrink-0 break-all whitespace-normal">{filename}</span
|
|
49
|
+
>
|
|
50
|
+
</code>
|
|
51
|
+
</div>
|
|
52
|
+
<slot name="status" />
|
|
53
|
+
</div>
|
|
54
|
+
<slot />
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
// shared static brand backdrop; the consumer owns the positioned section and foreground content
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<div
|
|
6
|
+
class="pointer-events-none absolute inset-0 [background-image:radial-gradient(circle_at_70%_18%,color-mix(in_oklch,var(--accent)_60%,transparent),transparent_30%),linear-gradient(to_right,color-mix(in_oklch,var(--border)_35%,transparent)_1px,transparent_1px),linear-gradient(to_bottom,color-mix(in_oklch,var(--border)_35%,transparent)_1px,transparent_1px)] [mask-image:linear-gradient(to_bottom,black,transparent_92%)] [background-size:auto,3rem_3rem,3rem_3rem] opacity-60"
|
|
7
|
+
aria-hidden="true"
|
|
8
|
+
>
|
|
9
|
+
</div>
|
|
@@ -13,7 +13,7 @@ const segments = text.split(/(\bmoldea\b)/giu);
|
|
|
13
13
|
/^moldea$/iu.test(segment) ? (
|
|
14
14
|
<code
|
|
15
15
|
class:list={[
|
|
16
|
-
'font-mono font-semibold',
|
|
16
|
+
'font-mono font-semibold tracking-normal',
|
|
17
17
|
variant === 'badge'
|
|
18
18
|
? 'rounded-md bg-code px-1.5 py-1 text-[0.86em] text-code-foreground'
|
|
19
19
|
: 'text-[0.92em]',
|