@moonarc/core 0.1.0
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/LICENSE +21 -0
- package/README.md +39 -0
- package/package.json +228 -0
- package/src/components/Accordion.astro +110 -0
- package/src/components/Accordion.meta.ts +33 -0
- package/src/components/AnimatedBeam.astro +217 -0
- package/src/components/AnimatedBeam.meta.ts +39 -0
- package/src/components/AnimatedIcons.astro +193 -0
- package/src/components/AnimatedIcons.meta.ts +34 -0
- package/src/components/AnimatedList.astro +305 -0
- package/src/components/AnimatedList.meta.ts +36 -0
- package/src/components/AnimatedSelect.astro +227 -0
- package/src/components/AnimatedSelect.meta.ts +38 -0
- package/src/components/ArrowButton.astro +133 -0
- package/src/components/ArrowButton.meta.ts +32 -0
- package/src/components/Aurora.astro +188 -0
- package/src/components/Aurora.meta.ts +36 -0
- package/src/components/AvatarStack.astro +205 -0
- package/src/components/AvatarStack.meta.ts +37 -0
- package/src/components/BarGrow.astro +123 -0
- package/src/components/BarGrow.meta.ts +42 -0
- package/src/components/BorderBeam.astro +75 -0
- package/src/components/BorderBeam.meta.ts +33 -0
- package/src/components/BrowserFrame.astro +172 -0
- package/src/components/BrowserFrame.meta.ts +36 -0
- package/src/components/BubbleText.astro +90 -0
- package/src/components/BubbleText.meta.ts +38 -0
- package/src/components/CardStack.astro +79 -0
- package/src/components/CardStack.meta.ts +33 -0
- package/src/components/Carousel.astro +147 -0
- package/src/components/Carousel.meta.ts +35 -0
- package/src/components/CheckDraw.astro +159 -0
- package/src/components/CheckDraw.meta.ts +37 -0
- package/src/components/CircularText.astro +103 -0
- package/src/components/CircularText.meta.ts +38 -0
- package/src/components/CodeTabs.astro +213 -0
- package/src/components/CodeTabs.meta.ts +36 -0
- package/src/components/CommandPalette.astro +363 -0
- package/src/components/CommandPalette.meta.ts +40 -0
- package/src/components/Confetti.astro +143 -0
- package/src/components/Confetti.meta.ts +40 -0
- package/src/components/CopyButton.astro +189 -0
- package/src/components/CopyButton.meta.ts +41 -0
- package/src/components/CountUp.astro +134 -0
- package/src/components/CountUp.meta.ts +40 -0
- package/src/components/CountdownFlip.astro +244 -0
- package/src/components/CountdownFlip.meta.ts +39 -0
- package/src/components/CounterPreloader.astro +122 -0
- package/src/components/CounterPreloader.meta.ts +34 -0
- package/src/components/Cursor.astro +177 -0
- package/src/components/Cursor.meta.ts +37 -0
- package/src/components/CursorGrid.astro +113 -0
- package/src/components/CursorGrid.meta.ts +37 -0
- package/src/components/Curtain.astro +305 -0
- package/src/components/Curtain.meta.ts +38 -0
- package/src/components/Dialog.astro +156 -0
- package/src/components/Dialog.meta.ts +37 -0
- package/src/components/Dock.astro +193 -0
- package/src/components/Dock.meta.ts +35 -0
- package/src/components/DotPattern.astro +58 -0
- package/src/components/DotPattern.meta.ts +32 -0
- package/src/components/DottedMap.astro +153 -0
- package/src/components/DottedMap.meta.ts +38 -0
- package/src/components/Drawer.astro +196 -0
- package/src/components/Drawer.meta.ts +35 -0
- package/src/components/DynamicIsland.astro +122 -0
- package/src/components/DynamicIsland.meta.ts +35 -0
- package/src/components/FileTree.astro +207 -0
- package/src/components/FileTree.meta.ts +34 -0
- package/src/components/FilterGrid.astro +170 -0
- package/src/components/FilterGrid.meta.ts +38 -0
- package/src/components/FooterReveal.astro +54 -0
- package/src/components/FooterReveal.meta.ts +31 -0
- package/src/components/GlitchText.astro +145 -0
- package/src/components/GlitchText.meta.ts +39 -0
- package/src/components/Globe.astro +304 -0
- package/src/components/Globe.meta.ts +41 -0
- package/src/components/GlyphField.astro +363 -0
- package/src/components/GlyphField.meta.ts +53 -0
- package/src/components/GradientText.astro +63 -0
- package/src/components/GradientText.meta.ts +35 -0
- package/src/components/GridPattern.astro +55 -0
- package/src/components/GridPattern.meta.ts +32 -0
- package/src/components/HighlightMarker.astro +68 -0
- package/src/components/HighlightMarker.meta.ts +37 -0
- package/src/components/HorizontalScroll.astro +112 -0
- package/src/components/HorizontalScroll.meta.ts +34 -0
- package/src/components/HoverImageLinks.astro +219 -0
- package/src/components/HoverImageLinks.meta.ts +34 -0
- package/src/components/HoverVideo.astro +179 -0
- package/src/components/HoverVideo.meta.ts +37 -0
- package/src/components/ImageCompare.astro +212 -0
- package/src/components/ImageCompare.meta.ts +39 -0
- package/src/components/ImageTrail.astro +169 -0
- package/src/components/ImageTrail.meta.ts +36 -0
- package/src/components/Lightbox.astro +236 -0
- package/src/components/Lightbox.meta.ts +40 -0
- package/src/components/LinkPreview.astro +234 -0
- package/src/components/LinkPreview.meta.ts +39 -0
- package/src/components/LiquidGlass.astro +148 -0
- package/src/components/LiquidGlass.meta.ts +37 -0
- package/src/components/Magnetic.astro +92 -0
- package/src/components/Magnetic.meta.ts +31 -0
- package/src/components/Marquee.astro +165 -0
- package/src/components/Marquee.meta.ts +38 -0
- package/src/components/Meteors.astro +156 -0
- package/src/components/Meteors.meta.ts +36 -0
- package/src/components/Noise.astro +61 -0
- package/src/components/Noise.meta.ts +33 -0
- package/src/components/NumberFlow.astro +143 -0
- package/src/components/NumberFlow.meta.ts +42 -0
- package/src/components/Orb.astro +130 -0
- package/src/components/Orb.meta.ts +39 -0
- package/src/components/OrbitingCircles.astro +119 -0
- package/src/components/OrbitingCircles.meta.ts +36 -0
- package/src/components/OtpInput.astro +215 -0
- package/src/components/OtpInput.meta.ts +42 -0
- package/src/components/PageTransition.astro +121 -0
- package/src/components/PageTransition.meta.ts +33 -0
- package/src/components/Parallax.astro +69 -0
- package/src/components/Parallax.meta.ts +32 -0
- package/src/components/Particles.astro +210 -0
- package/src/components/Particles.meta.ts +37 -0
- package/src/components/PersistPlayer.astro +126 -0
- package/src/components/PersistPlayer.meta.ts +35 -0
- package/src/components/PhoneFrame.astro +161 -0
- package/src/components/PhoneFrame.meta.ts +35 -0
- package/src/components/PillNav.astro +170 -0
- package/src/components/PillNav.meta.ts +37 -0
- package/src/components/Popover.astro +95 -0
- package/src/components/Popover.meta.ts +34 -0
- package/src/components/PriceSwitch.astro +251 -0
- package/src/components/PriceSwitch.meta.ts +41 -0
- package/src/components/Probe.astro +84 -0
- package/src/components/Progress.astro +165 -0
- package/src/components/Progress.meta.ts +40 -0
- package/src/components/ProgressiveBlur.astro +95 -0
- package/src/components/ProgressiveBlur.meta.ts +36 -0
- package/src/components/PromptInput.astro +273 -0
- package/src/components/PromptInput.meta.ts +46 -0
- package/src/components/ReadingToc.astro +153 -0
- package/src/components/ReadingToc.meta.ts +34 -0
- package/src/components/RetroGrid.astro +84 -0
- package/src/components/RetroGrid.meta.ts +36 -0
- package/src/components/Reveal.astro +292 -0
- package/src/components/Reveal.meta.ts +48 -0
- package/src/components/Ripple.astro +94 -0
- package/src/components/Ripple.meta.ts +34 -0
- package/src/components/RotatingText.astro +170 -0
- package/src/components/RotatingText.meta.ts +35 -0
- package/src/components/ScrollExpand.astro +90 -0
- package/src/components/ScrollExpand.meta.ts +35 -0
- package/src/components/ScrollProgress.astro +73 -0
- package/src/components/ScrollProgress.meta.ts +32 -0
- package/src/components/ScrollScrub.astro +96 -0
- package/src/components/ScrollScrub.meta.ts +37 -0
- package/src/components/ShimmerButton.astro +109 -0
- package/src/components/ShimmerButton.meta.ts +37 -0
- package/src/components/ShinyText.astro +72 -0
- package/src/components/ShinyText.meta.ts +35 -0
- package/src/components/Skeleton.astro +76 -0
- package/src/components/Skeleton.meta.ts +32 -0
- package/src/components/Sparkline.astro +168 -0
- package/src/components/Sparkline.meta.ts +43 -0
- package/src/components/Spinner.astro +79 -0
- package/src/components/Spinner.meta.ts +33 -0
- package/src/components/SplitFlap.astro +157 -0
- package/src/components/SplitFlap.meta.ts +39 -0
- package/src/components/SplitText.astro +208 -0
- package/src/components/SplitText.meta.ts +50 -0
- package/src/components/Spotlight.astro +92 -0
- package/src/components/Spotlight.meta.ts +34 -0
- package/src/components/StaggerMenu.astro +301 -0
- package/src/components/StaggerMenu.meta.ts +39 -0
- package/src/components/StatefulButton.astro +148 -0
- package/src/components/StatefulButton.meta.ts +35 -0
- package/src/components/StickyBanner.astro +168 -0
- package/src/components/StickyBanner.meta.ts +45 -0
- package/src/components/StickyReveal.astro +256 -0
- package/src/components/StickyReveal.meta.ts +33 -0
- package/src/components/StrokeDraw.astro +110 -0
- package/src/components/StrokeDraw.meta.ts +43 -0
- package/src/components/Switch.astro +165 -0
- package/src/components/Switch.meta.ts +37 -0
- package/src/components/Tabs.astro +194 -0
- package/src/components/Tabs.meta.ts +47 -0
- package/src/components/TextRoll.astro +78 -0
- package/src/components/TextRoll.meta.ts +36 -0
- package/src/components/TextScramble.astro +107 -0
- package/src/components/TextScramble.meta.ts +35 -0
- package/src/components/ThemeScript.astro +17 -0
- package/src/components/ThemeToggle.astro +182 -0
- package/src/components/ThemeToggle.meta.ts +34 -0
- package/src/components/Tilt.astro +97 -0
- package/src/components/Tilt.meta.ts +34 -0
- package/src/components/Toast.astro +184 -0
- package/src/components/Toast.meta.ts +33 -0
- package/src/components/TracingBeam.astro +113 -0
- package/src/components/TracingBeam.meta.ts +34 -0
- package/src/components/Typewriter.astro +124 -0
- package/src/components/Typewriter.meta.ts +39 -0
- package/src/components/UnderlineHover.astro +61 -0
- package/src/components/UnderlineHover.meta.ts +34 -0
- package/src/components/VanishInput.astro +240 -0
- package/src/components/VanishInput.meta.ts +41 -0
- package/src/components/VideoDialog.astro +230 -0
- package/src/components/VideoDialog.meta.ts +40 -0
- package/src/components/WeightWave.astro +70 -0
- package/src/components/WeightWave.meta.ts +36 -0
- package/src/components/internal/DialogMorph.astro +97 -0
- package/src/components/internal/PromptInputSubmitOnEnter.astro +32 -0
- package/src/index.d.ts +43 -0
- package/src/index.js +60 -0
- package/src/lib/canvas.ts +124 -0
- package/src/lib/confetti.ts +127 -0
- package/src/lib/contrast.ts +116 -0
- package/src/lib/form.ts +231 -0
- package/src/lib/geo.ts +77 -0
- package/src/lib/gsap.ts +58 -0
- package/src/lib/land.ts +11 -0
- package/src/lib/lenis.ts +112 -0
- package/src/lib/meta.ts +79 -0
- package/src/lib/morph.ts +74 -0
- package/src/lib/reduced-motion-plugin.js +299 -0
- package/src/lib/runtime.ts +215 -0
- package/src/lib/support.ts +102 -0
- package/src/lib/theme.ts +18 -0
- package/src/lib/toast.ts +17 -0
- package/src/styles/base.css +218 -0
- package/src/styles/motion.css +43 -0
- package/src/styles/theme.css +397 -0
- package/src/styles/theme.tailwind.css +75 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The script half of `<Dialog morph>`, rendered only when the prop is set,
|
|
4
|
+
* so a plain Dialog stays at 0 B (measured on its own fixture: `Dialog.morph`).
|
|
5
|
+
* It intercepts the invoker click on every trigger that points at the
|
|
6
|
+
* dialog, the close form, Escape and the backdrop click, and runs each
|
|
7
|
+
* through lib/morph so the box grows from the trigger and returns to it.
|
|
8
|
+
* The styles below shape the shared-name group; the root cross-fade is in
|
|
9
|
+
* base.css under html[data-ma-vt].
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
/** The dialog's id. */
|
|
13
|
+
for: string;
|
|
14
|
+
}
|
|
15
|
+
const { for: id } = Astro.props;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<span class="ma-dialog-morph" data-ma-dialog-morph={id} hidden></span>
|
|
19
|
+
|
|
20
|
+
<style is:global>
|
|
21
|
+
@layer components {
|
|
22
|
+
/* the morphing box: both snapshots fill the group without distortion, so a thumbnail becomes an image, not a smear */
|
|
23
|
+
:where(:root)[data-ma-vt='morph']::view-transition-old(ma-morph),
|
|
24
|
+
:where(:root)[data-ma-vt='morph']::view-transition-new(ma-morph) {
|
|
25
|
+
block-size: 100%;
|
|
26
|
+
inline-size: 100%;
|
|
27
|
+
object-fit: cover;
|
|
28
|
+
overflow: clip;
|
|
29
|
+
animation-duration: var(--ma-duration);
|
|
30
|
+
animation-timing-function: var(--ma-ease-out);
|
|
31
|
+
}
|
|
32
|
+
:where(:root)[data-ma-vt='morph']::view-transition-group(ma-morph) {
|
|
33
|
+
animation-duration: var(--ma-duration);
|
|
34
|
+
animation-timing-function: var(--ma-ease);
|
|
35
|
+
z-index: 2;
|
|
36
|
+
}
|
|
37
|
+
/* while the library's transition runs, the dialog's own entrance and exit step aside: the transition is the motion.
|
|
38
|
+
The attribute on <html> is what outranks the Dialog's zero-specificity rules (the same :where() here lost to them on
|
|
39
|
+
order), and it scopes the step-aside to the transition, so without one (no support, reduced motion) the dialog
|
|
40
|
+
opens and closes as usual. */
|
|
41
|
+
:where(:root)[data-ma-vt] :where(.ma-dialog[data-morph]) {
|
|
42
|
+
transition: none;
|
|
43
|
+
scale: 1;
|
|
44
|
+
translate: 0 0;
|
|
45
|
+
}
|
|
46
|
+
:where(:root)[data-ma-vt] :where(.ma-dialog[data-morph])::backdrop {
|
|
47
|
+
transition: none;
|
|
48
|
+
}
|
|
49
|
+
@media (prefers-reduced-motion: reduce) {
|
|
50
|
+
:where(:root)[data-ma-vt='morph']::view-transition-group(ma-morph) {
|
|
51
|
+
animation-duration: var(--ma-duration-fast);
|
|
52
|
+
animation-timing-function: linear;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
import { onMount } from '../../lib/runtime';
|
|
60
|
+
import { morphClose, morphOpen } from '../../lib/morph';
|
|
61
|
+
|
|
62
|
+
onMount<HTMLElement>('[data-ma-dialog-morph]', (mark, { signal }) => {
|
|
63
|
+
const dialog = document.getElementById(mark.dataset.maDialogMorph!) as HTMLDialogElement | null;
|
|
64
|
+
if (!dialog) return;
|
|
65
|
+
// every trigger on the page that points at this dialog: the click is ours, the invoker's default is cancelled (and the polyfill honours defaultPrevented)
|
|
66
|
+
for (const t of document.querySelectorAll<HTMLElement>(`[commandfor="${dialog.id}"][command="show-modal"]`)) {
|
|
67
|
+
t.addEventListener(
|
|
68
|
+
'click',
|
|
69
|
+
(e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
morphOpen(dialog, t);
|
|
72
|
+
},
|
|
73
|
+
{ signal },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
// Escape and the backdrop (closedby) arrive as a cancelable `cancel`; the close button is a form with method="dialog"
|
|
77
|
+
dialog.addEventListener(
|
|
78
|
+
'cancel',
|
|
79
|
+
(e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
morphClose(dialog);
|
|
82
|
+
},
|
|
83
|
+
{ signal },
|
|
84
|
+
);
|
|
85
|
+
dialog.addEventListener(
|
|
86
|
+
'submit',
|
|
87
|
+
(e) => {
|
|
88
|
+
const f = e.target as HTMLFormElement;
|
|
89
|
+
if (f.method === 'dialog') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
morphClose(dialog);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{ signal },
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The script half of `<PromptInput submitOnEnter>`, rendered only when the
|
|
4
|
+
* prop is set, so a plain PromptInput stays at 0 B (measured on its own
|
|
5
|
+
* fixture: `PromptInput.submitOnEnter`). Enter submits through
|
|
6
|
+
* form.requestSubmit(), so required and the host's submit handler still
|
|
7
|
+
* run; Shift+Enter is a newline; nothing happens while an IME is composing
|
|
8
|
+
* (isComposing, or the legacy keyCode 229), so a Japanese or Chinese
|
|
9
|
+
* reader's Enter confirms the candidate instead of sending the message.
|
|
10
|
+
*/
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<span class="ma-prompt-enter" data-ma-prompt-enter hidden></span>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import { onMount } from '../../lib/runtime';
|
|
17
|
+
|
|
18
|
+
onMount<HTMLElement>('[data-ma-prompt-enter]', (mark, { signal }) => {
|
|
19
|
+
const form = mark.closest('form');
|
|
20
|
+
const area = form?.querySelector('textarea');
|
|
21
|
+
if (!form || !area) return;
|
|
22
|
+
area.addEventListener(
|
|
23
|
+
'keydown',
|
|
24
|
+
(e) => {
|
|
25
|
+
if (e.key !== 'Enter' || e.shiftKey || e.isComposing || e.keyCode === 229) return;
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
if (!form.hasAttribute('data-pending')) form.requestSubmit();
|
|
28
|
+
},
|
|
29
|
+
{ signal },
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
</script>
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
|
|
3
|
+
export interface MoonarcOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Inject `@moonarc/core/styles/base.css` (theme-aware tokens) into every
|
|
6
|
+
* page. Set to `false` and import the stylesheet yourself to control order.
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
injectStyles?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Inject a tiny inline script that sets `data-ma-js` on `<html>` before paint,
|
|
12
|
+
* and again after every `<ClientRouter />` swap. Entrance effects hide content
|
|
13
|
+
* only behind that attribute, so pages without JS stay readable. Disable it
|
|
14
|
+
* and inline `gateScript` yourself if you manage head scripts.
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
jsGate?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Add `@moonarc/core` to `vite.ssr.noExternal`. Not needed on Astro 5–7;
|
|
20
|
+
* kept as an escape hatch for setups that externalise `.astro` files.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
noExternal?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Mirror every `@media (prefers-reduced-motion: reduce)` rule — the
|
|
26
|
+
* components', Tailwind's `motion-reduce:` utilities, your own — to
|
|
27
|
+
* `html[data-ma-motion='reduce']` at build time, so a page can offer a
|
|
28
|
+
* reduced-motion switch that previews exactly what the media query does.
|
|
29
|
+
* The runtime's `prefersReducedMotion()` reads the attribute too. Costs a
|
|
30
|
+
* few percent of CSS, no JavaScript.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
motionAttribute?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The text of the gate `jsGate` injects: sets `data-ma-js` on `<html>` now and
|
|
38
|
+
* on every `astro:after-swap` (the router replaces `<html>`'s attributes and
|
|
39
|
+
* does not re-run an inline script it has already run).
|
|
40
|
+
*/
|
|
41
|
+
export declare const gateScript: string;
|
|
42
|
+
|
|
43
|
+
export default function moonarc(options?: MoonarcOptions): AstroIntegration;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @moonarc/core — Astro integration.
|
|
3
|
+
*
|
|
4
|
+
* Plain JS on purpose: this file is evaluated while Astro loads the consumer's
|
|
5
|
+
* config, before any Vite transform exists. Types live in ./index.d.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { reducedMotionMirror } from './lib/reduced-motion-plugin.js';
|
|
9
|
+
|
|
10
|
+
const PKG = '@moonarc/core';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The head gate `jsGate` injects: `data-ma-js` on <html> now and after every
|
|
14
|
+
* ClientRouter swap (`dataset.maJs` is that attribute). f() sets it and returns
|
|
15
|
+
* 1, which is also the capture flag: the router dispatches astro:after-swap on
|
|
16
|
+
* document without bubbling, so a window listener hears it only while
|
|
17
|
+
* capturing, and then ahead of every listener on document. Exported so a
|
|
18
|
+
* layout that sets `jsGate: false` can inline the same text.
|
|
19
|
+
*/
|
|
20
|
+
export const gateScript = `(f=>addEventListener('astro:after-swap',f,f()))(()=>document.documentElement.dataset.maJs=1)`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('./index.js').MoonarcOptions} [options]
|
|
24
|
+
* @returns {import('astro').AstroIntegration}
|
|
25
|
+
*/
|
|
26
|
+
export default function moonarc(options = {}) {
|
|
27
|
+
const { injectStyles = true, jsGate = true, noExternal = false, motionAttribute = false } = options;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
name: 'moonarc',
|
|
31
|
+
hooks: {
|
|
32
|
+
'astro:config:setup': ({ injectScript, updateConfig }) => {
|
|
33
|
+
// Theme + motion tokens on every page, resolved and bundled by Vite.
|
|
34
|
+
if (injectStyles) {
|
|
35
|
+
injectScript('page-ssr', `import '${PKG}/styles/base.css';`);
|
|
36
|
+
}
|
|
37
|
+
// Entrance effects (Reveal, …) hide content only under html[data-ma-js],
|
|
38
|
+
// so server HTML stays readable when JS never runs. Set before paint,
|
|
39
|
+
// and again after every ClientRouter swap: the router replaces <html>'s
|
|
40
|
+
// attributes with the next page's and never re-runs this script (same
|
|
41
|
+
// text), so a session that landed on a page without a Moonarc script,
|
|
42
|
+
// and so without the runtime's carry-over, lost the gate for good.
|
|
43
|
+
if (jsGate) {
|
|
44
|
+
injectScript('head-inline', gateScript);
|
|
45
|
+
}
|
|
46
|
+
// Phase 0 consumer checks established that Astro 5–7 compile .astro
|
|
47
|
+
// files from node_modules without this. It stays as an escape hatch
|
|
48
|
+
// for exotic setups (see docs/integration).
|
|
49
|
+
if (noExternal) {
|
|
50
|
+
updateConfig({ vite: { ssr: { noExternal: [PKG] } } });
|
|
51
|
+
}
|
|
52
|
+
// Mirror every reduced-motion rule to html[data-ma-motion='reduce'] at
|
|
53
|
+
// build, so a page can offer a switch that previews the media query.
|
|
54
|
+
if (motionAttribute) {
|
|
55
|
+
updateConfig({ vite: { plugins: [reducedMotionMirror()] } });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @moonarc/core/canvas — the shared half of every canvas component
|
|
3
|
+
* (Particles, Globe, Confetti, and yours). A canvas is the one place the
|
|
4
|
+
* library cannot let CSS do the moving, so the rules that keep a CSS
|
|
5
|
+
* component cheap are written here once:
|
|
6
|
+
*
|
|
7
|
+
* fit(canvas, host, signal, onSize) size from a ResizeObserver, DPR capped at 2,
|
|
8
|
+
* the context pre-scaled — never the resize event
|
|
9
|
+
* loop(frame, signal) a requestAnimationFrame loop with wake()/sleep():
|
|
10
|
+
* it never runs in a hidden tab
|
|
11
|
+
* visible(host, signal, cb) IntersectionObserver → cb(true|false); pair it with
|
|
12
|
+
* loop so an off-screen canvas costs nothing
|
|
13
|
+
* onTheme(signal, cb) data-theme / class / data-ma-motion on <html>, the OS
|
|
14
|
+
* scheme or the OS reduced-motion setting changed — re-read
|
|
15
|
+
* colours, re-decide the loop
|
|
16
|
+
* (ThemeScript's contract; the page's reduced-motion switch)
|
|
17
|
+
* colors(el) five theme-resolved colours: the element's `color`
|
|
18
|
+
* and four border colours, which a component's CSS
|
|
19
|
+
* maps its tokens onto (rgb strings a 2D context takes)
|
|
20
|
+
* num(el, name, fallback) a unitless custom property as a number — physics
|
|
21
|
+
* constants live in CSS, not in the script
|
|
22
|
+
* ms(el, name, fallback) a duration token in milliseconds (`450ms` or `1.39s`)
|
|
23
|
+
*
|
|
24
|
+
* Everything is released through the AbortSignal the runtime hands a mount.
|
|
25
|
+
* Zero dependencies; ~1 kB.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type Frame = (t: number, dt: number) => void;
|
|
29
|
+
|
|
30
|
+
export interface Loop {
|
|
31
|
+
wake(): void;
|
|
32
|
+
sleep(): void;
|
|
33
|
+
/** Draw one frame now without starting the loop (reduced motion, theme change). */
|
|
34
|
+
once(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Cap the device pixel ratio: 3× canvases cost nine times the paint of 1× for no visible gain in a dot field. */
|
|
38
|
+
export const dpr = (): number => Math.min(2, window.devicePixelRatio || 1);
|
|
39
|
+
|
|
40
|
+
export function fit(canvas: HTMLCanvasElement, host: Element, signal: AbortSignal, onSize: (w: number, h: number, ctx: CanvasRenderingContext2D) => void): void {
|
|
41
|
+
const ctx = canvas.getContext('2d')!;
|
|
42
|
+
const apply = () => {
|
|
43
|
+
const r = host.getBoundingClientRect();
|
|
44
|
+
const w = Math.max(1, Math.round(r.width));
|
|
45
|
+
const h = Math.max(1, Math.round(r.height));
|
|
46
|
+
const k = dpr();
|
|
47
|
+
canvas.width = w * k;
|
|
48
|
+
canvas.height = h * k;
|
|
49
|
+
ctx.setTransform(k, 0, 0, k, 0, 0);
|
|
50
|
+
onSize(w, h, ctx);
|
|
51
|
+
};
|
|
52
|
+
const ro = new ResizeObserver(apply);
|
|
53
|
+
ro.observe(host);
|
|
54
|
+
signal.addEventListener('abort', () => ro.disconnect());
|
|
55
|
+
apply();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loop(frame: Frame, signal: AbortSignal): Loop {
|
|
59
|
+
let raf = 0;
|
|
60
|
+
let last = 0;
|
|
61
|
+
let want = false;
|
|
62
|
+
const tick = (t: number) => {
|
|
63
|
+
raf = 0;
|
|
64
|
+
if (!want || document.hidden) return;
|
|
65
|
+
frame(t, last ? Math.min(t - last, 100) : 0);
|
|
66
|
+
last = t;
|
|
67
|
+
raf = requestAnimationFrame(tick);
|
|
68
|
+
};
|
|
69
|
+
const l: Loop = {
|
|
70
|
+
wake() {
|
|
71
|
+
want = true;
|
|
72
|
+
if (!raf && !document.hidden) {
|
|
73
|
+
last = 0;
|
|
74
|
+
raf = requestAnimationFrame(tick);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
sleep() {
|
|
78
|
+
want = false;
|
|
79
|
+
cancelAnimationFrame(raf);
|
|
80
|
+
raf = 0;
|
|
81
|
+
},
|
|
82
|
+
once() {
|
|
83
|
+
frame(performance.now(), 0);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
// Hiding cancels nothing: a hidden tab runs no frames, and the one still queued either fires on return or, where
|
|
87
|
+
// an engine runs it while hidden, finds document.hidden and clears raf. Cancelling it here left raf holding a dead
|
|
88
|
+
// id, and wake() on return then saw a frame pending and never asked for one: the canvas froze after a tab switch.
|
|
89
|
+
document.addEventListener('visibilitychange', () => want && l.wake(), { signal });
|
|
90
|
+
signal.addEventListener('abort', l.sleep);
|
|
91
|
+
return l;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function visible(host: Element, signal: AbortSignal, cb: (on: boolean) => void): void {
|
|
95
|
+
const io = new IntersectionObserver(([e]) => cb(!!e?.isIntersecting));
|
|
96
|
+
io.observe(host);
|
|
97
|
+
signal.addEventListener('abort', () => io.disconnect());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function onTheme(signal: AbortSignal, cb: () => void): void {
|
|
101
|
+
const mo = new MutationObserver(cb);
|
|
102
|
+
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class', 'data-ma-motion'] });
|
|
103
|
+
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', cb, { signal });
|
|
104
|
+
// reduced motion turned on or off in the OS while the page is open: a running loop stops (or starts) without a reload
|
|
105
|
+
matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', cb, { signal });
|
|
106
|
+
signal.addEventListener('abort', () => mo.disconnect());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `color`, then border top / right / bottom / left — resolved by the engine to rgb, so tokens, color-mix() and oklch() all arrive as something the context can paint. */
|
|
110
|
+
export function colors(el: Element): [string, string, string, string, string] {
|
|
111
|
+
const s = getComputedStyle(el);
|
|
112
|
+
return [s.color, s.borderTopColor, s.borderRightColor, s.borderBottomColor, s.borderLeftColor];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function num(el: Element, name: string, fallback: number): number {
|
|
116
|
+
const v = parseFloat(getComputedStyle(el).getPropertyValue(name));
|
|
117
|
+
return Number.isFinite(v) ? v : fallback;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ms(el: Element, name: string, fallback: number): number {
|
|
121
|
+
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
|
122
|
+
const n = parseFloat(v);
|
|
123
|
+
return Number.isFinite(n) ? (v.endsWith('ms') ? n : n * 1000) : fallback;
|
|
124
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* confetti(options?) — a burst of paper from a point, on one fixed canvas that
|
|
3
|
+
* exists only while pieces are in the air. The `<Confetti />` component wires
|
|
4
|
+
* this to a load, a click or an `ma:confetti` event on document; call it
|
|
5
|
+
* directly from your own handler.
|
|
6
|
+
*
|
|
7
|
+
* Physics constants are custom properties on :root (the component's stylesheet
|
|
8
|
+
* declares them per preset; without it the defaults below apply):
|
|
9
|
+
* --ma-confetti-gravity px/frame² at 60 fps, pulls the pieces down
|
|
10
|
+
* --ma-confetti-drag velocity kept per frame, 0–1
|
|
11
|
+
* --ma-confetti-velocity launch speed in px/frame
|
|
12
|
+
* --ma-confetti-spread launch cone in degrees
|
|
13
|
+
* --ma-confetti-flutter how fast a piece rocks as it falls
|
|
14
|
+
* Bursts share the canvas; the last piece off screen removes it. Nothing fires
|
|
15
|
+
* under prefers-reduced-motion (or the page's data-ma-motion switch).
|
|
16
|
+
*/
|
|
17
|
+
import { loop, num, type Loop } from './canvas';
|
|
18
|
+
import { prefersReducedMotion } from './runtime';
|
|
19
|
+
|
|
20
|
+
export interface ConfettiOptions {
|
|
21
|
+
/** Pieces per burst. */
|
|
22
|
+
count?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Literal colours (hex, rgb(), oklch()); the burst cycles through them. var() is not resolved by the canvas — for
|
|
25
|
+
* tokens use CSS on .ma-confetti (the component reads its computed colours), or pass getComputedStyle(el).color.
|
|
26
|
+
*/
|
|
27
|
+
colors?: string[];
|
|
28
|
+
/** Launch point in viewport px; default the centre. */
|
|
29
|
+
origin?: { x: number; y: number };
|
|
30
|
+
/** Launch direction in degrees, 0 = up, 90 = right. */
|
|
31
|
+
angle?: number;
|
|
32
|
+
/** Launch cone in degrees; overrides the token. */
|
|
33
|
+
spread?: number;
|
|
34
|
+
/** Launch speed in px/frame; overrides the token. */
|
|
35
|
+
velocity?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Piece {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
vx: number;
|
|
42
|
+
vy: number;
|
|
43
|
+
w: number;
|
|
44
|
+
h: number;
|
|
45
|
+
a: number;
|
|
46
|
+
va: number;
|
|
47
|
+
f: number;
|
|
48
|
+
c: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
52
|
+
let ctx: CanvasRenderingContext2D;
|
|
53
|
+
let pieces: Piece[] = [];
|
|
54
|
+
let run: Loop | null = null;
|
|
55
|
+
let abort: AbortController | null = null;
|
|
56
|
+
|
|
57
|
+
function teardown(): void {
|
|
58
|
+
abort?.abort();
|
|
59
|
+
canvas?.remove();
|
|
60
|
+
canvas = null;
|
|
61
|
+
run = null;
|
|
62
|
+
pieces = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function frame(): void {
|
|
66
|
+
if (!canvas) return;
|
|
67
|
+
const w = innerWidth;
|
|
68
|
+
const h = innerHeight;
|
|
69
|
+
const k = Math.min(2, devicePixelRatio || 1);
|
|
70
|
+
if (canvas.width !== w * k || canvas.height !== h * k) {
|
|
71
|
+
canvas.width = w * k;
|
|
72
|
+
canvas.height = h * k;
|
|
73
|
+
}
|
|
74
|
+
ctx.setTransform(k, 0, 0, k, 0, 0);
|
|
75
|
+
ctx.clearRect(0, 0, w, h);
|
|
76
|
+
const root = document.documentElement;
|
|
77
|
+
const g = num(root, '--ma-confetti-gravity', 0.22);
|
|
78
|
+
const drag = num(root, '--ma-confetti-drag', 0.985);
|
|
79
|
+
const fl = num(root, '--ma-confetti-flutter', 0.12);
|
|
80
|
+
pieces = pieces.filter((p) => {
|
|
81
|
+
p.vy += g;
|
|
82
|
+
p.vx *= drag;
|
|
83
|
+
p.vy *= drag;
|
|
84
|
+
p.x += p.vx;
|
|
85
|
+
p.y += p.vy;
|
|
86
|
+
p.a += p.va;
|
|
87
|
+
p.f += fl;
|
|
88
|
+
ctx.save();
|
|
89
|
+
ctx.translate(p.x, p.y);
|
|
90
|
+
ctx.rotate(p.a);
|
|
91
|
+
ctx.scale(1, Math.cos(p.f));
|
|
92
|
+
ctx.fillStyle = p.c;
|
|
93
|
+
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
94
|
+
ctx.restore();
|
|
95
|
+
return p.y < h + 40 && p.x > -40 && p.x < w + 40;
|
|
96
|
+
});
|
|
97
|
+
if (pieces.length === 0) teardown();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function confetti(options: ConfettiOptions = {}): void {
|
|
101
|
+
if (prefersReducedMotion()) return;
|
|
102
|
+
const root = document.documentElement;
|
|
103
|
+
const count = options.count ?? 120;
|
|
104
|
+
const colors = options.colors?.length ? options.colors : ['#f5a524', '#e4572e', '#3a9a5b', '#4c7dff', '#c94ad3'];
|
|
105
|
+
const ox = options.origin?.x ?? innerWidth / 2;
|
|
106
|
+
const oy = options.origin?.y ?? innerHeight / 2;
|
|
107
|
+
const spread = ((options.spread ?? num(root, '--ma-confetti-spread', 70)) * Math.PI) / 180;
|
|
108
|
+
const v = options.velocity ?? num(root, '--ma-confetti-velocity', 14);
|
|
109
|
+
const dir = ((options.angle ?? 0) * Math.PI) / 180;
|
|
110
|
+
for (let i = 0; i < count; i++) {
|
|
111
|
+
const a = dir + (Math.random() - 0.5) * spread;
|
|
112
|
+
const s = v * (0.5 + Math.random());
|
|
113
|
+
pieces.push({ x: ox, y: oy, vx: Math.sin(a) * s, vy: -Math.cos(a) * s, w: 6 + Math.random() * 6, h: 4 + Math.random() * 8, a: Math.random() * Math.PI, va: (Math.random() - 0.5) * 0.3, f: Math.random() * Math.PI, c: colors[i % colors.length]! });
|
|
114
|
+
}
|
|
115
|
+
if (!canvas) {
|
|
116
|
+
canvas = document.createElement('canvas');
|
|
117
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
118
|
+
canvas.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:2147483647';
|
|
119
|
+
ctx = canvas.getContext('2d')!;
|
|
120
|
+
document.body.append(canvas);
|
|
121
|
+
abort = new AbortController();
|
|
122
|
+
run = loop(frame, abort.signal);
|
|
123
|
+
// a navigation mid-burst takes the canvas with it; a new page starts clean
|
|
124
|
+
document.addEventListener('astro:before-swap', teardown, { signal: abort.signal });
|
|
125
|
+
}
|
|
126
|
+
run!.wake();
|
|
127
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time contrast for highlighted code. `readableCode()` is a Shiki transformer (pass it to Astro's <Code
|
|
3
|
+
* transformers>, or to markdown.shikiConfig.transformers) that moves every token colour's OKLCH lightness — hue kept,
|
|
4
|
+
* chroma kept unless sRGB cannot hold it — just far enough to read at `ratio` against the ground it is shown on. A colour
|
|
5
|
+
* that already passes is untouched, so a theme keeps its look and only its faint greys, reds and oranges get firmer.
|
|
6
|
+
* Any theme becomes AA; nothing runs in the browser.
|
|
7
|
+
*
|
|
8
|
+
* Grounds: `light` and `dark` (the `--shiki-light` / `--shiki-dark` variables of a dual theme) default to a light and a
|
|
9
|
+
* dark panel a little heavier than the library's own, so a block stays AA on a tinted surface too. A single theme's
|
|
10
|
+
* `color` is read against the background the theme paints on its <pre>.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type Rgb = [number, number, number];
|
|
14
|
+
interface HastNode {
|
|
15
|
+
type: string;
|
|
16
|
+
tagName?: string;
|
|
17
|
+
properties?: Record<string, unknown>;
|
|
18
|
+
children?: HastNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ReadableCodeOptions {
|
|
22
|
+
/** Minimum WCAG 2 ratio. */
|
|
23
|
+
ratio?: number;
|
|
24
|
+
/** Ground per theme key (`light`, `dark`, `color`, or a custom key of `themes`), as #rrggbb. */
|
|
25
|
+
grounds?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const GROUNDS: Record<string, string> = { light: '#eceff1', dark: '#2a2f33' };
|
|
29
|
+
|
|
30
|
+
const toLin = (c: number) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
31
|
+
const fromLin = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
32
|
+
const lum = ([r, g, b]: Rgb) => 0.2126 * toLin(r) + 0.7152 * toLin(g) + 0.0722 * toLin(b);
|
|
33
|
+
const ratioOf = (a: Rgb, b: Rgb) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
34
|
+
|
|
35
|
+
function parseHex(hex: string): { rgb: Rgb; alpha: string } | null {
|
|
36
|
+
let h = hex.slice(1);
|
|
37
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join('');
|
|
38
|
+
if (h.length !== 6 && h.length !== 8) return null;
|
|
39
|
+
const n = (i: number) => parseInt(h.slice(i, i + 2), 16) / 255;
|
|
40
|
+
return { rgb: [n(0), n(2), n(4)], alpha: h.slice(6) };
|
|
41
|
+
}
|
|
42
|
+
const toHex = (rgb: Rgb) => '#' + rgb.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, '0')).join('');
|
|
43
|
+
|
|
44
|
+
function toOklch([r, g, b]: Rgb): Rgb {
|
|
45
|
+
const [lr, lg, lb] = [toLin(r), toLin(g), toLin(b)];
|
|
46
|
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
47
|
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
48
|
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
49
|
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
50
|
+
const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
51
|
+
const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
52
|
+
return [L, Math.hypot(A, B), Math.atan2(B, A)];
|
|
53
|
+
}
|
|
54
|
+
function fromOklch(L: number, C: number, h: number): Rgb | null {
|
|
55
|
+
const A = C * Math.cos(h), B = C * Math.sin(h);
|
|
56
|
+
const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
|
|
57
|
+
const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
|
|
58
|
+
const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
|
|
59
|
+
const lin = [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
60
|
+
if (lin.some((c) => c < -1e-4 || c > 1 + 1e-4)) return null;
|
|
61
|
+
return lin.map((c) => fromLin(Math.min(1, Math.max(0, c)))) as Rgb;
|
|
62
|
+
}
|
|
63
|
+
/** The colour at lightness L with this hue, and as much of chroma C as sRGB can hold. */
|
|
64
|
+
function inGamut(L: number, C: number, h: number): Rgb {
|
|
65
|
+
const direct = fromOklch(L, C, h);
|
|
66
|
+
if (direct) return direct;
|
|
67
|
+
let lo = 0, hi = C;
|
|
68
|
+
for (let i = 0; i < 16; i++) { const mid = (lo + hi) / 2; if (fromOklch(L, mid, h)) lo = mid; else hi = mid; }
|
|
69
|
+
return fromOklch(L, lo, h) ?? [L, L, L];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `fg` moved in OKLCH lightness until it reads at `ratio` on `ground`; unchanged when it already does. */
|
|
73
|
+
export function readable(fg: string, ground: string, ratio = 4.5): string {
|
|
74
|
+
const f = parseHex(fg), g = parseHex(ground);
|
|
75
|
+
if (!f || !g || ratioOf(f.rgb, g.rgb) >= ratio) return fg;
|
|
76
|
+
const [L, C, h] = toOklch(f.rgb);
|
|
77
|
+
const darken = lum(g.rgb) > 0.18;
|
|
78
|
+
// lightness is monotonic in the ratio on either side of the ground: bisect between the colour and black or white
|
|
79
|
+
let near = L, far = darken ? 0 : 1;
|
|
80
|
+
for (let i = 0; i < 24; i++) {
|
|
81
|
+
const mid = (near + far) / 2;
|
|
82
|
+
if (ratioOf(inGamut(mid, C, h), g.rgb) >= ratio) far = mid; else near = mid;
|
|
83
|
+
}
|
|
84
|
+
let out = toHex(inGamut(far, C, h));
|
|
85
|
+
// 8-bit rounding can land a hair under; step until it clears
|
|
86
|
+
for (let i = 0; i < 8 && ratioOf(parseHex(out)!.rgb, g.rgb) < ratio; i++) {
|
|
87
|
+
far += darken ? -0.005 : 0.005;
|
|
88
|
+
out = toHex(inGamut(far, C, h));
|
|
89
|
+
}
|
|
90
|
+
return out + f.alpha;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const COLOR = /(^|;)(\s*)(color|--shiki-[a-zA-Z0-9]+)(\s*:\s*)(#[0-9a-fA-F]{3,8})\b/g;
|
|
94
|
+
|
|
95
|
+
export function readableCode({ ratio = 4.5, grounds = {} }: ReadableCodeOptions = {}) {
|
|
96
|
+
const ground = (key: string, pre: string | undefined) => grounds[key] ?? (key === 'color' ? pre : GROUNDS[key]);
|
|
97
|
+
const fix = (style: string, pre: string | undefined) =>
|
|
98
|
+
style.replace(COLOR, (m, sep: string, sp: string, prop: string, colon: string, hex: string) => {
|
|
99
|
+
const g = ground(prop === 'color' ? 'color' : prop.slice('--shiki-'.length), pre);
|
|
100
|
+
return g ? `${sep}${sp}${prop}${colon}${readable(hex, g, ratio)}` : m;
|
|
101
|
+
});
|
|
102
|
+
const walk = (node: HastNode, pre: string | undefined) => {
|
|
103
|
+
const style = node.properties?.['style'];
|
|
104
|
+
if (typeof style === 'string') node.properties!['style'] = fix(style, pre);
|
|
105
|
+
node.children?.forEach((c) => walk(c, pre));
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
name: 'moonarc:readable-code',
|
|
109
|
+
root(root: HastNode) {
|
|
110
|
+
const find = (n: HastNode): HastNode | undefined => (n.tagName === 'pre' ? n : n.children?.map(find).find(Boolean));
|
|
111
|
+
const style = find(root)?.properties?.['style'];
|
|
112
|
+
const pre = typeof style === 'string' ? /background-color\s*:\s*(#[0-9a-fA-F]{3,8})/.exec(style)?.[1] : undefined;
|
|
113
|
+
walk(root, pre);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|