From 288811271f048b4b5b97ebe2841614fc2ae4cf3b Mon Sep 17 00:00:00 2001 From: KTibow Date: Thu, 12 Dec 2024 17:50:29 -0800 Subject: [PATCH] doc update: color and guide --- .prettierignore | 10 +- scripts/generate-guide.ts | 522 ++++++++++++ scripts/generate-llms.ts | 397 --------- src/app.css | 28 +- src/routes/docs/Base.svelte | 5 +- src/routes/docs/Snippet.svelte | 38 +- .../docs/detailed-walkthrough/+page.svelte | 19 +- src/routes/docs/guide/+page.svelte | 758 ++++++++++++++++++ src/routes/docs/quick-start/+page.svelte | 9 +- static/llms.txt | 258 +++--- 10 files changed, 1464 insertions(+), 580 deletions(-) create mode 100644 scripts/generate-guide.ts delete mode 100644 scripts/generate-llms.ts create mode 100644 src/routes/docs/guide/+page.svelte diff --git a/.prettierignore b/.prettierignore index 3897265..8cd0692 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,13 +1,7 @@ .DS_Store node_modules +pnpm-lock.yaml /build /.svelte-kit /package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock +/src/routes/docs/guide diff --git a/scripts/generate-guide.ts b/scripts/generate-guide.ts new file mode 100644 index 0000000..e40fc77 --- /dev/null +++ b/scripts/generate-guide.ts @@ -0,0 +1,522 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import glob from "fast-glob"; + +// Helper to read a file and extract its props +function extractProps(content: string, file: string): string[] { + const props: string[] = []; + const exportMatches = content.matchAll( + /export\s+let\s+(\w+)(?::\s*([^=\n]+))?(?:\s*=\s*([^;\n]+))?/g, + ); + let anyTypeCount = 0; + for (const match of exportMatches) { + const [, name, type = "", defaultValue] = match; + let inferredType = type.trim(); + if (!inferredType && defaultValue) { + if (defaultValue === "false" || defaultValue === "true") { + inferredType = "boolean"; + } else if (!isNaN(Number(defaultValue))) { + inferredType = "number"; + } else if (defaultValue.startsWith('"') || defaultValue.startsWith("'")) { + inferredType = "string"; + } + } + if (!inferredType) { + console.warn(`⚠️ Warning: No type found for prop '${name}' in ${file}`); + anyTypeCount++; + } + const prop = `${name}: ${inferredType || "any"}`; + props.push(prop); + } + if (anyTypeCount > 0) { + console.warn(`⚠️ Found ${anyTypeCount} props with implicit 'any' type in ${file}`); + } + return props; +} + +// Helper to format props for documentation +function formatProps(props: string[]): string { + if (props.length === 0) return ""; + return ( + "- Props:\n" + + props + .map((prop) => { + const [name, type] = prop.split(":").map((s) => s.trim()); + return ` - ${name.replace("?", "")}: ${type.replace(";", "")}`; + }) + .join("\n") + ); +} + +// Helper to extract slots from a component file +function extractSlots(content: string): string[] { + const slots: string[] = []; + const slotMatches = content.matchAll(//g); + for (const match of slotMatches) { + const slotName = match[1] || "default"; + if (!slots.includes(slotName)) { + slots.push(slotName); + } + } + return slots; +} + +// Data structures +const components = { + buttons: ["Button", "ButtonLink", "SegmentedButtonContainer", "SegmentedButtonItem", "FAB"], + containers: [ + "Card", + "CardClickable", + "Dialog", + "Snackbar", + "SnackbarAnim", + "SnackbarItem", + "BottomSheet", + "ListItem", + "ListItemButton", + "ListItemLabel", + "Menu", + "MenuItem", + ], + progress: [ + "LinearProgress", + "LinearProgressIndeterminate", + "CircularProgress", + "CircularProgressIndeterminate", + ], + inputs: [ + "RadioAnim1", + "RadioAnim2", + "RadioAnim3", + "Checkbox", + "CheckboxAnim", + "Switch", + "Slider", + "SliderTicks", + ], + textfields: [ + "TextField", + "TextFieldMultiline", + "TextFieldOutlined", + "TextFieldOutlinedMultiline", + ], + pickers: ["DatePickerDocked"], + chips: ["Chip"], + nav: [ + "NavDrawer", + "NavDrawerButton", + "NavDrawerLink", + "NavList", + "NavListButton", + "NavListLink", + "Tabs", + "TabsLink", + "VariableTabs", + "VariableTabsLink", + ], + utils: ["ChipChooser", "Divider", "DateField"], + misc: ["StyleFromScheme", "Icon", "Layer"], +}; + +const examples = { + Button: ``, + Card: `Hello +Click me`, + Dialog: ` + Delete these items? + + + + +`, + Snackbar: ` + +`, + TextField: ``, + DateField: ``, + FAB: ``, + Menu: ` + Undo + Redo +`, + SegmentedButtonContainer: ` + + Tab A + + Tab B +`, + RadioAnim1: ` + +`, + Switch: ``, + Slider: ` +`, + Tabs: ``, +}; + +const tips = { + Snackbar: + "Set the CSS variable `--m3-util-bottom-offset` to a size to move all snackbars upwards.", + TextField: + "For outlined text fields on a surface that isn't the default background, set the CSS variable `--m3-util-background` to the current background make the text field fit in.", + DateField: "This component is like DatePickerDocked but it has a field and it's easier to use", +}; + +const componentDocs = { + Button: { + types: ["elevated", "filled", "tonal", "outlined", "text"], + iconTypes: ["none", "left", "full"], + }, + Card: { + types: ["elevated", "filled", "outlined"], + }, + TextField: { + variants: [ + { name: "TextField", component: "TextField" }, + { name: "Multiline", component: "TextFieldMultiline" }, + { name: "Outlined", component: "TextFieldOutlined" }, + { name: "Outlined Multiline", component: "TextFieldOutlinedMultiline" }, + ], + }, + RadioAnim1: { + variants: [ + { name: "Style 1", component: "RadioAnim1" }, + { name: "Style 2", component: "RadioAnim2" }, + { name: "Style 3", component: "RadioAnim3" }, + ], + }, + Slider: { + variants: [ + { name: "Continuous", component: "Slider" }, + { name: "With Ticks", component: "SliderTicks" }, + ], + }, +}; + +// Extracts all component data +async function extractComponentData() { + const componentFiles = await glob("src/lib/**/*.svelte", { + ignore: ["**/+*.svelte", "**/forms/_picker/**"], + }); + + // Check for files that should exist but don't + for (const [, items] of Object.entries(components)) { + for (const item of items) { + const searchName = item === "Icon" ? "_icon" : item; + const matchingFiles = componentFiles.filter((f) => f.includes(`/${searchName}.svelte`)); + if (matchingFiles.length === 0) { + console.warn(`⚠️ Warning: Listed component '${item}' not found in filesystem`); + } else if (matchingFiles.length > 1) { + console.warn(`⚠️ Warning: Multiple files found for component '${item}':`, matchingFiles); + } + } + } + + const componentData = []; + + for (const file of componentFiles) { + const content = readFileSync(file, "utf-8"); + const componentName = file.split("/").pop()?.replace(".svelte", ""); + + if (!componentName || componentName.startsWith("_")) continue; + + const props = extractProps(content, file); + const slots = extractSlots(content); + const specs = componentDocs[componentName]; + + // Skip variants that will be documented under main component + if ( + specs?.variants?.some((v) => v.component !== componentName && v.component === componentName) + ) { + continue; + } + + let isVariant = false; + for (const [mainComponent, mainSpecs] of Object.entries(componentDocs)) { + if ( + mainSpecs.variants?.some( + (v) => v.component === componentName && mainComponent !== componentName, + ) + ) { + isVariant = true; + break; + } + } + if (isVariant) continue; + + const displayName = specs?.variants?.some((v) => v.component === componentName) + ? componentName.replace(/[123]$/, "") + : componentName; + + let variantProps = {}; + if (specs?.variants) { + for (const variant of specs.variants) { + const variantFiles = await glob(`src/lib/**/${variant.component}.svelte`); + if (variantFiles.length === 0) { + console.warn(`⚠️ Warning: Variant component '${variant.component}' not found`); + continue; + } + const variantContent = readFileSync(variantFiles[0], "utf-8"); + variantProps[variant.name] = extractProps(variantContent, variantFiles[0]); + } + } + + componentData.push({ + name: componentName, + displayName, + props, + slots, + specs, + variantProps, + example: examples[componentName], + tip: tips[componentName], + }); + } + + return componentData; +} + +// Generates documentation from component data +function formatDocumentation(componentData) { + let doc = `# M3 Svelte\n\n## Importing components\n\n\`\`\`svelte\nimport { ComponentName } from "m3-svelte";\n\`\`\`\n\n## Available components\n\n`; + + for (const [category, items] of Object.entries(components)) { + doc += `**${category.charAt(0).toUpperCase() + category.slice(1)}**\n`; + for (const item of items) { + doc += `- ${item}\n`; + } + doc += "\n"; + } + + doc += "## Components\n\n"; + + for (const component of componentData) { + const { displayName, props, slots, specs, variantProps, example, tip } = component; + + doc += `### ${displayName}\n`; + + if (specs) { + if (specs.types) { + doc += `- Types: ${specs.types.join(", ")}\n`; + } + if (specs.iconTypes) { + doc += `- Optional icon: ${specs.iconTypes.join(", ")}\n`; + } + if (specs.variants) { + doc += `- Variants: ${specs.variants.map((v) => `${v.name} (${v.component})`).join(", ")}\n`; + + const baseProps = new Set(variantProps[specs.variants[0].name]); + const differences = specs.variants.slice(1).some((v) => { + const varProps = new Set(variantProps[v.name]); + return ( + ![...baseProps].every((p) => varProps.has(p)) || + ![...varProps].every((p) => baseProps.has(p)) + ); + }); + + if (differences) { + doc += "- Variant-specific props:\n"; + for (const variant of specs.variants) { + const varProps = variantProps[variant.name]; + if (varProps?.length) { + doc += ` ${variant.name}:\n${formatProps(varProps) + .split("\n") + .map((l) => " " + l) + .join("\n")}\n`; + } + } + } + } + } + + const formattedProps = formatProps(props); + if (formattedProps) { + doc += formattedProps + "\n"; + } + + const formattedSlots = + slots.length == 1 && slots[0] == "default" + ? `- Has a slot` + : slots.length + ? "- Slots:\n" + slots.map((slot) => ` - ${slot}`).join("\n") + : ""; + + if (formattedSlots) { + doc += formattedSlots + "\n"; + } + + if (example) { + doc += "Example:\n```svelte\n" + example + "\n```\n"; + } + + if (tip) { + doc += "Tip:\n" + tip.replace(/\{/g, "{").replace(/\}/g, "}") + "\n"; + } + doc += "\n"; + } + + return doc; +} + +// Extract all component data +const componentData = await extractComponentData(); +const doc = formatDocumentation(componentData); + +// Write raw docs +writeFileSync("static/llms.txt", doc); + +// Generate Svelte page +let sveltePage = ` + + +

