diff --git a/scripts/generate-llms.ts b/scripts/generate-llms.ts new file mode 100644 index 0000000..25e65f3 --- /dev/null +++ b/scripts/generate-llms.ts @@ -0,0 +1,397 @@ +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/routes/+layout.svelte b/src/routes/+layout.svelte index 524879f..f15490e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,26 +1,20 @@ @@ -12,16 +11,17 @@ y = e.clientY / window.innerHeight; }} /> -
- -

M3 Svelte

-

+

+
+

M3 Svelte

+
+

M3 Svelte implements the Material 3 design system in Svelte, from the components to the animations to the theming.

- - Now with 256 stars and ripples + + Updated docs, plus 269 stars
@@ -59,6 +59,12 @@
diff --git a/src/routes/docs/Base.svelte b/src/routes/docs/Base.svelte new file mode 100644 index 0000000..17b2715 --- /dev/null +++ b/src/routes/docs/Base.svelte @@ -0,0 +1,106 @@ + + +
+ +
+ +
+
+ + diff --git a/src/routes/docs/detailed-walkthrough/+page.svelte b/src/routes/docs/detailed-walkthrough/+page.svelte new file mode 100644 index 0000000..a43f476 --- /dev/null +++ b/src/routes/docs/detailed-walkthrough/+page.svelte @@ -0,0 +1,174 @@ + + + +

+ Welcome to M3 Svelte! If you haven't already, play around with the components on the home page. + It's rather fun. They even adapt to the theme you pick. You should also check out the Discord + and GitHub if you want to keep track of this project. +

+

+ This page is a detailed walkthrough of how to use M3 Svelte: we'll take you from configuration + to advanced usage. +

+ +

Configuring your app

+

+ The first thing to do is to set up your theme and font. If you haven't done that yet, the quick + start page will help you. +

+

+ Beyond that, you can configure some general settings with CSS variables. You might already know + that you can change the default font stack with --m3-font, but you can also + customize the fonts of certain sizes (eg --m3-font-label) and their configuration + (eg --m3-font-label-large-size). You can also tweak rounding by setting variables + like --m3-util-rounding-small and --m3-button-shape. +

+

+ One obscure thing you can configure is ripples. You can use a simpler layer by defining + M3_SVELTE_NO_RIPPLE to true in your Vite config. +

+ +

Make your own components

+ + + Plain + + Tailwind + +

+ Chances are M3 doesn't have everything you need. That's where you can make your own components + 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:

