Skip to content
Open
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
4 changes: 3 additions & 1 deletion docs/src/lib/components/DocsMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { sortFunc } from '@layerstack/utils';
import { cls } from '@layerstack/tailwind';

import LucideBot from '~icons/lucide/bot';
import LucideCompass from '~icons/lucide/compass';
import LucideGalleryVertical from '~icons/lucide/gallery-vertical';
import LucideGalleryHorizontalEnd from '~icons/lucide/gallery-horizontal-end';
Expand All @@ -28,7 +29,8 @@
{ name: 'Simplified charts', path: 'simplified-charts' },
{ name: 'Scales', path: 'scales' },
{ name: 'State', path: 'state' },
{ name: 'Styles', path: 'styles' }
{ name: 'Styles', path: 'styles' },
{ name: 'LLMs', path: 'LLMs' }
];

const componentsByCategory = flatGroup(allComponents, (d) => d.category?.toLowerCase())
Expand Down
156 changes: 156 additions & 0 deletions docs/src/lib/components/OpenWithButton.svelte
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cycle4passion let's call this OpenWithButton.svelte instead of OpenLLMs.svelte and update the applicable imports / usage

Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<script lang="ts">
import { page } from '$app/state';
import { fly } from 'svelte/transition';
import { Button, Dialog, Toggle, ButtonGroup, Icon, MenuItem, Menu } from 'svelte-ux';

import ChevronDownIcon from '~icons/lucide/chevron-down';
import SimpleIconsOpenai from '~icons/simple-icons/openai';
import SimpleIconsClaude from '~icons/simple-icons/claude';
import LucideCopyIcon from '~icons/lucide/copy';
import LucideCodeIcon from '~icons/lucide/code';
import SimpleIconsMarkdown from '~icons/simple-icons/markdown';
import LucideGithub from '~icons/lucide/github';
import Code from './Code.svelte';

let { metadata = {}, example = false } = $props();
// svelte-ignore state_referenced_locally
let isOpen = $state(example);
let openSourceModal = $state(false);
let showButtonCopied = $state(false);
const pkg = {
name: 'LayerChart',
url: 'https://layerchart.com/docs',
description: 'A charting/visualization library for Svelte 5'
};
const pageName = page.url.href.split('/').pop();
const llmBaseContext = `The following is a documentation page from ${pkg.name} (${pkg.description}). The page URL for "${pageName}" is ${page.url.href}. Be ready to help answer questions about this page.`;

const llms = $state([
{
label: 'View Page Markdown',
icon: SimpleIconsMarkdown,
fn: () => window.open(`${page.url.href}/llms.txt`, '_self')
},
{
lineBreakBefore: true,
label: 'Open in Claude',
icon: SimpleIconsClaude,
fn: () => window.open(generateLlmUrl('https://claude.ai/new?q='), '_blank')
},
{
label: 'Open in ChatGPT',
icon: SimpleIconsOpenai,
fn: () => {
window.open(generateLlmUrl('https://chatgpt.com/?q='), '_blank');
}
}
]);

// Add source button if component page
// svelte-ignore state_referenced_locally
if (metadata?.source || example) {
llms.unshift({
label: 'View Component Source',
icon: LucideCodeIcon,
fn: () => {
// show menu item, but do not open source modal when it's an example page
if (!example) openSourceModal = true;
}
});
}

function copy(text: string) {
navigator.clipboard.writeText(text);
showButtonCopied = true;
setTimeout(() => {
showButtonCopied = false;
}, 2000);
}

function generateLlmUrl(url: string): string {
return `${url}${encodeURIComponent(llmBaseContext)}`;
}
</script>