+ This is an overview of M3 Svelte. This page is also available raw. +

+ +
`; + +// Combine categories with their component documentation +for (const [category, items] of Object.entries(components)) { + sveltePage += ` +
+

${category.charAt(0).toUpperCase() + category.slice(1)}

+
    `; + for (const item of items) { + sveltePage += `\n
  • ${item}
  • `; + } + sveltePage += `\n
+
`; + + // Add documentation for components in this category, maintaining the same order as the list + for (const componentName of items) { + const component = componentData.find((c) => c.displayName === componentName); + if (!component) continue; + const { displayName, props, slots, specs, example, tip } = component; + + sveltePage += `\n
+

${displayName}

`; + + if (specs?.types) { + sveltePage += `\n

Types: ${specs.types.join(", ")}

`; + } + if (specs?.iconTypes) { + sveltePage += `\n

Optional icon: ${specs.iconTypes.join(", ")}

`; + } + + if (props.length > 0) { + sveltePage += `\n

Props

\n
    `; + for (const prop of props) { + // Escape angle brackets in prop types for display + // Escape angle brackets and curly braces in prop types for display + const escapedProp = prop + .replace(//g, ">") + .replace(/\{/g, "{") + .replace(/\}/g, "}"); + sveltePage += `\n
  • ${escapedProp}
  • `; + } + sveltePage += `\n