+ )", + "on-primary": "rgb(var(--m3-scheme-on-primary) / )", + "primary-container": "rgb(var(--m3-scheme-primary-container) / )", + "on-primary-container": "rgb(var(--m3-scheme-on-primary-container) / )", + "secondary": "rgb(var(--m3-scheme-secondary) / )", + "on-secondary": "rgb(var(--m3-scheme-on-secondary) / )", + "secondary-container": "rgb(var(--m3-scheme-secondary-container) / )", + "on-secondary-container": "rgb(var(--m3-scheme-on-secondary-container) / )", + "tertiary": "rgb(var(--m3-scheme-tertiary) / )", + "on-tertiary": "rgb(var(--m3-scheme-on-tertiary) / )", + "tertiary-container": "rgb(var(--m3-scheme-tertiary-container) / )", + "on-tertiary-container": "rgb(var(--m3-scheme-on-tertiary-container) / )", + "error": "rgb(var(--m3-scheme-error) / )", + "on-error": "rgb(var(--m3-scheme-on-error) / )", + "error-container": "rgb(var(--m3-scheme-error-container) / )", + "on-error-container": "rgb(var(--m3-scheme-on-error-container) / )", + "background": "rgb(var(--m3-scheme-background) / )", + "on-background": "rgb(var(--m3-scheme-on-background) / )", + "surface": "rgb(var(--m3-scheme-surface) / )", + "on-surface": "rgb(var(--m3-scheme-on-surface) / )", + "surface-variant": "rgb(var(--m3-scheme-surface-variant) / )", + "on-surface-variant": "rgb(var(--m3-scheme-on-surface-variant) / )", + "outline": "rgb(var(--m3-scheme-outline) / )", + "outline-variant": "rgb(var(--m3-scheme-outline-variant) / )", + "shadow": "rgb(var(--m3-scheme-shadow) / )", + "scrim": "rgb(var(--m3-scheme-scrim) / )", + "inverse-surface": "rgb(var(--m3-scheme-inverse-surface) / )", + "inverse-on-surface": "rgb(var(--m3-scheme-inverse-on-surface) / )", + "inverse-primary": "rgb(var(--m3-scheme-inverse-primary) / )", + "surface-bright": "rgb(var(--m3-scheme-surface-bright) / )", + "surface-container": "rgb(var(--m3-scheme-surface-container) / )", + "surface-container-high": "rgb(var(--m3-scheme-surface-container-high) / )", + "surface-container-highest": "rgb(var(--m3-scheme-surface-container-highest) / )", + "surface-container-low": "rgb(var(--m3-scheme-surface-container-low) / )", + "surface-container-lowest": "rgb(var(--m3-scheme-surface-container-lowest) / )", + "surface-dim": "rgb(var(--m3-scheme-surface-dim) / )", + "surface-tint": "rgb(var(--m3-scheme-surface-tint) / )" +}`} + name="tailwind.config.js" + /> + {:else} + Click me<${""}/button> + diff --git a/src/routes/docs/quick-start/+page.svelte b/src/routes/docs/quick-start/+page.svelte new file mode 100644 index 0000000..4e975d3 --- /dev/null +++ b/src/routes/docs/quick-start/+page.svelte @@ -0,0 +1,187 @@ + + + +
    +
  1. +
    +
    + 1 + +
    +
    +
    +

    Install M3 Svelte with npm i m3-svelte (or your package manager).

    +
    +
  2. +
  3. +
    +
    + 2 + +
    + + + StyleFromScheme + + Manual + +
    +
    + {#if step2Page == "stylefromscheme"} +

    + Copy a theme snippet and paste it on your site. +

    + + import { StyleFromScheme } from "m3-svelte"; +<${""}/script> + +[your theme snippet]`} + name="+layout.svelte or similar" + /> + {:else} +

    + If you want to lower your bundle size, manually adding the styles is an option. While + this means you only send the styles once, it isn't recommended. +

    +

    + You'll need to set all the colors on :root, add theme-color + meta tags, and add the + base styles. +

    + {/if} +
    +
  4. +
  5. +
    +
    + 3 + +
    + + + Roboto + + Manual + +
    +
    +

    Get a font for M3 Svelte to use.

    + {#if step3Page == "roboto"} + `} + name="app.html" + /> + {:else} + + {/if} +
    +
  6. +