<ButtonGroup variant="fill-light" size="sm" color="primary" class={example ? 'mb-40 mt-4' : ''}>
<Button
icon={LucideCopyIcon}
variant="fill-light"
size="sm"
color="primary"
onclick={async () => {
const md = await fetch(`${page.url.href}/llms.txt`).then((res) => res.text());
copy(md);
}}
>
<span class="overflow-hidden relative inline-block" style="width: 70px; height: 1.2em;">
{#key showButtonCopied}
<span
in:fly={{ y: showButtonCopied ? 20 : -20, duration: 500 }}
out:fly={{ y: showButtonCopied ? -20 : 20, duration: 500 }}
class="absolute inset-0 flex items-center justify-center ml-1"
>
{showButtonCopied ? 'Copied!' : 'Copy Page'}
</span>
{/key}
</span>
</Button>
<Toggle bind:on={isOpen} let:on={open} let:toggle let:toggleOff>
<Button on:click={toggle}>
<span style="transition: transform 300ms ease; transform: rotate({open ? -180 : 0}deg);">
<ChevronDownIcon />
</span>
<Menu {open} on:close={toggleOff} placement="bottom-start" class="z-25">
{#each llms as llm (llm.label)}
{#if llm.lineBreakBefore}
<hr class="my-1" />
{/if}
<MenuItem
onclick={() => {
toggleOff();
llm.fn?.();
}}
>
<Icon data={llm.icon} />
{llm.label}
</MenuItem>
{/each}
</Menu>
</Button>
</Toggle>
</ButtonGroup>
<Dialog
bind:open={openSourceModal}
on:close={() => (openSourceModal = false)}
class="max-h-[98dvh] md:max-h-[90dvh] max-w-[98vw] md:max-w-[90vw] grid grid-rows-[auto_1fr_auto]"
>
<div class="grid grid-cols-[1fr_auto] gap-3 items-center p-4">
<div class="overflow-auto">
<div class="text-lg font-semibold">Source</div>
<div class="text-xs text-surface-content/50 truncate">{metadata?.sourceUrl}</div>
</div>

{#if metadata?.sourceUrl}
<Button
icon={LucideGithub}
variant="fill-light"
color="primary"
href={metadata.sourceUrl}
target="_blank"
>
View on Github
</Button>
{/if}
</div>

<div class="overflow-auto border-t">
<Code
source={metadata?.source}
language={metadata?.source?.startsWith('<script') ? 'svelte' : 'js'}
/>
</div>

<div slot="actions">
<Button variant="fill" color="primary">Close</Button>
</div>
</Dialog>
2 changes: 1 addition & 1 deletion docs/src/lib/markdown/components/Note.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

<div
class={cls(
'border border-l-[6px] px-4 py-2 my-4 rounded-sm flex items-center gap-2 text-sm',
'border border-l-[6px] px-4 py-2 my-4 rounded-sm flex items-center gap-4 text-sm',
'bg-(--color)/10 border-(--color)/50',
'[*&>p]:my-2',
className
Expand Down
108 changes: 108 additions & 0 deletions docs/src/lib/markdown/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,111 @@ export async function loadExamplesFromMarkdown(

return examples;
}

/**
* Process content only outside of code blocks (``` ... ```)
* Preserves code block content unchanged while applying transformations elsewhere
*/
function processOutsideCodeBlocks(content: string, processor: (text: string) => string): string {
// Split by code blocks, preserving the delimiters
const parts = content.split(/(```[\s\S]*?```)/g);

return parts
.map((part, index) => {
// Odd indices are code blocks (matched by the capture group)
if (index % 2 === 1) {
return part; // Keep code blocks unchanged
}
return processor(part); // Process non-code-block content
})
.join('');
}

// Process markdown content for LLMs by removing custom syntax and converting to vanilla markdown
export function processMarkdownContent(content: string): string {
// Remove frontmatter (YAML between --- markers at start of file)
content = content.replace(/^---\n[\s\S]*?\n---\n*/, '');

// Remove Svelte script blocks and components ONLY outside of code blocks
content = processOutsideCodeBlocks(content, (text) => {
// Remove Svelte script blocks
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>\n*/g, '');
// Remove Svelte components (self-closing and with content)
text = text.replace(/<[A-Z][a-zA-Z]*[^>]*\/>\n*/g, '');
text = text.replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>\n*/g, '');
return text;
});

// Extract title from code blocks and add as "File:" line before (must run before tabs processing)
content = content.replace(/(```\w*)\s+([^\n]*title="[^"]+")[^\n]*$/gm, (_, lang, meta) => {
const titleMatch = meta.match(/title="([^"]+)"/);
if (titleMatch) {
return `File: ${titleMatch[1]} ${lang}`;
}
return lang;
});

// Process tabs - convert to table
content = content.replace(
/:::tabs\{key="([^"]+)"\}\s*([\s\S]*?)(?=\n:::(?:\s*$|\s*\n))\n:::/gm,
(_, key, tabsContent) => {
const tabs: { label: string; content: string }[] = [];
const tabRegex = /::tab\{label="([^"]+)"[^}]*\}\s*([\s\S]*?)\s*(?=\n\s*::(?:\s*$|\s+))\n\s*::/gm;
let match;
while ((match = tabRegex.exec(tabsContent)) !== null) {
tabs.push({ label: match[1], content: match[2].trim() });
}

if (tabs.length === 0) return '';

// Build table with capitalized key as header
const header = key.charAt(0).toUpperCase() + key.slice(1);
let table = `| ${header} | Details |\n|-----------|---------|`;
for (const tab of tabs) {
// Clean up content: remove :button syntax, convert to links, unwrap code blocks
const cleanContent = tab.content
.replace(/:button\{label="([^"]+)"\s+href="([^"]+)"[^}]*\}/g, '[$1]($2)')
.replace(/```\w*\n([\s\S]*?)```/g, '$1') // Remove code block fences, keep content
.replace(/\n/g, ' ')
.trim();
table += `\n| ${tab.label} | ${cleanContent} |`;
}
return table;
}
);

// Convert ::note/:::note, ::tip/:::tip, etc. to blockquote (2 or 3 colons)
content = content.replace(
/:{2,3}(note|tip|warning|caution)\s*([\s\S]*?)(?=\n:{2,3}(?:\s*$|\s*\n))\n:{2,3}/gm,
(_, variant, noteContent) => {
return `> ${variant}: ${noteContent.trim()}\n`;
}
);

// Convert ::steps to numbered list (convert ## headings to numbered items)
content = content.replace(/::steps\s*([\s\S]*?)(?=\n::(?:\s*$|\s*\n))\n::/gm, (_, stepsContent: string) => {
let stepNum = 0;
return stepsContent.replace(/^## (.+)$/gm, (_match: string, heading: string) => {
stepNum++;
return `**${stepNum}. ${heading}**`;
});
});

// Remove any remaining standalone ::
content = content.replace(/^::\s*$/gm, '');

// Remove :icon syntax, keep text if in brackets, otherwise just remove icon
content = content.replace(/\[:icon\{[^}]+\}\s*([^\]]+)\]/g, '$1');
content = content.replace(/:icon\{[^}]+\}\s*/g, '');

// Convert :example to reference link
content = content.replace(
/:example\{component="([^"]+)"\s+name="([^"]+)"[^}]*\}/g,
'See example: [$1/$2](https://layerchart.com/docs/components/$1/$2)'
);

// Clean up multiple blank lines
content = content.replace(/\n{3,}/g, '\n\n');

return content.trim();
}
12 changes: 2 additions & 10 deletions docs/src/routes/docs/components/[name]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import { getSettings } from 'layerchart';
import { Button, Menu, Switch, Toggle, ToggleGroup, ToggleOption, Tooltip } from 'svelte-ux';
import { toTitleCase } from '@layerstack/utils';
import OpenWithButton from '$lib/components/OpenWithButton.svelte';

import ViewSourceButton from '$lib/components/ViewSourceButton.svelte';
import { examples } from '$lib/context.js';
import { loadExample } from '$lib/examples.js';
import { page } from '$app/state';

import LucideSettings from '~icons/lucide/settings';
import LucideCode from '~icons/lucide/code';
import LucideChevronLeft from '~icons/lucide/chevron-left';
import LucideChevronRight from '~icons/lucide/chevron-right';

Expand Down Expand Up @@ -129,14 +128,7 @@
<div class="text-sm text-surface-content/70">{metadata.description}</div>

<div class="flex gap-2 mt-3">
{#if 'source' in metadata}
<ViewSourceButton
label="Source"
source={metadata.source}
href={metadata.sourceUrl}
icon={LucideCode}
/>
{/if}
<OpenWithButton {metadata} />

<!-- <ViewSourceButton
label="Page source"
Expand Down
5 changes: 5 additions & 0 deletions docs/src/routes/docs/components/[name]/[example]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import LucideLink from '~icons/lucide/link';
import ComponentLink from '$lib/components/ComponentLink.svelte';
import OpenWithButton from '$lib/components/OpenWithButton.svelte';

let { data } = $props();

Expand All @@ -17,6 +18,10 @@
// console.log({ exampleInfo, data });
</script>

<div class="mb-4">
<OpenWithButton />
</div>

<Example name={example} {component} showCode />

<H2>Component Docs</H2>
Expand Down
Loading
Loading