`; + } + + if (slots.length > 0) { + sveltePage += `\n

Slots

\n
    `; + for (const slot of slots) { + sveltePage += `\n
  • ${slot}
  • `; + } + sveltePage += `\n
`; + } + + if (example) { + sveltePage += `\n

Examples

+ `; + } + + if (tip) { + sveltePage += `\n

Tips

\n

${tip.replace(/\{/g, "{").replace(/\}/g, "}")}

`; + } + + sveltePage += `\n
`; + } +} + +sveltePage += `\n
+ + +`; + +// Write Svelte page +writeFileSync("src/routes/docs/guide/+page.svelte", sveltePage); diff --git a/scripts/generate-llms.ts b/scripts/generate-llms.ts deleted file mode 100644 index 25e65f3..0000000 --- a/scripts/generate-llms.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import glob from "fast-glob"; - -// Helper to read a file and extract its props -function extractProps(content: string, file: string): string[] { - const props: string[] = []; - const exportMatches = content.matchAll( - /export\s+let\s+(\w+)(?::\s*([^=\n]+))?(?:\s*=\s*([^;\n]+))?/g, - ); - let anyTypeCount = 0; - for (const match of exportMatches) { - const [, name, type = "", defaultValue] = match; - let inferredType = type.trim(); - if (!inferredType && defaultValue) { - if (defaultValue === "false" || defaultValue === "true") { - inferredType = "boolean"; - } else if (!isNaN(Number(defaultValue))) { - inferredType = "number"; - } else if (defaultValue.startsWith('"') || defaultValue.startsWith("'")) { - inferredType = "string"; - } - } - if (!inferredType) { - console.warn(`⚠️ Warning: No type found for prop '${name}' in ${file}`); - anyTypeCount++; - } - const prop = `${name}: ${inferredType || "any"}`; - props.push(prop); - } - if (anyTypeCount > 0) { - console.warn(`⚠️ Found ${anyTypeCount} props with implicit 'any' type in ${file}`); - } - return props; -} - -// Helper to format props for documentation -function formatProps(props: string[]): string { - if (props.length === 0) return ""; - return ( - "- Props:\n" + - props - .map((prop) => { - const [name, type] = prop.split(":").map((s) => s.trim()); - return ` - ${name.replace("?", "")}: ${type.replace(";", "")}`; - }) - .join("\n") - ); -} - -async function generateDocs() { - const components = { - misc: ["StyleFromScheme", "Icon", "Layer"], // Use exported name, not filename - buttons: ["Button", "ButtonLink", "SegmentedButtonContainer", "SegmentedButtonItem", "FAB"], - containers: [ - "BottomSheet", - "Card", - "CardClickable", - "Dialog", - "ListItem", - "ListItemButton", - "ListItemLabel", - "Menu", - "MenuItem", - "Snackbar", - "SnackbarAnim", - "SnackbarItem", - ], - progress: [ - "LinearProgress", - "LinearProgressIndeterminate", - "CircularProgress", - "CircularProgressIndeterminate", - ], - inputs: [ - "RadioAnim1", - "RadioAnim2", - "RadioAnim3", - "Checkbox", - "CheckboxAnim", - "Switch", - "Slider", - "SliderTicks", - ], - textfields: [ - "TextField", - "TextFieldMultiline", - "TextFieldOutlined", - "TextFieldOutlinedMultiline", - ], - pickers: ["DatePickerDocked"], - chips: ["Chip"], - nav: [ - "NavDrawer", - "NavDrawerButton", - "NavDrawerLink", - "NavList", - "NavListButton", - "NavListLink", - "Tabs", - "TabsLink", - "VariableTabs", - "VariableTabsLink", - ], - utils: ["ChipChooser", "Divider", "DateField"], - }; - - // Check for components in index.ts that aren't categorized - const indexContent = readFileSync("src/lib/index.ts", "utf-8"); - // Match both direct exports and renamed exports - const exportedComponents = [ - ...indexContent.matchAll(/export\s+{\s*default\s+as\s+(\w+)\s*}/g), - ...indexContent.matchAll(/export\s*{\s*default\s+as\s+(\w+)\s*}\s*from/g), - ].map((match) => match[1]); - - const categorizedComponents = new Set(Object.values(components).flat()); - - const uncategorizedComponents = exportedComponents.filter( - (comp) => !categorizedComponents.has(comp), - ); - - if (uncategorizedComponents.length > 0) { - console.warn( - "⚠️ Warning: Found uncategorized components in index.ts:", - uncategorizedComponents, - ); - } - - let doc = `# M3 Svelte - -## Importing components - -\`\`\`svelte -import { ComponentName } from "m3-svelte"; -\`\`\` - -## Available components\n\n`; - - // List all components by category - for (const [category, items] of Object.entries(components)) { - doc += `**${category.charAt(0).toUpperCase() + category.slice(1)}**\n`; - for (const item of items) { - doc += `- ${item}\n`; - } - doc += "\n"; - } - - doc += "## Components\n\n"; - - // Generate detailed docs for each component - const componentFiles = await glob("src/lib/**/*.svelte", { - ignore: ["**/+*.svelte", "**/forms/_picker/**"], - }); - - // Check for files that should exist but don't - for (const [, items] of Object.entries(components)) { - for (const item of items) { - // Handle special case for Icon which is _icon.svelte - const searchName = item === "Icon" ? "_icon" : item; - const matchingFiles = componentFiles.filter((f) => f.includes(`/${searchName}.svelte`)); - if (matchingFiles.length === 0) { - console.warn(`⚠️ Warning: Listed component '${item}' not found in filesystem`); - } else if (matchingFiles.length > 1) { - console.warn(`⚠️ Warning: Multiple files found for component '${item}':`, matchingFiles); - } - } - } - - // Extract slots from a component file - function extractSlots(content: string): string[] { - const slots: string[] = []; - const slotMatches = content.matchAll(//g); - for (const match of slotMatches) { - const slotName = match[1] || "default"; - if (!slots.includes(slotName)) { - slots.push(slotName); - } - } - return slots; - } - - // Common examples and tips - const examples = { - Button: ``, - Card: `Hello -Click me`, - Dialog: ` - Delete these items? - - - - -`, - Snackbar: ` - -`, - TextField: ``, - DateField: ``, - FAB: ``, - Menu: ` - Undo - Redo -`, - SegmentedButtonContainer: ` - - Tab A - - Tab B -`, - RadioAnim1: ` - -`, - Switch: ``, - Slider: ` -`, - Tabs: ``, - }; - - // Tips for specific components - const tips = { - Snackbar: - "Set the CSS variable `--m3-util-bottom-offset` to a size to move all snackbars upwards.", - TextField: - "For outlined text fields on a surface that isn't the default background, set the CSS variable `--m3-util-background` to the current background make the text field fit in.", - DateField: "This component is like DatePickerDocked but it has a field and it's easier to use", - }; - - // Component-specific documentation - const componentDocs = { - Button: { - types: ["elevated", "filled", "tonal", "outlined", "text"], - iconTypes: ["none", "left", "full"], - }, - Card: { - types: ["elevated", "filled", "outlined"], - }, - TextField: { - variants: [ - { name: "TextField", component: "TextField" }, - { name: "Multiline", component: "TextFieldMultiline" }, - { name: "Outlined", component: "TextFieldOutlined" }, - { name: "Outlined Multiline", component: "TextFieldOutlinedMultiline" }, - ], - }, - RadioAnim1: { - variants: [ - { name: "Style 1", component: "RadioAnim1" }, - { name: "Style 2", component: "RadioAnim2" }, - { name: "Style 3", component: "RadioAnim3" }, - ], - }, - Slider: { - variants: [ - { name: "Continuous", component: "Slider" }, - { name: "With Ticks", component: "SliderTicks" }, - ], - }, - }; - - // Generate detailed docs for each component - for (const file of componentFiles) { - const content = readFileSync(file, "utf-8"); - const componentName = file.split("/").pop()?.replace(".svelte", ""); - - if (!componentName || componentName.startsWith("_")) continue; - - const props = extractProps(content, file); - const formattedProps = formatProps(props); - - // Extract and format slots - const slots = extractSlots(content); - const formattedSlots = - slots.length == 1 && slots[0] == "default" - ? `- Has a slot` - : slots.length - ? "- Slots:\n" + slots.map((slot) => ` - ${slot}`).join("\n") - : ""; - - // Skip variants as they'll be documented under their main component - const specs = componentDocs[componentName]; - if ( - specs?.variants?.some((v) => v.component !== componentName && v.component === componentName) - ) { - continue; - } - // Also skip if this component is a variant of another component - let isVariant = false; - for (const [mainComponent, mainSpecs] of Object.entries(componentDocs)) { - if ( - mainSpecs.variants?.some( - (v) => v.component === componentName && mainComponent !== componentName, - ) - ) { - isVariant = true; - break; - } - } - if (isVariant) continue; - - // For components with variants, use a cleaner name (e.g. "RadioAnim" instead of "RadioAnim1") - const displayName = specs?.variants?.some((v) => v.component === componentName) - ? componentName.replace(/[123]$/, "") - : componentName; - doc += `### ${displayName}\n`; - - // Add component-specific documentation - if (specs) { - if (specs.types) { - doc += `- Types: ${specs.types.join(", ")}\n`; - } - if (specs.iconTypes) { - doc += `- Optional icon: ${specs.iconTypes.join(", ")}\n`; - } - if (specs.variants) { - doc += `- Variants: ${specs.variants.map((v) => `${v.name} (${v.component})`).join(", ")}\n`; - - // Get props for each variant - const variantProps = {}; - for (const variant of specs.variants) { - const variantFiles = await glob(`src/lib/**/${variant.component}.svelte`); - if (variantFiles.length === 0) { - console.warn(`⚠️ Warning: Variant component '${variant.component}' not found`); - continue; - } - const variantContent = readFileSync(variantFiles[0], "utf-8"); - variantProps[variant.name] = extractProps(variantContent, variantFiles[0]); - } - - // Show prop differences if any - const baseProps = new Set(variantProps[specs.variants[0].name]); - const differences = specs.variants.slice(1).some((v) => { - const props = new Set(variantProps[v.name]); - return ( - ![...baseProps].every((p) => props.has(p)) || ![...props].every((p) => baseProps.has(p)) - ); - }); - - if (differences) { - doc += "- Variant-specific props:\n"; - for (const variant of specs.variants) { - const props = variantProps[variant.name]; - if (props?.length) { - doc += ` ${variant.name}:\n${formatProps(props) - .split("\n") - .map((l) => " " + l) - .join("\n")}\n`; - } - } - } - } - } - - // Add props and slots - if (formattedProps) { - doc += formattedProps + "\n"; - } - if (formattedSlots) { - doc += formattedSlots + "\n"; - } - - // Add example if available - if (examples[componentName]) { - doc += "Example:\n```svelte\n" + examples[componentName] + "\n```\n"; - } - - // Add tips if any - if (tips[componentName]) { - doc += "Tip:\n" + tips[componentName] + "\n"; - } - doc += "\n"; - } - - writeFileSync("static/llms.txt", doc); -} - -generateDocs(); diff --git a/src/app.css b/src/app.css index 92b52ba..a11e623 100644 --- a/src/app.css +++ b/src/app.css @@ -4,17 +4,17 @@ body { color: rgb(var(--m3-scheme-on-background)); } -@media (prefers-color-scheme: light) { - pre code.hljs { - display: block; - overflow-x: auto; - padding: 1em; - } +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} - code.hljs { - padding: 3px 5px; - } +code.hljs { + padding: 3px 5px; +} +@media (prefers-color-scheme: light) { .hljs { color: #24292e; background: #ffffff; @@ -108,16 +108,6 @@ body { } } @media (prefers-color-scheme: dark) { - pre code.hljs { - display: block; - overflow-x: auto; - padding: 1em; - } - - code.hljs { - padding: 3px 5px; - } - .hljs { color: #c9d1d9; background: #0d1117; diff --git a/src/routes/docs/Base.svelte b/src/routes/docs/Base.svelte index 17b2715..6269577 100644 --- a/src/routes/docs/Base.svelte +++ b/src/routes/docs/Base.svelte @@ -20,6 +20,7 @@ const pages = { "quick-start": "Quick start", "detailed-walkthrough": "Detailed walkthrough", + guide: "Component guide", }; const normalizePath = (path: string) => { const u = new URL(path, $page.url.href); @@ -40,10 +41,6 @@ {label} {/each} - - - Component guide -
diff --git a/src/routes/docs/Snippet.svelte b/src/routes/docs/Snippet.svelte index c0549e2..77acca8 100644 --- a/src/routes/docs/Snippet.svelte +++ b/src/routes/docs/Snippet.svelte @@ -2,12 +2,27 @@ import { Button, Snackbar, type SnackbarIn } from "$lib"; import Icon from "$lib/misc/_icon.svelte"; import iconCopy from "@ktibow/iconset-material-symbols/content-copy-outline"; + import Highlight from "svelte-highlight"; + import type { LanguageType } from "svelte-highlight/languages"; + import javascript from "svelte-highlight/languages/javascript"; + import css from "svelte-highlight/languages/css"; + import xml from "svelte-highlight/languages/xml"; export let code: string; export let name: string | undefined; + export let lang: string; let snackbar: (data: SnackbarIn) => void; + const languageType = { + javascript, + css, + xml, + }[lang]; + if (!languageType) { + throw new Error(`Language "${lang}" not supported`); + } + function copyToClipboard() { navigator.clipboard.writeText(code); snackbar({ closable: true, message: "Text copied to clipboard", timeout: 2000 }); @@ -16,7 +31,7 @@
{#if name} -

{name}

+

{name}

{/if}
-
{code}
+
diff --git a/src/routes/docs/detailed-walkthrough/+page.svelte b/src/routes/docs/detailed-walkthrough/+page.svelte index f3d4adb..8c592aa 100644 --- a/src/routes/docs/detailed-walkthrough/+page.svelte +++ b/src/routes/docs/detailed-walkthrough/+page.svelte @@ -7,7 +7,8 @@ import Snippet from "../Snippet.svelte"; let styleType = "plain"; - const componentCode1 = `${"<"}button>Click me${"<"}/button> + const componentCode1 = `${"<"}button class="bg-surface-container-low text-primary rounded-full">Click me${"<"}/button>`; + const componentCode2 = `${"<"}button>Click me${"<"}/button> ${"<"}style> button { background-color: rgb(var(--m3-scheme-surface-container-low)); @@ -16,7 +17,7 @@ ${"<"}style> border-radius: var(--m3-util-rounding-full); } ${"<"}/style>`; - const componentCode2 = `${"<"}script> + const componentCode3 = `${"<"}script> import { Button } from "m3-svelte"; ${"<"}/script> @@ -63,10 +64,7 @@ ${"<"}Button type="filled" on:click={() => alert("Hello world!")}>Click me${"<"} while still using Material 3 elements. Here's an example.

{#if styleType == "tailwind"} - Click me<${""}/button>`} - name="Component.svelte" - /> +

You'll need to update your Tailwind config too:

alert("Hello world!")}>Click me${"<"} "surface-tint": "rgb(var(--m3-scheme-surface-tint) / )" }`} name="tailwind.config.js" + lang="javascript" /> {:else} - + {/if}

You might also want to make your app match your theme. Here's what that could look like:

{#if styleType == "tailwind"} `} name="app.html" + lang="xml" /> {:else} - `} name="app.html" /> + `} name="app.html" lang="xml" /> {/if}