+ + + diff --git a/static/llms.txt b/static/llms.txt index de03854..88f3c0b 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -15,7 +15,7 @@ import { ComponentName } from "m3-svelte"; **Buttons** - Button -- ButtonLink +- ButtonLink - SegmentedButtonContainer - SegmentedButtonItem - FAB @@ -34,11 +34,13 @@ import { ComponentName } from "m3-svelte"; - SnackbarAnim - SnackbarItem -**Forms** +**Progress** - LinearProgress - LinearProgressIndeterminate - CircularProgress - CircularProgressIndeterminate + +**Inputs** - RadioAnim1 - RadioAnim2 - RadioAnim3 @@ -47,14 +49,20 @@ import { ComponentName } from "m3-svelte"; - Switch - Slider - SliderTicks + +**Textfields** - TextField - TextFieldMultiline - TextFieldOutlined - TextFieldOutlinedMultiline + +**Pickers** - DatePickerDocked + +**Chips** - Chip -**Navigation** +**Nav** - NavDrawer - NavDrawerButton - NavDrawerLink @@ -66,45 +74,236 @@ import { ComponentName } from "m3-svelte"; - VariableTabs - VariableTabsLink -**Utilities** +**Utils** - ChipChooser - Divider - DateField -## Usage +## Components + +### ChipChooser +- Props: + - options: { label + - chosenOptions: string[] + +### DateField +- Props: + - name: string + - date: string + - required: boolean + - disabled: boolean + - extraOptions: HTMLInputAttributes +Example: +```svelte + +``` +Tip: +This component is like DatePickerDocked but it has a field and it's easier to use + +### Divider +- Props: + - inset: boolean -BUTTON +### Button - Types: elevated, filled, tonal, outlined, text -- Optional icon: none, left icon, full icon +- Optional icon: none, left, full - Props: - - type: "elevated" | "filled" | "tonal" | "outlined" | "text" + - display: string + - extraOptions: HTMLButtonAttributes - iconType: "none" | "left" | "full" + - type: "elevated" | "filled" | "tonal" | "outlined" | "text" - disabled: boolean +- Has a slot Example: ```svelte - ``` -CARD +### ButtonLink +- Props: + - display: string + - extraOptions: HTMLAnchorAttributes + - iconType: "none" | "left" | "full" + - type: "elevated" | "filled" | "tonal" | "outlined" | "text" + - href: string +- Has a slot + +### 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 +Example: +```svelte + +``` + +### SegmentedButtonContainer +- Props: + - display: string + - extraOptions: HTMLAttributes +- Has a slot +Example: +```svelte + + + Tab A + + Tab B + +``` + +### SegmentedButtonItem +- Props: + - display: string + - extraOptions: HTMLLabelAttributes + - input: string + - icon: IconifyIcon | undefined +- Has a slot + +### NavDrawer +- Props: + - display: string + - extraOptions: HTMLAttributes +- Has a slot + +### NavDrawerButton +- Props: + - selected: boolean + - extraOptions: HTMLButtonAttributes + - icon: IconifyIcon +- Has a slot + +### NavDrawerLink +- Props: + - href: string + - selected: boolean + - extraOptions: HTMLAnchorAttributes + - icon: IconifyIcon +- Has a slot + +### NavList +- Props: + - display: string + - extraOptions: HTMLAttributes + - type: "rail" | "bar" | "auto" +- Has a slot + +### NavListButton +- Props: + - display: string + - extraOptions: HTMLButtonAttributes + - type: "rail" | "bar" | "auto" + - selected: boolean + - icon: IconifyIcon +- Has a slot + +### NavListLink +- Props: + - display: string + - extraOptions: HTMLAnchorAttributes + - type: "rail" | "bar" | "auto" + - href: string + - selected: boolean + - icon: IconifyIcon +- Has a slot + +### Tabs +- Props: + - display: string + - extraWrapperOptions: HTMLAttributes + - extraOptions: HTMLInputAttributes + - secondary: boolean + - tab: string + - items: { +Example: +```svelte + +``` + +### TabsLink +- Props: + - display: string + - extraWrapperOptions: HTMLAttributes + - extraOptions: HTMLAnchorAttributes + - secondary: boolean + - tab: string + - items: { + +### VariableTabs +- Props: + - display: string + - extraWrapperOptions: HTMLAttributes + - extraOptions: HTMLInputAttributes + - secondary: boolean + - tab: string + - items: { + +### VariableTabsLink +- Props: + - display: string + - extraWrapperOptions: HTMLAttributes + - extraOptions: HTMLAnchorAttributes + - secondary: boolean + - tab: string + - items: { + +### Layer + +### StyleFromScheme +- Props: + - lightScheme: SerializedScheme + - darkScheme: SerializedScheme + +### BottomSheet +- Has a slot + +### Card - Types: elevated, filled, outlined -- Can be clickable or static - Props: + - display: string + - extraOptions: HTMLAttributes & HTMLButtonAttributes - type: "elevated" | "filled" | "outlined" - - clickable: boolean (use CardClickable component) +- Has a slot Example: ```svelte Hello Click me ``` -DIALOG -- Modal dialog with icon, headline, content and buttons +### CardClickable - Props: - - icon: IconifyIcon + - display: string + - extraOptions: HTMLAttributes & HTMLButtonAttributes + - type: "elevated" | "filled" | "outlined" +- Has a slot + +### Dialog +- Props: + - display: string + - extraOptions: HTMLDialogAttributes + - icon: IconifyIcon | undefined - headline: string - open: boolean + - closeOnEsc: boolean + - closeOnClick: boolean +- Slots: + - default + - buttons Example: ```svelte @@ -116,13 +315,64 @@ Example: ``` -SNACKBAR -- Toast-style notifications -- Optional actions and close button +### ListItem +- Props: + - display: string + - extraOptions: HTMLAttributes + - 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 +- Has a slot +Example: +```svelte + + Undo + Redo + +``` + +### MenuItem - Props: - - message: string - - actions?: Record void> - - closable?: boolean + - icon: IconifyIcon | "space" | undefined + - disabled: boolean +- Has a slot + +### Snackbar +- Props: + - extraWrapperOptions: HTMLAttributes + - extraOptions: ComponentProps Example: ```svelte