Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/svelte.dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@supabase/supabase-js": "^2.43.4",
"@sveltejs/adapter-vercel": "^5.4.3",
"@sveltejs/enhanced-img": "^0.3.4",
"@sveltejs/kit": "^2.6.3",
"@sveltejs/kit": "^2.7.0",
"@sveltejs/site-kit": "workspace:*",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.6",
"@types/cookie": "^0.6.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/site-kit/src/lib/components/Text.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
margin-top: 5rem;
}

code,
code:not(pre *),
kbd {
white-space: pre-wrap;
padding: 0.2rem 0.4rem;
Expand Down
116 changes: 80 additions & 36 deletions packages/site-kit/src/lib/docs/Tooltip.svelte
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
<script lang="ts">
import { tick } from 'svelte';
import Text from '../components/Text.svelte';

let {
html = '',
x = 0,
y = 0,
onmouseenter,
onmouseleave
}: {
html?: string;
x?: number;
y?: number;
onmouseenter?: (event: any) => void;
onmouseleave?: (event: any) => void;
} = $props();

let width = $state(1);
let tooltip = $state() as HTMLDivElement | undefined;
interface Props {
html: string;
x: number;
y: number;
onmouseenter: (event: any) => void;
onmouseleave: (event: any) => void;
}

let { html, x, y, onmouseenter, onmouseleave }: Props = $props();

let visible = $state(false);
let tooltip: HTMLDivElement;
let offset = $state(0);

// container starts out at maxium size, then shrinks to prevent page scrolling to the right
let width = $state('calc(100vw - 2 * var(--sk-page-padding-side))');

// bit of a gross hack but it works — this prevents the
// tooltip from disappearing off the side of the screen
$effect(() => {
if (html && tooltip) {
tick().then(() => {
width = tooltip!.getBoundingClientRect().width;
});
}
(async () => {
// first, measure the window with the tooltip hidden
const window_width = window.innerWidth;

// then, display the tooltip
visible = true;
await tick();

// then, figure out how much padding we need
const container_width = parseFloat(getComputedStyle(tooltip.parentElement!).width);
const padding = (window_width - container_width) / 2;

// then, calculate the necessary offset to ensure the
// right edge of the tooltip is inside the padding
const rect = tooltip.getBoundingClientRect();
offset = Math.min(window_width - padding - rect.right, -20);

width = rect.width + 'px';
})();
});
</script>

Expand All @@ -34,35 +49,63 @@
{onmouseleave}
role="tooltip"
class="tooltip-container"
class:visible
style:width
style:left="{x}px"
style:top="{y}px"
style:--offset="{Math.min(-10, window.innerWidth - (x + width + 10))}px"
style:--offset="{offset}px"
>
<div bind:this={tooltip} class="tooltip">
<span>{@html html}</span>
<Text>
<span>{@html html}</span>
</Text>
</div>
</div>

<style>
.tooltip-container {
--bg: var(--sk-theme-2);
--arrow-size: 0.4rem;
--bg: var(--sk-back-2);
--arrow-size: 0.6rem;
display: none;
position: absolute;
transform: translate(var(--offset), calc(2rem + var(--arrow-size)));
z-index: 2;
filter: var(--sk-shadow);

&.visible {
display: block;
}
}

.tooltip {
margin: 0 2rem 0 0;
background-color: var(--bg);
color: #fff;
text-align: left;
padding: 0.4rem 0.6rem;
padding: 1.6rem;
border-radius: var(--sk-border-radius);
font-family: var(--sk-font-family-mono);
font-size: 1.2rem;
white-space: pre-wrap;
z-index: 100;
filter: drop-shadow(2px 4px 6px #67677866);
font: var(--sk-font-body-small);
display: inline-block;
max-width: min(var(--sk-page-content-width), calc(100vw - 2 * var(--sk-page-padding-side)));
max-height: 30rem;
overflow-y: auto;

:global {
p,
ol,
ul {
font: var(--sk-font-body-small);
}

.tags {
display: grid;
grid-template-columns: 8rem 1fr;
align-items: baseline;

.tag,
.param {
font: var(--sk-font-mono);
}
}
}
}

.tooltip::after {
Expand All @@ -74,8 +117,9 @@
border-bottom-color: var(--bg);
}

.tooltip :global(a) {
color: white;
text-decoration: underline;
span {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>
93 changes: 62 additions & 31 deletions packages/site-kit/src/lib/docs/hover.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,87 @@
import { mount, onMount, unmount } from 'svelte';
import Tooltip from './Tooltip.svelte';

const CLASSNAME = 'highlight';

export function setupDocsHovers() {
onMount(() => {
let tooltip: any;
let hovered: HTMLSpanElement | null = null;
let timeout: NodeJS.Timeout;

function clear() {
if (!tooltip) return;

unmount(tooltip);
hovered?.classList.remove(CLASSNAME);
tooltip = hovered = null;
}

function over(event: MouseEvent) {
const target = event.target as HTMLElement;
if (event.buttons !== 0) return; // probably selecting

let target = event.target as HTMLSpanElement;

if (!target.classList?.contains('twoslash-hover')) {
return;
}

clearTimeout(timeout);

if (target.classList?.contains('twoslash-hover')) {
clearTimeout(timeout);
if (target === hovered) return;

if (tooltip) {
unmount(tooltip);
tooltip = null;
clear();

const container = target.querySelector('.twoslash-popup-container')!;

const code = container.querySelector('.twoslash-popup-code pre code');
if (code && code.children.length === 2) {
// for reasons I don't really understand, generated types are duplicated.
// this is the easiest way to fix it
const [a, b] = code.children;
if (a.outerHTML === b.outerHTML) {
b.remove();
}
}

const rect = target?.getBoundingClientRect();
const html = target?.innerHTML;
const html = container.innerHTML;

if (html) {
const rect = target.getBoundingClientRect();
const x = (rect.left + rect.right) / 2 + window.scrollX;
const y = rect.top + window.scrollY;

if (html) {
tooltip = mount(Tooltip, {
target: document.body,
props: {
html,
x,
y,
onmouseenter: () => {
clearTimeout(timeout);
},
onmouseleave: () => {
clearTimeout(timeout);
unmount(tooltip);
tooltip = null;
}
tooltip = mount(Tooltip, {
target: document.body,
props: {
html,
x,
y,
onmouseenter: () => {
clearTimeout(timeout);
},
onmouseleave: () => {
clearTimeout(timeout);
timeout = setTimeout(clear, 0);
}
});
}
}
});

hovered = target;
hovered.classList.add(CLASSNAME);
}
}

function out(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target.classList?.contains('twoslash-hover')) {
timeout = setTimeout(() => {
unmount(tooltip);
tooltip = null;
}, 200);
let target = event.target as HTMLElement | null;

while (target) {
if (target.classList.contains('twoslash-hover')) {
timeout = setTimeout(clear, 0);
return;
}

target = target.parentElement;
}
}

Expand Down
65 changes: 63 additions & 2 deletions packages/site-kit/src/lib/markdown/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHash, Hash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
import * as marked from 'marked';
import { codeToHtml, createCssVariablesTheme } from 'shiki';
import { transformerTwoslash } from '@shikijs/twoslash';
import { SHIKI_LANGUAGE_MAP, slugify, smart_quotes, transform } from './utils';
Expand Down Expand Up @@ -185,11 +186,11 @@ const snippets = await create_snippet_cache();
export async function render_content_markdown(
filename: string,
body: string,
options: { check?: boolean },
options?: { check?: boolean },
twoslashBanner?: TwoslashBanner
) {
const headings: string[] = [];
const { check = true } = options;
const { check = true } = options ?? {};

return await transform(body, {
async walkTokens(token) {
Expand Down Expand Up @@ -675,6 +676,66 @@ async function syntax_highlight({
});

html = html.replace(/ {27,}/g, () => redactions.shift()!);

if (check) {
// munge the twoslash output so that it renders sensibly. the order of operations
// here is important — we need to work backwards, to avoid corrupting the offsets
const replacements: Array<{ start: number; end: number; content: string }> = [];

for (const match of html.matchAll(/<div class="twoslash-popup-docs">([^]+?)<\/div>/g)) {
const content = await render_content_markdown('<twoslash>', match[1], { check: false });

replacements.push({
start: match.index,
end: match.index + match[0].length,
content: '<div class="twoslash-popup-docs">' + content + '</div>'
});
}

while (replacements.length > 0) {
const { start, end, content } = replacements.pop()!;
html = html.slice(0, start) + content + html.slice(end);
}

for (const match of html.matchAll(
/<span class="twoslash-popup-docs-tag"><span class="twoslash-popup-docs-tag-name">([^]+?)<\/span><span class="twoslash-popup-docs-tag-value">([^]+?)<\/span><\/span>/g
)) {
const tag = match[1];
let value = match[2];

let content = `<span class="tag">${tag}</span><span class="value">`;

if (tag === '@param' || tag === '@throws') {
const words = value.split(' ');
let param = words.shift()!;
value = words.join(' ');

if (tag === '@throws') {
if (param[0] !== '{' || param[param.length - 1] !== '}') {
throw new Error('TODO robustify @throws handling');
}

param = param.slice(1, -1);
}

content += `<span class="param">${param}</span> `;
}

content += marked.parseInline(value);
content += '</span>';

replacements.push({
start: match.index,
end: match.index + match[0].length,
content: '<div class="tags">' + content + '</div>'
});
}

while (replacements.length > 0) {
const { start, end, content } = replacements.pop()!;
html = html.slice(0, start) + content + html.slice(end);
}
}
} catch (e) {
console.error((e as Error).message);
console.warn(prelude + redacted);
Expand Down
2 changes: 1 addition & 1 deletion packages/site-kit/src/lib/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
--sk-text-4: hsl(0, 0%, 45%);
--sk-text-translucent: hsla(0, 0%, 100%, 0.9);
--sk-scrollbar: rgba(255, 255, 255, 0.3);
--sk-shadow: drop-shadow(0px 0px 0 1px var(--sk-back-4));
--sk-shadow: drop-shadow(1px 2px 16px rgb(0 0 0 / 0.5));

--sk-theme-1-variant: hsl(15, 100%, 40%);
--sk-theme-2-variant: hsl(240, 8%, 35%);
Expand Down
Loading
Loading