@@ -140,7 +141,7 @@ ${"<"}Button type="filled" on:click={() => alert("Hello world!")}>Click me${"<"}

It's usually simple to use components. For example, this is what it looks like to use a Button:

- +

There are a few ways to get more info on how to use a component.

  • Click the code button on the component on the home page
  • diff --git a/src/routes/docs/guide/+page.svelte b/src/routes/docs/guide/+page.svelte new file mode 100644 index 0000000..935030d --- /dev/null +++ b/src/routes/docs/guide/+page.svelte @@ -0,0 +1,758 @@ + + + +

    + This is an overview of M3 Svelte. This page is also available raw. +

    + +
    +
    +

    Buttons

    +
      +
    • Button
    • +
    • ButtonLink
    • +
    • SegmentedButtonContainer
    • +
    • SegmentedButtonItem
    • +
    • FAB
    • +
    +
    +
    +

    Button

    +

    Types: elevated, filled, tonal, outlined, text

    +

    Optional icon: none, left, full

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • iconType: "none" | "left" | "full"
    • +
    • type: "elevated" | "filled" | "tonal" | "outlined" | "text";
    • +
    • disabled: boolean
    • +
    +

    Slots

    +
      +
    • default
    • +
    +

    Examples

    + alert("Hello world!")}> + Click me +`} lang="xml" /> +
    +
    +

    ButtonLink

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAnchorAttributes
    • +
    • iconType: "none" | "left" | "full"
    • +
    • type: "elevated" | "filled" | "tonal" | "outlined" | "text";
    • +
    • href: string;
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    SegmentedButtonContainer

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    +

    Slots

    +
      +
    • default
    • +
    +

    Examples

    + + + Tab A + + Tab B +`} lang="xml" /> +
    +
    +

    SegmentedButtonItem

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLLabelAttributes
    • +
    • input: string;
    • +
    • icon: IconifyIcon | undefined
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    FAB

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • color: "primary" | "surface" | "secondary" | "tertiary"
    • +
    • size: "small" | "normal" | "large"
    • +
    • elevation: "normal" | "lowered" | "none"
    • +
    • icon: IconifyIcon | undefined
    • +
    • text: string | undefined
    • +
    +

    Examples

    + `} lang="xml" /> +
    +
    +

    Containers

    +
      +
    • Card
    • +
    • CardClickable
    • +
    • Dialog
    • +
    • Snackbar
    • +
    • SnackbarAnim
    • +
    • SnackbarItem
    • +
    • BottomSheet
    • +
    • ListItem
    • +
    • ListItemButton
    • +
    • ListItemLabel
    • +
    • Menu
    • +
    • MenuItem
    • +
    +
    +
    +

    Card

    +

    Types: elevated, filled, outlined

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement> & HTMLButtonAttributes
    • +
    • type: "elevated" | "filled" | "outlined";
    • +
    +

    Slots

    +
      +
    • default
    • +
    +

    Examples

    + Hello +Click me`} lang="xml" /> +
    +
    +

    CardClickable

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement> & HTMLButtonAttributes
    • +
    • type: "elevated" | "filled" | "outlined";
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    Dialog

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLDialogAttributes
    • +
    • icon: IconifyIcon | undefined
    • +
    • headline: string;
    • +
    • open: boolean;
    • +
    • closeOnEsc: boolean
    • +
    • closeOnClick: boolean
    • +
    +

    Slots

    +
      +
    • default
    • +
    • buttons
    • +
    +

    Examples

    + + Delete these items? + + + + +`} lang="xml" /> +
    +
    +

    Snackbar

    +

    Props

    +
      +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: ComponentProps<SnackbarItem>
    • +
    +

    Examples

    + +let snackbar: (data: SnackbarIn) => void; + + +`} lang="xml" /> +

    Tips

    +

    Set the CSS variable `--m3-util-bottom-offset` to a size to move all snackbars upwards.

    +
    +
    +

    SnackbarAnim

    +

    Props

    +
      +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: ComponentProps<SnackbarItem>
    • +
    +
    +
    +

    SnackbarItem

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    BottomSheet

    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    ListItem

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    • overline: string
    • +
    • headline: string
    • +
    • supporting: string
    • +
    • lines: number | undefined
    • +
    +

    Slots

    +
      +
    • leading
    • +
    • trailing
    • +
    +
    +
    +

    ListItemButton

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • overline: string
    • +
    • headline: string
    • +
    • supporting: string
    • +
    • lines: number | undefined
    • +
    +

    Slots

    +
      +
    • leading
    • +
    • trailing
    • +
    +
    +
    +

    ListItemLabel

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLLabelAttributes
    • +
    • overline: string
    • +
    • headline: string
    • +
    • supporting: string
    • +
    • lines: number | undefined
    • +
    +

    Slots

    +
      +
    • leading
    • +
    • trailing
    • +
    +
    +
    +

    Menu

    +

    Props

    +
      +
    • display: string
    • +
    +

    Slots

    +
      +
    • default
    • +
    +

    Examples

    + + Undo + Redo +`} lang="xml" /> +
    +
    +

    MenuItem

    +

    Props

    +
      +
    • icon: IconifyIcon | "space" | undefined
    • +
    • disabled: boolean
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    Progress

    +
      +
    • LinearProgress
    • +
    • LinearProgressIndeterminate
    • +
    • CircularProgress
    • +
    • CircularProgressIndeterminate
    • +
    +
    +
    +

    LinearProgress

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    • percent: number;
    • +
    +
    +
    +

    LinearProgressIndeterminate

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    +
    +
    +

    CircularProgress

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<SVGElement>
    • +
    • percent: number;
    • +
    +
    +
    +

    CircularProgressIndeterminate

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<SVGElement>
    • +
    +
    +
    +

    Inputs

    +
      +
    • RadioAnim1
    • +
    • RadioAnim2
    • +
    • RadioAnim3
    • +
    • Checkbox
    • +
    • CheckboxAnim
    • +
    • Switch
    • +
    • Slider
    • +
    • SliderTicks
    • +
    +
    +
    +

    Checkbox

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    CheckboxAnim

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    Switch

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLAttributes<HTMLDivElement>
    • +
    • checked: boolean
    • +
    • disabled: boolean
    • +
    +

    Examples

    + `} lang="xml" /> +
    +
    +

    Slider

    +

    Props

    +
      +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLInputAttributes
    • +
    • value: number;
    • +
    • min: number
    • +
    • max: number
    • +
    • step: number | "any"
    • +
    • disabled: boolean
    • +
    • showValue: boolean
    • +
    • format: (n: number)
    • +
    +

    Examples

    + +`} lang="xml" /> +
    +
    +

    Textfields

    +
      +
    • TextField
    • +
    • TextFieldMultiline
    • +
    • TextFieldOutlined
    • +
    • TextFieldOutlinedMultiline
    • +
    +
    +
    +

    TextField

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLInputAttributes
    • +
    • name: string;
    • +
    • leadingIcon: IconifyIcon | undefined
    • +
    • trailingIcon: IconifyIcon | undefined
    • +
    • disabled: boolean
    • +
    • required: boolean
    • +
    • error: boolean
    • +
    • value: string
    • +
    +

    Examples

    + `} lang="xml" /> +

    Tips

    +

    For outlined text fields on a surface that isn't the default background, set the CSS variable `--m3-util-background` to the current background make the text field fit in.

    +
    +
    +

    Pickers

    +
      +
    • DatePickerDocked
    • +
    +
    +
    +

    DatePickerDocked

    +

    Props

    +
      +
    • display: string
    • +
    • date: string
    • +
    • clearable: boolean;
    • +
    • focusedMonth: number
    • +
    • dateValidator: (date: string)
    • +
    +
    +
    +

    Chips

    +
      +
    • Chip
    • +
    +
    +
    +

    Chip

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • type: "input" | "assist" | "general";
    • +
    • icon: IconifyIcon | null
    • +
    • trailingIcon: IconifyIcon | null
    • +
    • elevated: boolean
    • +
    • disabled: boolean
    • +
    • selected: boolean
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    Nav

    +
      +
    • NavDrawer
    • +
    • NavDrawerButton
    • +
    • NavDrawerLink
    • +
    • NavList
    • +
    • NavListButton
    • +
    • NavListLink
    • +
    • Tabs
    • +
    • TabsLink
    • +
    • VariableTabs
    • +
    • VariableTabsLink
    • +
    +
    +
    +

    NavDrawer

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLElement>
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    NavDrawerButton

    +

    Props

    +
      +
    • selected: boolean;
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • icon: IconifyIcon;
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    NavDrawerLink

    +

    Props

    +
      +
    • href: string;
    • +
    • selected: boolean;
    • +
    • extraOptions: HTMLAnchorAttributes
    • +
    • icon: IconifyIcon;
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    NavList

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAttributes<HTMLElement>
    • +
    • type: "rail" | "bar" | "auto";
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    NavListButton

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLButtonAttributes
    • +
    • type: "rail" | "bar" | "auto";
    • +
    • selected: boolean;
    • +
    • icon: IconifyIcon;
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    NavListLink

    +

    Props

    +
      +
    • display: string
    • +
    • extraOptions: HTMLAnchorAttributes
    • +
    • type: "rail" | "bar" | "auto";
    • +
    • href: string;
    • +
    • selected: boolean;
    • +
    • icon: IconifyIcon;
    • +
    +

    Slots

    +
      +
    • default
    • +
    +
    +
    +

    Tabs

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLInputAttributes
    • +
    • secondary: boolean
    • +
    • tab: string;
    • +
    • items: {
    • +
    +

    Examples

    + `} lang="xml" /> +
    +
    +

    TabsLink

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLAnchorAttributes
    • +
    • secondary: boolean
    • +
    • tab: string;
    • +
    • items: {
    • +
    +
    +
    +

    VariableTabs

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLInputAttributes
    • +
    • secondary: boolean
    • +
    • tab: string;
    • +
    • items: {
    • +
    +
    +
    +

    VariableTabsLink

    +

    Props

    +
      +
    • display: string
    • +
    • extraWrapperOptions: HTMLAttributes<HTMLDivElement>
    • +
    • extraOptions: HTMLAnchorAttributes
    • +
    • secondary: boolean
    • +
    • tab: string;
    • +
    • items: {
    • +
    +
    +
    +

    Utils

    +
      +
    • ChipChooser
    • +
    • Divider
    • +
    • DateField
    • +
    +
    +
    +

    ChipChooser

    +

    Props

    +
      +
    • options: { label: string; value: string; icon?: IconifyIcon }[];
    • +
    • chosenOptions: string[]
    • +
    +
    +
    +

    Divider

    +

    Props

    +
      +
    • inset: boolean
    • +
    +
    +
    +

    DateField

    +

    Props

    +
      +
    • name: string;
    • +
    • date: string
    • +
    • required: boolean
    • +
    • disabled: boolean
    • +
    • extraOptions: HTMLInputAttributes
    • +
    +

    Examples

    + `} lang="xml" /> +

    Tips

    +

    This component is like DatePickerDocked but it has a field and it's easier to use

    +
    +
    +

    Misc

    +
      +
    • StyleFromScheme
    • +
    • Icon
    • +
    • Layer
    • +
    +
    +
    +

    StyleFromScheme

    +

    Props

    +
      +
    • lightScheme: SerializedScheme;
    • +
    • darkScheme: SerializedScheme;
    • +
    +
    +
    +

    Layer

    +
    +
    + + + \ No newline at end of file diff --git a/src/routes/docs/quick-start/+page.svelte b/src/routes/docs/quick-start/+page.svelte index b352945..0dfb354 100644 --- a/src/routes/docs/quick-start/+page.svelte +++ b/src/routes/docs/quick-start/+page.svelte @@ -70,6 +70,7 @@ ${"<"}Button type="filled" on:click={() => alert("Hello world!")}>Click me${"<"} [your theme snippet]`} name="+layout.svelte or similar" + lang="xml" /> {:else}

    @@ -117,6 +118,7 @@ ${"<"}Button type="filled" on:click={() => alert("Hello world!")}>Click me${"<"} `} name="app.html" + lang="xml" /> {:else} alert("Hello world!")}>Click me${"<"} --m3-font: [your font], system-ui, sans-serif; }`} name="app.css" + lang="css" /> {/if}

-

- Now you can start using components like this. Check the rest of the docs to learn more. -

- +

Now you can start using components like this. Check the rest of the docs to learn more.

+