From 3404cf8c9583eacf61b8660269d855f5e80d58e0 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 18 Jun 2026 21:58:36 +0300 Subject: [PATCH 1/3] feat(playground): add design system blocks with multi-file TSX samples and file tree Overhaul the playground app to support multi-file editing with a new FileTree component. Replace Vue SFC samples with TSX + Styleframe config pairs covering Avatar, Badge, Button, Callout, Card, Checkbox, Input, and Spinner. Introduce bundlePreview pipeline (replaces compileVueSfc), new playground UI recipes (file tab, file tree, brand mark, editor shell), and update state management and build config accordingly. Also simplifies the field-group recipe to consolidate .input/.select/.textarea flex-grow selectors into a single joined CSS selector. --- .changeset/playground-design-system-blocks.md | 5 + apps/playground/AGENTS.md | 93 ++-- apps/playground/README.md | 64 +-- apps/playground/package.json | 8 +- apps/playground/src/App.vue | 136 +++--- apps/playground/src/components/BrandMark.vue | 32 +- apps/playground/src/components/EditorPane.vue | 126 +++-- .../playground/src/components/FileTabList.vue | 27 +- apps/playground/src/components/FileTree.vue | 421 ++++++++++++++++ apps/playground/src/components/Icon.vue | 13 +- .../src/components/PreviewFrame.vue | 16 + apps/playground/src/components/SplitPane.vue | 17 +- apps/playground/src/editor/codemirror.ts | 16 +- apps/playground/src/pipeline/buildSrcdoc.ts | 100 +--- apps/playground/src/pipeline/bundlePreview.ts | 218 +++++++++ apps/playground/src/pipeline/compileVueSfc.ts | 80 ---- .../playground/src/pipeline/evalUserConfig.ts | 24 + apps/playground/src/pipeline/pipeline.ts | 90 ++-- .../src/pipeline/transpileStyleframe.ts | 5 +- apps/playground/src/recipes/index.ts | 1 + .../src/recipes/playground.styleframe.ts | 29 ++ .../src/recipes/useBrandMarkRecipe.ts | 10 + .../src/recipes/useEditorShellRecipe.ts | 19 + .../src/recipes/useFileTabRecipe.ts | 29 ++ .../src/recipes/useFileTreeRecipe.ts | 269 +++++++++++ apps/playground/src/samples/App.sample.tsx | 255 ++++++++++ apps/playground/src/samples/App.sample.vue | 27 -- .../src/samples/App.styleframe.sample.ts | 146 ++++++ apps/playground/src/samples/Avatar.sample.tsx | 22 + .../src/samples/Avatar.styleframe.sample.ts | 19 + apps/playground/src/samples/Badge.sample.tsx | 38 ++ .../src/samples/Badge.styleframe.sample.ts | 19 + apps/playground/src/samples/Button.sample.tsx | 42 ++ apps/playground/src/samples/Button.sample.vue | 37 -- .../src/samples/Button.styleframe.sample.ts | 19 + .../playground/src/samples/Callout.sample.tsx | 27 ++ .../src/samples/Callout.styleframe.sample.ts | 18 + apps/playground/src/samples/Card.sample.tsx | 28 ++ apps/playground/src/samples/Card.sample.vue | 30 -- .../src/samples/Card.styleframe.sample.ts | 26 + .../src/samples/Checkbox.sample.tsx | 28 ++ .../src/samples/Checkbox.styleframe.sample.ts | 21 + apps/playground/src/samples/Input.sample.tsx | 32 ++ .../src/samples/Input.styleframe.sample.ts | 18 + .../playground/src/samples/Spinner.sample.tsx | 19 + .../src/samples/Spinner.styleframe.sample.ts | 19 + .../src/samples/styleframe.config.sample.ts | 175 +++++-- apps/playground/src/state/playground.ts | 227 ++++++++- .../playground/src/types/virtual-modules.d.ts | 22 +- apps/playground/test/buildSrcdoc.test.ts | 40 +- apps/playground/test/bundlePreview.test.ts | 64 +++ apps/playground/test/codemirror.test.ts | 8 +- apps/playground/test/compileVueSfc.test.ts | 85 ---- apps/playground/test/evalUserConfig.test.ts | 30 +- apps/playground/test/pipeline.test.ts | 136 ++++-- apps/playground/test/playgroundState.test.ts | 217 ++++++++- apps/playground/tsconfig.app.json | 3 +- apps/playground/vite.config.ts | 92 ++-- pnpm-lock.yaml | 451 ++++++++++++++++-- .../field-group/useFieldGroupRecipe.ts | 12 +- 60 files changed, 3474 insertions(+), 826 deletions(-) create mode 100644 .changeset/playground-design-system-blocks.md create mode 100644 apps/playground/src/components/FileTree.vue create mode 100644 apps/playground/src/pipeline/bundlePreview.ts delete mode 100644 apps/playground/src/pipeline/compileVueSfc.ts create mode 100644 apps/playground/src/recipes/useFileTreeRecipe.ts create mode 100644 apps/playground/src/samples/App.sample.tsx delete mode 100644 apps/playground/src/samples/App.sample.vue create mode 100644 apps/playground/src/samples/App.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Avatar.sample.tsx create mode 100644 apps/playground/src/samples/Avatar.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Badge.sample.tsx create mode 100644 apps/playground/src/samples/Badge.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Button.sample.tsx delete mode 100644 apps/playground/src/samples/Button.sample.vue create mode 100644 apps/playground/src/samples/Button.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Callout.sample.tsx create mode 100644 apps/playground/src/samples/Callout.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Card.sample.tsx delete mode 100644 apps/playground/src/samples/Card.sample.vue create mode 100644 apps/playground/src/samples/Card.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Checkbox.sample.tsx create mode 100644 apps/playground/src/samples/Checkbox.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Input.sample.tsx create mode 100644 apps/playground/src/samples/Input.styleframe.sample.ts create mode 100644 apps/playground/src/samples/Spinner.sample.tsx create mode 100644 apps/playground/src/samples/Spinner.styleframe.sample.ts create mode 100644 apps/playground/test/bundlePreview.test.ts delete mode 100644 apps/playground/test/compileVueSfc.test.ts diff --git a/.changeset/playground-design-system-blocks.md b/.changeset/playground-design-system-blocks.md new file mode 100644 index 00000000..05ea8f61 --- /dev/null +++ b/.changeset/playground-design-system-blocks.md @@ -0,0 +1,5 @@ +--- +"@styleframe/theme": patch +--- + +Simplify field-group recipe selector: consolidate `.input`, `.select`, `.textarea` flex-grow rules into a single joined selector. diff --git a/apps/playground/AGENTS.md b/apps/playground/AGENTS.md index baf23529..507bd111 100644 --- a/apps/playground/AGENTS.md +++ b/apps/playground/AGENTS.md @@ -1,11 +1,12 @@ # Styleframe Playground -Interactive client-side playground for Styleframe. Users edit three files in CodeMirror; an iframe renders the live result. +Interactive client-side playground for Styleframe. Users edit an arbitrary set of files (React `tsx` components + a `styleframe.config.ts`) in CodeMirror; an iframe renders the live result. ## Package - **Name:** `@styleframe/playground` -- **Build tool:** Vite 7 + Vue 3 +- **Shell:** Vite + Vue 3 (the playground UI is a Vue app; it is invisible to users) +- **Preview:** React 19 + `tsx`, bundled in the browser with `esbuild-wasm` - **Runs:** fully client-side — no server, no auth, no persistence. - **Not published to npm.** @@ -14,42 +15,35 @@ Interactive client-side playground for Styleframe. Users edit three files in Cod ``` apps/playground/ ├── styleframe.config.ts # shell: theme presets + #app / html selectors -├── vite.config.ts # Styleframe Vite plugin + vue() +├── vite.config.ts # Styleframe Vite plugin (minify:false) + vue() + React vendoring ├── index.html ├── src/ -│ ├── App.vue # playground shell root +│ ├── App.vue # playground shell root (Editor │ Output │ Tree) │ ├── main.ts # imports virtual:styleframe.css, mounts App -│ ├── recipes/ # playground-local recipes (pg-*) -│ │ ├── useTabRecipe.ts -│ │ ├── useTabListRecipe.ts -│ │ ├── useSplitPaneRecipe.ts -│ │ ├── useEditorShellRecipe.ts -│ │ ├── useErrorBannerRecipe.ts -│ │ ├── useToolbarRecipe.ts -│ │ ├── index.ts # barrel +│ ├── recipes/ # playground-local recipes (pg-*), incl. useFileTreeRecipe │ │ └── playground.styleframe.ts # registers recipes into the shell instance │ ├── components/ -│ │ ├── SplitPane.vue -│ │ ├── TabList.vue -│ │ ├── EditorPane.vue # 3-tab editor (config, App, Component) -│ │ ├── OutputPane.vue # 3-tab output (Preview, CSS, JS) -│ │ ├── CodeOutput.vue # read-only CodeMirror +│ │ ├── SplitPane.vue # Editor │ Output split +│ │ ├── FileTree.vue # project tree (far right): create / rename / delete +│ │ ├── EditorPane.vue # dynamic, path-keyed CodeMirror editors + tabs +│ │ ├── OutputPane.vue # Preview / CSS / JS tabs │ │ ├── PreviewFrame.vue # iframe + postMessage listener │ │ └── ErrorBanner.vue │ ├── editor/ -│ │ ├── codemirror.ts # createEditor({parent, doc, language, onChange}) -│ │ └── theme.ts # CodeMirror theme extension +│ │ ├── codemirror.ts # createEditor(...) + languageForPath() (ts / tsx / css) +│ │ └── theme.ts │ ├── pipeline/ │ │ ├── esbuild.ts # lazy esbuild-wasm init (single instance) -│ │ ├── transformTs.ts -│ │ ├── evalUserConfig.ts # rewrites imports, runs via new Function +│ │ ├── transformTs.ts # strips TS from the config before eval +│ │ ├── evalUserConfig.ts # rewrites imports, runs config via new Function +│ │ ├── scanAndRegisterUtilities.ts │ │ ├── transpileStyleframe.ts -│ │ ├── compileVueSfc.ts -│ │ ├── buildSrcdoc.ts +│ │ ├── bundlePreview.ts # esbuild-wasm bundle of all user files → one ESM module +│ │ ├── buildSrcdoc.ts # CSS + React vendor + bundle → srcdoc │ │ └── pipeline.ts # orchestrator + debounce + PipelineResult -│ ├── samples/ # default file contents, imported as ?raw +│ ├── samples/ # default file contents (.tsx + config), imported as ?raw │ └── state/ -│ └── playground.ts # reactive { files, output, error, active* } +│ └── playground.ts # reactive { files, output, error, active* } + file actions └── test/ # Vitest specs for pipeline units ``` @@ -60,36 +54,45 @@ apps/playground/ | Context | Compiled by | Where CSS lives | Runtime | |---|---|---|---| | Shell UI | `@styleframe/plugin/vite` at dev/build time | ` -
- +
+ + `; const revoke = () => { - URL.revokeObjectURL(configUrl); - URL.revokeObjectURL(cardUrl); - URL.revokeObjectURL(buttonUrl); - URL.revokeObjectURL(appUrl); - URL.revokeObjectURL(bootUrl); + URL.revokeObjectURL(reactUrl); + URL.revokeObjectURL(bundleUrl); }; return { srcdoc, revoke }; diff --git a/apps/playground/src/pipeline/bundlePreview.ts b/apps/playground/src/pipeline/bundlePreview.ts new file mode 100644 index 00000000..794b9514 --- /dev/null +++ b/apps/playground/src/pipeline/bundlePreview.ts @@ -0,0 +1,218 @@ +import type { Loader, Plugin } from "esbuild-wasm"; +import { getEsbuild } from "./esbuild"; + +export interface BundlePreviewInput { + /** All editable files, keyed by path. */ + files: Record; + /** Entry file — its default export is rendered as the preview root. */ + entryPath: string; + /** The file exporting the Styleframe instance (excluded from bundling). */ + configPath: string; + /** Generated `virtual:styleframe` runtime module (recipe functions). */ + runtimeTs: string; +} + +export interface BundlePreviewResult { + /** The compiled preview as a single ESM module. */ + bundleJs: string; + /** CSS emitted by any user `import './styles.css'` (usually empty). */ + css: string; + /** IIFE that publishes React on `globalThis.PGReactVendor`. */ + reactIife: string; +} + +const SOURCE_EXTENSIONS = ["", ".tsx", ".ts", ".jsx", ".js", ".css"] as const; +const INDEX_EXTENSIONS = [ + "/index.tsx", + "/index.ts", + "/index.jsx", + "/index.js", +] as const; + +/** Resolve a `./` or `../` import against the in-memory file map. */ +function joinPath(baseDir: string, spec: string): string { + const fromRoot = spec.startsWith("/"); + const stack = fromRoot || !baseDir ? [] : baseDir.split("/"); + for (const part of spec.split("/")) { + if (part === "" || part === ".") continue; + if (part === "..") stack.pop(); + else stack.push(part); + } + return stack.join("/"); +} + +function loaderFor(path: string): Loader { + if (path.endsWith(".tsx")) return "tsx"; + if (path.endsWith(".ts")) return "ts"; + if (path.endsWith(".jsx")) return "jsx"; + if (path.endsWith(".css")) return "css"; + return "js"; +} + +const VENDOR_SPECIFIERS = new Set([ + "react", + "react-dom/client", + "react/jsx-runtime", + "@styleframe/runtime", + "virtual:styleframe", + "virtual:styleframe.css", +]); + +const VENDOR_GLOBALS: Record = { + react: "React", + "react-dom/client": "ReactDOMClient", + "react/jsx-runtime": "JsxRuntime", +}; + +/** + * A shim module that re-exports a vendored React entry off the global the + * vendor IIFE publishes, so the single bundled React instance is shared. + */ +function reactShim(globalKey: string, keys: string[]): string { + const ref = `globalThis.PGReactVendor.${globalKey}`; + return [ + `const mod = ${ref};`, + "export default mod;", + ...keys.map((key) => `export const ${key} = mod[${JSON.stringify(key)}];`), + ].join("\n"); +} + +function createVirtualFsPlugin( + input: BundlePreviewInput, + vendor: { + reactKeys: string[]; + reactDomClientKeys: string[]; + jsxRuntimeKeys: string[]; + }, + runtimeSrc: string, +): Plugin { + const { files, configPath, runtimeTs } = input; + + function resolveFileKey(path: string): string | null { + for (const ext of SOURCE_EXTENSIONS) { + if (`${path}${ext}` in files) return `${path}${ext}`; + } + for (const ext of INDEX_EXTENSIONS) { + if (`${path}${ext}` in files) return `${path}${ext}`; + } + return null; + } + + return { + name: "pg-virtual-fs", + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + const spec = args.path; + if (VENDOR_SPECIFIERS.has(spec)) { + return { path: spec, namespace: "pg-vendor" }; + } + if (spec.startsWith(".") || spec.startsWith("/")) { + const importer = + args.importer && args.importer !== "" ? args.importer : ""; + const baseDir = importer.includes("/") + ? importer.slice(0, importer.lastIndexOf("/")) + : ""; + const key = resolveFileKey(joinPath(baseDir, spec)); + // The config and *.styleframe.ts files are authoring-only — they + // are evaluated in the parent, never bundled into the preview. + if (key && key !== configPath && !key.endsWith(".styleframe.ts")) { + return { path: key, namespace: "pg-file" }; + } + return { + errors: [ + { text: `Cannot resolve "${spec}" from "${args.importer}".` }, + ], + }; + } + return { + errors: [ + { + text: `Unknown import "${spec}". Create it as a file, or import recipes from "virtual:styleframe".`, + }, + ], + }; + }); + + build.onLoad({ filter: /.*/, namespace: "pg-file" }, (args) => ({ + contents: files[args.path] ?? "", + loader: loaderFor(args.path), + resolveDir: "/", + })); + + build.onLoad({ filter: /.*/, namespace: "pg-vendor" }, (args) => { + const spec = args.path; + if (spec === "virtual:styleframe") { + return { contents: runtimeTs, loader: "ts", resolveDir: "/" }; + } + if (spec === "virtual:styleframe.css") { + return { contents: "", loader: "js" }; + } + if (spec === "@styleframe/runtime") { + return { contents: runtimeSrc, loader: "js", resolveDir: "/" }; + } + const globalKey = VENDOR_GLOBALS[spec]!; + const keys = + spec === "react" + ? vendor.reactKeys + : spec === "react-dom/client" + ? vendor.reactDomClientKeys + : vendor.jsxRuntimeKeys; + return { contents: reactShim(globalKey, keys), loader: "js" }; + }); + }, + }; +} + +export async function bundlePreview( + input: BundlePreviewInput, +): Promise { + const esbuild = await getEsbuild(); + const [{ default: reactVendor }, { default: runtimeSrc }] = await Promise.all( + [import("virtual:pg-react-vendor"), import("virtual:pg-runtime-src")], + ); + + const boot = ` +import { createRoot } from "react-dom/client"; +import App from ${JSON.stringify(`./${input.entryPath}`)}; +const notify = (detail) => parent.postMessage({ type: "pg:error", detail }, "*"); +window.addEventListener("error", (event) => { + notify({ message: event.message, stack: event.error && event.error.stack }); +}); +window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason; + notify({ message: reason && reason.message ? reason.message : String(reason), stack: reason && reason.stack }); +}); +try { + createRoot(document.getElementById("root")).render(); +} catch (error) { + notify({ message: error && error.message ? error.message : String(error), stack: error && error.stack }); +} +`; + + const result = await esbuild.build({ + stdin: { + contents: boot, + resolveDir: "/", + sourcefile: "pgBoot.tsx", + loader: "tsx", + }, + bundle: true, + write: false, + format: "esm", + jsx: "automatic", + jsxDev: false, + target: "es2022", + plugins: [createVirtualFsPlugin(input, reactVendor, runtimeSrc)], + }); + + // esbuild names the stdin JS output ``; CSS from user imports lands + // in a sibling `.css` file. + const bundleJs = + result.outputFiles.find((file) => !file.path.endsWith(".css"))?.text ?? ""; + const css = result.outputFiles + .filter((file) => file.path.endsWith(".css")) + .map((file) => file.text) + .join("\n"); + + return { bundleJs, css, reactIife: reactVendor.iife }; +} diff --git a/apps/playground/src/pipeline/compileVueSfc.ts b/apps/playground/src/pipeline/compileVueSfc.ts deleted file mode 100644 index b51d99c0..00000000 --- a/apps/playground/src/pipeline/compileVueSfc.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc"; -import { transformTs } from "./transformTs"; - -export interface CompiledSfc { - code: string; -} - -function hashId(filename: string): string { - let hash = 5381; - for (let i = 0; i < filename.length; i++) { - hash = (hash * 33) ^ filename.charCodeAt(i); - } - return `pg-${(hash >>> 0).toString(36)}`; -} - -export async function compileVueSfc( - source: string, - filename: string, -): Promise { - const { descriptor, errors } = parse(source, { filename }); - if (errors.length > 0) { - throw new Error(errors.map((e) => e.message).join("\n")); - } - - const id = hashId(filename); - const hasScoped = descriptor.styles.some((s) => s.scoped); - - let scriptCode = ""; - let scriptLang: string | undefined; - let bindingMetadata: ReturnType["bindings"]; - if (descriptor.script || descriptor.scriptSetup) { - const compiled = compileScript(descriptor, { - id, - inlineTemplate: false, - genDefaultAs: "__sfc__", - }); - scriptCode = compiled.content; - scriptLang = - compiled.lang ?? descriptor.scriptSetup?.lang ?? descriptor.script?.lang; - bindingMetadata = compiled.bindings; - } else { - scriptCode = "const __sfc__ = {};"; - } - - let renderCode = ""; - if (descriptor.template) { - const compiled = compileTemplate({ - source: descriptor.template.content, - filename, - id, - scoped: hasScoped, - isProd: false, - compilerOptions: { bindingMetadata }, - }); - if (compiled.errors.length > 0) { - const messages = compiled.errors.map((e) => - typeof e === "string" ? e : e.message, - ); - throw new Error(messages.join("\n")); - } - renderCode = compiled.code.replace( - /export (function|const) (render|ssrRender)/, - "$1 _sfc_$2", - ); - } - - let combined = scriptCode; - combined += "\n" + renderCode + "\n"; - if (descriptor.template) { - combined += `__sfc__.render = _sfc_render;\n`; - } - combined += `__sfc__.__file = ${JSON.stringify(filename)};\n`; - combined += `export default __sfc__;\n`; - - if (scriptLang === "ts" || scriptLang === "tsx") { - combined = await transformTs(combined); - } - - return { code: combined }; -} diff --git a/apps/playground/src/pipeline/evalUserConfig.ts b/apps/playground/src/pipeline/evalUserConfig.ts index f990e940..daa30a7d 100644 --- a/apps/playground/src/pipeline/evalUserConfig.ts +++ b/apps/playground/src/pipeline/evalUserConfig.ts @@ -94,3 +94,27 @@ export async function evalUserConfig(compiledJs: string): Promise { } return candidate; } + +/** + * Evaluate a `*.styleframe.ts` extension file against the shared global + * instance. `import { styleframe } from "virtual:styleframe"` returns the same + * instance the config created, so the extension's recipe registrations mutate + * it — exactly how `@styleframe/plugin` loads extension files. + */ +export function evalStyleframeFile( + compiledJs: string, + instance: Styleframe, +): void { + const rewritten = rewriteImports(compiledJs); + const wrapped = `"use strict";\nconst __pgOutput = {};\n${rewritten}\nreturn __pgOutput;`; + const fn = new Function("__pgModules", wrapped) as ( + modules: Record, + ) => unknown; + fn({ + ...moduleShims, + "virtual:styleframe": { + styleframe: () => instance, + default: instance, + }, + }); +} diff --git a/apps/playground/src/pipeline/pipeline.ts b/apps/playground/src/pipeline/pipeline.ts index 01d91503..e36c4b5c 100644 --- a/apps/playground/src/pipeline/pipeline.ts +++ b/apps/playground/src/pipeline/pipeline.ts @@ -1,6 +1,6 @@ import { buildSrcdoc, type BuildSrcdocResult } from "./buildSrcdoc"; -import { compileVueSfc } from "./compileVueSfc"; -import { evalUserConfig } from "./evalUserConfig"; +import { bundlePreview } from "./bundlePreview"; +import { evalStyleframeFile, evalUserConfig } from "./evalUserConfig"; import { scanAndRegisterUtilities, type ScanResult, @@ -8,23 +8,21 @@ import { import { transformTs } from "./transformTs"; import { transpileStyleframe } from "./transpileStyleframe"; +/** `*.styleframe.ts` extension files are evaluated, not bundled or scanned. */ +export function isStyleframeFile(path: string): boolean { + return path.endsWith(".styleframe.ts"); +} + export interface PipelineInput { - config: string; - app: string; - card: string; - button: string; - vueUrl: string; - runtimeUrl: string; + files: Record; + configPath: string; + entryPath: string; } export interface PipelineSuccess { ok: true; css: string; runtimeTs: string; - configCode: string; - appCode: string; - cardCode: string; - buttonCode: string; srcdoc: string; scan: ScanResult; revoke: () => void; @@ -35,10 +33,10 @@ export interface PipelineFailure { stage: | "config-transform" | "config-eval" + | "styleframe" | "scan" | "transpile" - | "config-compile" - | "vue" + | "bundle" | "assemble"; error: Error; } @@ -53,9 +51,11 @@ function toError(value: unknown): Error { export async function runPipeline( input: PipelineInput, ): Promise { + const configSource = input.files[input.configPath] ?? ""; + let compiledConfig: string; try { - compiledConfig = await transformTs(input.config); + compiledConfig = await transformTs(configSource); } catch (error) { return { ok: false, stage: "config-transform", error: toError(error) }; } @@ -67,13 +67,25 @@ export async function runPipeline( return { ok: false, stage: "config-eval", error: toError(error) }; } + // Auto-include every *.styleframe.ts file: each extends the shared instance. + try { + const styleframePaths = Object.keys(input.files) + .filter((path) => path !== input.configPath && isStyleframeFile(path)) + .sort(); + for (const path of styleframePaths) { + const compiled = await transformTs(input.files[path] ?? ""); + evalStyleframeFile(compiled, instance); + } + } catch (error) { + return { ok: false, stage: "styleframe", error: toError(error) }; + } + let scan: ScanResult; try { - scan = scanAndRegisterUtilities(instance, [ - { content: input.app, filePath: "App.vue" }, - { content: input.card, filePath: "Card.vue" }, - { content: input.button, filePath: "Button.vue" }, - ]); + const sources = Object.entries(input.files) + .filter(([path]) => path !== input.configPath && !isStyleframeFile(path)) + .map(([filePath, content]) => ({ content, filePath })); + scan = scanAndRegisterUtilities(instance, sources); } catch (error) { return { ok: false, stage: "scan", error: toError(error) }; } @@ -85,36 +97,24 @@ export async function runPipeline( return { ok: false, stage: "transpile", error: toError(error) }; } - let configCode: string; - try { - configCode = await transformTs(transpiled.ts); - } catch (error) { - return { ok: false, stage: "config-compile", error: toError(error) }; - } - - let appCompiled: { code: string }; - let cardCompiled: { code: string }; - let buttonCompiled: { code: string }; + let bundled: Awaited>; try { - [appCompiled, cardCompiled, buttonCompiled] = await Promise.all([ - compileVueSfc(input.app, "App.vue"), - compileVueSfc(input.card, "Card.vue"), - compileVueSfc(input.button, "Button.vue"), - ]); + bundled = await bundlePreview({ + files: input.files, + entryPath: input.entryPath, + configPath: input.configPath, + runtimeTs: transpiled.ts, + }); } catch (error) { - return { ok: false, stage: "vue", error: toError(error) }; + return { ok: false, stage: "bundle", error: toError(error) }; } let built: BuildSrcdocResult; try { built = buildSrcdoc({ - css: transpiled.css, - configCode, - appCode: appCompiled.code, - cardCode: cardCompiled.code, - buttonCode: buttonCompiled.code, - vueUrl: input.vueUrl, - runtimeUrl: input.runtimeUrl, + css: transpiled.css + bundled.css, + bundleJs: bundled.bundleJs, + reactIife: bundled.reactIife, }); } catch (error) { return { ok: false, stage: "assemble", error: toError(error) }; @@ -124,10 +124,6 @@ export async function runPipeline( ok: true, css: transpiled.css, runtimeTs: transpiled.ts, - configCode, - appCode: appCompiled.code, - cardCode: cardCompiled.code, - buttonCode: buttonCompiled.code, srcdoc: built.srcdoc, scan, revoke: built.revoke, diff --git a/apps/playground/src/pipeline/transpileStyleframe.ts b/apps/playground/src/pipeline/transpileStyleframe.ts index 18f304b3..99ebf299 100644 --- a/apps/playground/src/pipeline/transpileStyleframe.ts +++ b/apps/playground/src/pipeline/transpileStyleframe.ts @@ -9,7 +9,10 @@ export interface StyleframeOutput { export async function transpileStyleframe( instance: Styleframe, ): Promise { - const output = await transpile(instance, { treeshake: true, scanner: true }); + const output = await transpile(instance, { + treeshake: false, + scanner: false, + }); let css = ""; let ts = ""; for (const file of output.files) { diff --git a/apps/playground/src/recipes/index.ts b/apps/playground/src/recipes/index.ts index b2ce0749..7af439f4 100644 --- a/apps/playground/src/recipes/index.ts +++ b/apps/playground/src/recipes/index.ts @@ -3,6 +3,7 @@ export * from "./useBrowserChromeRecipe"; export * from "./useEditorShellRecipe"; export * from "./useErrorBannerRecipe"; export * from "./useFileTabRecipe"; +export * from "./useFileTreeRecipe"; export * from "./useSplitPaneRecipe"; export * from "./useStatusBarRecipe"; export * from "./useTabListRecipe"; diff --git a/apps/playground/src/recipes/playground.styleframe.ts b/apps/playground/src/recipes/playground.styleframe.ts index 8128a970..f18c2b3b 100644 --- a/apps/playground/src/recipes/playground.styleframe.ts +++ b/apps/playground/src/recipes/playground.styleframe.ts @@ -2,6 +2,7 @@ import { useButtonRecipe } from "@styleframe/theme"; import { styleframe } from "virtual:styleframe"; import { useBrandBadgeRecipe, + useBrandLinkRecipe, useBrandLogotypeRecipe, useBrandMarkRecipe, useBrowserActionRecipe, @@ -13,14 +14,27 @@ import { useBrowserUrlTextRecipe, useBrowserViewportFrameRecipe, useBrowserViewportRecipe, + useEditorEmptyRecipe, useEditorShellRecipe, useEditorSurfaceRecipe, useErrorBannerCloseRecipe, useErrorBannerMessageRecipe, useErrorBannerRecipe, + useFileTabCloseRecipe, useFileTabDotRecipe, useFileTabListRecipe, useFileTabRecipe, + useFileTreeActionRecipe, + useFileTreeHeaderRecipe, + useFileTreeIconRecipe, + useFileTreeInputRecipe, + useFileTreeItemRecipe, + useFileTreeLabelRecipe, + useFileTreeListRecipe, + useFileTreeMenuItemRecipe, + useFileTreeMenuRecipe, + useFileTreeRecipe, + useFileTreeTwistyRecipe, useHmrDotRecipe, useHmrIndicatorRecipe, useSplitPaneDividerRecipe, @@ -46,6 +60,7 @@ export const pgSplitPanePane = useSplitPanePaneRecipe(s); export const pgSplitPaneDivider = useSplitPaneDividerRecipe(s); export const pgEditorShell = useEditorShellRecipe(s); export const pgEditorSurface = useEditorSurfaceRecipe(s); +export const pgEditorEmpty = useEditorEmptyRecipe(s); export const pgErrorBanner = useErrorBannerRecipe(s); export const pgErrorBannerMessage = useErrorBannerMessageRecipe(s); export const pgErrorBannerClose = useErrorBannerCloseRecipe(s); @@ -55,12 +70,26 @@ export const pgThemeToggle = useThemeToggleRecipe(s); export const button = useButtonRecipe(s); export const pgBrandMark = useBrandMarkRecipe(s); +export const pgBrandLink = useBrandLinkRecipe(s); export const pgBrandLogotype = useBrandLogotypeRecipe(s); export const pgBrandBadge = useBrandBadgeRecipe(s); export const pgFileTabList = useFileTabListRecipe(s); export const pgFileTab = useFileTabRecipe(s); export const pgFileTabDot = useFileTabDotRecipe(s); +export const pgFileTabClose = useFileTabCloseRecipe(s); + +export const pgFileTree = useFileTreeRecipe(s); +export const pgFileTreeHeader = useFileTreeHeaderRecipe(s); +export const pgFileTreeList = useFileTreeListRecipe(s); +export const pgFileTreeItem = useFileTreeItemRecipe(s); +export const pgFileTreeLabel = useFileTreeLabelRecipe(s); +export const pgFileTreeIcon = useFileTreeIconRecipe(s); +export const pgFileTreeTwisty = useFileTreeTwistyRecipe(s); +export const pgFileTreeAction = useFileTreeActionRecipe(s); +export const pgFileTreeInput = useFileTreeInputRecipe(s); +export const pgFileTreeMenu = useFileTreeMenuRecipe(s); +export const pgFileTreeMenuItem = useFileTreeMenuItemRecipe(s); export const pgBrowserChrome = useBrowserChromeRecipe(s); export const pgBrowserDots = useBrowserDotsRecipe(s); diff --git a/apps/playground/src/recipes/useBrandMarkRecipe.ts b/apps/playground/src/recipes/useBrandMarkRecipe.ts index 65d9ac9d..961ade8a 100644 --- a/apps/playground/src/recipes/useBrandMarkRecipe.ts +++ b/apps/playground/src/recipes/useBrandMarkRecipe.ts @@ -9,6 +9,16 @@ export const useBrandMarkRecipe = createUseRecipe("pg-brand-mark", { }, }); +export const useBrandLinkRecipe = createUseRecipe("pg-brand-link", { + base: { + display: "inline-flex", + alignItems: "center", + color: "inherit", + textDecoration: "none", + cursor: "pointer", + }, +}); + export const useBrandLogotypeRecipe = createUseRecipe("pg-brand-logotype", { base: { display: "block", diff --git a/apps/playground/src/recipes/useEditorShellRecipe.ts b/apps/playground/src/recipes/useEditorShellRecipe.ts index ecc53640..827846e4 100644 --- a/apps/playground/src/recipes/useEditorShellRecipe.ts +++ b/apps/playground/src/recipes/useEditorShellRecipe.ts @@ -15,6 +15,7 @@ export const useEditorShellRecipe = createUseRecipe("pg-editor-shell", { export const useEditorSurfaceRecipe = createUseRecipe("pg-editor-surface", { base: { + position: "relative", flex: "1 1 auto", minHeight: "0", overflow: "auto", @@ -24,3 +25,21 @@ export const useEditorSurfaceRecipe = createUseRecipe("pg-editor-surface", { lineHeight: "@line-height.normal", }, }); + +export const useEditorEmptyRecipe = createUseRecipe("pg-editor-empty", { + base: { + position: "absolute", + top: "0", + left: "0", + right: "0", + bottom: "0", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "@1", + textAlign: "center", + fontSize: "@font-size.sm", + fontFamily: "@font-family.base", + color: "@color.gray-500", + }, +}); diff --git a/apps/playground/src/recipes/useFileTabRecipe.ts b/apps/playground/src/recipes/useFileTabRecipe.ts index 25aef95a..2dfc9d11 100644 --- a/apps/playground/src/recipes/useFileTabRecipe.ts +++ b/apps/playground/src/recipes/useFileTabRecipe.ts @@ -91,3 +91,32 @@ export const useFileTabDotRecipe = createUseRecipe("pg-file-tab-dot", { }, }, }); + +export const useFileTabCloseRecipe = createUseRecipe("pg-file-tab-close", { + base: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + flex: "0 0 auto", + width: "16px", + height: "16px", + marginLeft: "@0.25", + padding: "0", + color: "@color.gray-400", + background: "transparent", + borderWidth: "@border-width.none", + borderRadius: "@border-radius.sm", + cursor: "pointer", + "&:hover": { + background: "@color.gray-200", + color: "@color.gray-900", + }, + "&:dark": { + color: "@color.gray-500", + }, + "&:dark:hover": { + background: "@color.gray-700", + color: "@color.white", + }, + }, +}); diff --git a/apps/playground/src/recipes/useFileTreeRecipe.ts b/apps/playground/src/recipes/useFileTreeRecipe.ts new file mode 100644 index 00000000..b4c351f1 --- /dev/null +++ b/apps/playground/src/recipes/useFileTreeRecipe.ts @@ -0,0 +1,269 @@ +import { createUseRecipe } from "@styleframe/theme"; + +export const useFileTreeRecipe = createUseRecipe("pg-file-tree", { + base: { + display: "flex", + flexDirection: "column", + flex: "0 0 auto", + width: "220px", + minHeight: "0", + height: "100%", + background: "@color.gray-50", + borderLeftWidth: "@border-width.thin", + borderLeftStyle: "@border-style.solid", + borderLeftColor: "@color.gray-200", + "&:dark": { + background: "@color.gray-900", + borderLeftColor: "@color.gray-800", + }, + }, +}); + +export const useFileTreeHeaderRecipe = createUseRecipe("pg-file-tree-header", { + base: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "@0.5", + paddingTop: "@0.5", + paddingBottom: "@0.5", + paddingLeft: "@0.75", + paddingRight: "@0.5", + fontSize: "@font-size.xs", + fontWeight: "@font-weight.medium", + textTransform: "uppercase", + color: "@color.gray-500", + borderBottomWidth: "@border-width.thin", + borderBottomStyle: "@border-style.solid", + borderBottomColor: "@color.gray-200", + "&:dark": { + color: "@color.gray-400", + borderBottomColor: "@color.gray-800", + }, + }, +}); + +export const useFileTreeListRecipe = createUseRecipe("pg-file-tree-list", { + base: { + display: "flex", + flexDirection: "column", + flex: "1 1 auto", + minHeight: "0", + overflowY: "auto", + paddingTop: "@0.25", + paddingBottom: "@0.25", + }, +}); + +export const useFileTreeItemRecipe = createUseRecipe("pg-file-tree-item", { + base: { + display: "flex", + alignItems: "center", + gap: "@0.375", + width: "100%", + paddingTop: "@0.25", + paddingBottom: "@0.25", + paddingRight: "@0.5", + fontSize: "@font-size.sm", + fontFamily: "inherit", + color: "@color.gray-700", + background: "transparent", + borderWidth: "@border-width.none", + textAlign: "left", + whiteSpace: "nowrap", + cursor: "pointer", + "&:hover": { + background: "@color.gray-100", + }, + "&:dark": { + color: "@color.gray-300", + }, + "&:dark:hover": { + background: "@color.gray-800", + }, + }, + variants: { + state: { + inactive: {}, + active: { + background: "@color.gray-200", + color: "@color.gray-900", + "&:hover": { + background: "@color.gray-200", + }, + "&:dark": { + background: "@color.gray-800", + color: "@color.white", + }, + "&:dark:hover": { + background: "@color.gray-800", + }, + }, + }, + }, + defaultVariants: { + state: "inactive", + }, +}); + +export const useFileTreeLabelRecipe = createUseRecipe("pg-file-tree-label", { + base: { + flex: "1 1 auto", + overflow: "hidden", + textOverflow: "ellipsis", + }, +}); + +export const useFileTreeIconRecipe = createUseRecipe("pg-file-tree-icon", { + base: { + flex: "0 0 auto", + color: "@color.gray-500", + "&:dark": { + color: "@color.gray-400", + }, + }, +}); + +export const useFileTreeTwistyRecipe = createUseRecipe("pg-file-tree-twisty", { + base: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + flex: "0 0 auto", + width: "16px", + height: "16px", + color: "@color.gray-400", + "&:dark": { + color: "@color.gray-500", + }, + }, +}); + +export const useFileTreeActionRecipe = createUseRecipe("pg-file-tree-action", { + base: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + flex: "0 0 auto", + width: "20px", + height: "20px", + padding: "0", + color: "@color.gray-500", + background: "transparent", + borderWidth: "@border-width.none", + borderRadius: "@border-radius.sm", + cursor: "pointer", + "&:hover": { + background: "@color.gray-200", + color: "@color.gray-900", + }, + "&:dark:hover": { + background: "@color.gray-700", + color: "@color.white", + }, + }, +}); + +export const useFileTreeInputRecipe = createUseRecipe("pg-file-tree-input", { + base: { + width: "100%", + marginTop: "@0.25", + marginBottom: "@0.25", + marginLeft: "@0.5", + marginRight: "@0.5", + paddingTop: "@0.25", + paddingBottom: "@0.25", + paddingLeft: "@0.5", + paddingRight: "@0.5", + fontSize: "@font-size.sm", + fontFamily: "inherit", + color: "@color.gray-900", + background: "@color.white", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "@color.primary", + borderRadius: "@border-radius.sm", + outline: "none", + "&:dark": { + color: "@color.white", + background: "@color.gray-950", + }, + }, +}); + +export const useFileTreeMenuRecipe = createUseRecipe("pg-file-tree-menu", { + base: { + position: "fixed", + zIndex: "50", + display: "flex", + flexDirection: "column", + minWidth: "168px", + paddingTop: "@0.25", + paddingBottom: "@0.25", + background: "@color.white", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "@color.gray-200", + borderRadius: "@border-radius.md", + boxShadow: "@box-shadow.lg", + "&:dark": { + background: "@color.gray-900", + borderColor: "@color.gray-700", + }, + }, +}); + +export const useFileTreeMenuItemRecipe = createUseRecipe( + "pg-file-tree-menu-item", + { + base: { + display: "flex", + alignItems: "center", + gap: "@0.5", + width: "100%", + paddingTop: "@0.375", + paddingBottom: "@0.375", + paddingLeft: "@0.75", + paddingRight: "@0.75", + fontSize: "@font-size.sm", + fontFamily: "inherit", + textAlign: "left", + color: "@color.gray-700", + background: "transparent", + borderWidth: "@border-width.none", + cursor: "pointer", + whiteSpace: "nowrap", + "&:hover": { + background: "@color.gray-100", + }, + "&:dark": { + color: "@color.gray-300", + }, + "&:dark:hover": { + background: "@color.gray-800", + }, + }, + variants: { + tone: { + default: {}, + danger: { + color: "@color.error", + "&:hover": { + background: "@color.error", + color: "@color.white", + }, + "&:dark": { + color: "@color.error-300", + }, + "&:dark:hover": { + background: "@color.error", + color: "@color.white", + }, + }, + }, + }, + defaultVariants: { + tone: "default", + }, + }, +); diff --git a/apps/playground/src/samples/App.sample.tsx b/apps/playground/src/samples/App.sample.tsx new file mode 100644 index 00000000..018681b8 --- /dev/null +++ b/apps/playground/src/samples/App.sample.tsx @@ -0,0 +1,255 @@ +import "virtual:styleframe.css"; +import Avatar from "./components/Avatar/Avatar"; +import Badge from "./components/Badge/Badge"; +import Button from "./components/Button/Button"; +import Callout from "./components/Callout/Callout"; +import Card from "./components/Card/Card"; +import Checkbox from "./components/Checkbox/Checkbox"; +import Input from "./components/Input/Input"; +import Spinner from "./components/Spinner/Spinner"; + +const COLORS = [ + "primary", + "secondary", + "success", + "info", + "warning", + "error", + "neutral", + "light", + "dark", +] as const; +const BUTTON_VARIANTS = [ + "solid", + "outline", + "soft", + "subtle", + "ghost", + "link", +] as const; +const BADGE_VARIANTS = ["solid", "outline", "soft", "subtle"] as const; +const SIZES = ["xs", "sm", "md", "lg", "xl"] as const; +const CARD_VARIANTS = ["solid", "soft", "subtle"] as const; + +// Color scales generated for every base color by the design-token preset. +// (neutral / light / dark are surface colors without a 50–950 ramp.) +const PALETTE = [ + "primary", + "secondary", + "success", + "info", + "warning", + "error", + "gray", +] as const; +const LEVELS = [ + 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, + 850, 900, 950, +] as const; + +// Each chip applies exactly the utility classes printed beneath it. +const UTILITIES = [ + "_background:color.primary _color:white", + "_background:color.gray-100 _color:gray-900", + "_color:primary _font-weight:medium", + "_border-width:thin _border-style:solid _border-color:primary", +]; + +export default function App() { + return ( +
+
+
+

Styleframe UI Kit

+

+ Components, tokens, and utilities generated from{" "} + styleframe.config.ts. Edit the config or any component + on the left, then press Cmd+S (macOS) or{" "} + Ctrl+S (otherwise). +

+
+ +
+

Colors

+

+ Each base color generates a full 50–950 lightness scale. +

+ {PALETTE.map((name) => ( +
+ {name} + +
+ {LEVELS.map((level) => ( + + ))} +
+
+ ))} +
+ +
+

Buttons

+ {BUTTON_VARIANTS.map((variant) => ( +
+ {variant} + {COLORS.map((color) => ( + + ))} +
+ ))} +
+ disabled + {COLORS.map((color) => ( + + ))} +
+
+ sizes + {SIZES.map((size) => ( + + ))} +
+
+ +
+

Badges

+ {BADGE_VARIANTS.map((variant) => ( +
+ {variant} + {COLORS.map((color) => ( + + {color} + + ))} +
+ ))} +
+ +
+

Avatars

+
+ sizes + {SIZES.map((size) => ( + + SF + + ))} +
+
+ colors + + AB + + + CD + + EF + GH + + IJ + +
+
+ +
+

Callouts

+
+ + Heads up — this is an informational message. + + + Success — your changes have been saved. + + + Warning — double-check before continuing. + + + Error — something went wrong. + +
+
+ +
+

Cards

+
+ {CARD_VARIANTS.map((variant) => ( + {variant} card} + footer={} + > +

+ A {variant} card built from the card recipe with header, body, + and footer parts. +

+
+ ))} +
+
+ +
+

Form controls

+
+ input + + + + +
+
+ checkbox + Checked + Unchecked + Disabled +
+
+ +
+

Spinners

+
+ sizes + + + + +
+
+ +
+

Utility classes

+

+ Each chip is styled with the exact utility classes shown beneath it. +

+
+ {UTILITIES.map((className) => ( +
+ + Aa + + {className} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/playground/src/samples/App.sample.vue b/apps/playground/src/samples/App.sample.vue deleted file mode 100644 index 3894312a..00000000 --- a/apps/playground/src/samples/App.sample.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/apps/playground/src/samples/App.styleframe.sample.ts b/apps/playground/src/samples/App.styleframe.sample.ts new file mode 100644 index 00000000..e9d6b2b3 --- /dev/null +++ b/apps/playground/src/samples/App.styleframe.sample.ts @@ -0,0 +1,146 @@ +import { styleframe } from "virtual:styleframe"; + +const s = styleframe(); + +const { selector } = s; + +// Page scaffold +selector(".ui-kit", { + minHeight: "100vh", + background: "@color.background", + color: "@color.text", + paddingTop: "calc(@spacing * 2)", + paddingBottom: "calc(@spacing * 2)", +}); + +selector(".ui-kit-inner", { + display: "flex", + flexDirection: "column", + gap: "@spacing.2xl", + maxWidth: "60rem", + marginLeft: "auto", + marginRight: "auto", + paddingLeft: "@spacing.lg", + paddingRight: "@spacing.lg", +}); + +// Header +selector(".ui-kit-header", { + display: "flex", + flexDirection: "column", + gap: "@spacing.2xs", +}); + +selector(".ui-kit-title", { + margin: "0", + fontSize: "@font-size.2xl", + fontWeight: "@font-weight.bold", +}); + +selector(".ui-kit-subtitle", { + margin: "0", + fontSize: "@font-size.md", + color: "@color.text-weak", +}); + +// Sections +selector(".ui-kit-section", { + display: "flex", + flexDirection: "column", + gap: "@spacing.sm", +}); + +selector(".ui-kit-section-title", { + margin: "0", + paddingBottom: "@spacing.xs", + fontSize: "@font-size.lg", + fontWeight: "@font-weight.semibold", + borderBottomWidth: "@border-width.thin", + borderBottomStyle: "@border-style.solid", + borderBottomColor: "@color.gray-200", +}); + +selector('[data-theme="dark"] .ui-kit-section-title', { + borderBottomColor: "@color.gray-800", +}); + +selector(".ui-kit-row", { + display: "flex", + flexWrap: "wrap", + alignItems: "center", + gap: "@spacing.sm", +}); + +selector(".ui-kit-stack", { + display: "flex", + flexDirection: "column", + gap: "@spacing.sm", +}); + +selector(".ui-kit-label", { + minWidth: "5rem", + fontSize: "@font-size.sm", + fontWeight: "@font-weight.medium", + color: "@color.text-weak", +}); + +selector(".ui-kit-grid", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.md", +}); + +// Card grid — each direct child shares the available width. +selector(".ui-kit-cards", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.md", +}); + +selector(".ui-kit-cards > *", { + flex: "1 1 16rem", + maxWidth: "20rem", +}); + +// Token swatches +selector(".ui-kit-swatch", { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: "@spacing.2xs", +}); + +selector(".ui-kit-caption", { + fontSize: "@font-size.xs", + color: "@color.text-weak", + fontFamily: "@font-family.mono", +}); + +// Color scales — one individual square swatch per level. +selector(".ui-kit-ramp-row", { + display: "flex", + alignItems: "center", + gap: "@spacing.sm", +}); + +selector(".ui-kit-ramp", { + display: "flex", + flexWrap: "wrap", + flex: "1 1 auto", + minWidth: "0", + gap: "@spacing.2xs", +}); + +selector(".ui-kit-ramp-cell", { + flex: "0 0 auto", + width: "28px", + height: "28px", + borderRadius: "@border-radius.sm", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "@color.gray-200", +}); + +selector('[data-theme="dark"] .ui-kit-ramp-cell', { + borderColor: "@color.gray-800", +}); diff --git a/apps/playground/src/samples/Avatar.sample.tsx b/apps/playground/src/samples/Avatar.sample.tsx new file mode 100644 index 00000000..c9a1f70b --- /dev/null +++ b/apps/playground/src/samples/Avatar.sample.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; +import { avatar } from "virtual:styleframe"; + +interface AvatarProps { + color?: "primary" | "light" | "dark" | "neutral"; + variant?: "solid" | "soft"; + shape?: "circle" | "square"; + size?: "xs" | "sm" | "md" | "lg" | "xl"; + children?: ReactNode; +} + +export default function Avatar({ + color = "neutral", + variant = "soft", + shape = "circle", + size = "md", + children, +}: AvatarProps) { + return ( +
{children}
+ ); +} diff --git a/apps/playground/src/samples/Avatar.styleframe.sample.ts b/apps/playground/src/samples/Avatar.styleframe.sample.ts new file mode 100644 index 00000000..56773c15 --- /dev/null +++ b/apps/playground/src/samples/Avatar.styleframe.sample.ts @@ -0,0 +1,19 @@ +import { styleframe } from "virtual:styleframe"; +import { useAvatarRecipe } from "@styleframe/theme"; + +const s = styleframe(); + +// Basic example configuration for the Avatar recipe. +// Edit the base styles or default variants below to customize every . +// Anything you leave out falls back to the Styleframe theme defaults. +export const avatarRecipe = useAvatarRecipe(s, { + base: { + fontWeight: "@font-weight.semibold", + }, + defaultVariants: { + color: "neutral", + variant: "soft", + shape: "circle", + size: "md", + }, +}); diff --git a/apps/playground/src/samples/Badge.sample.tsx b/apps/playground/src/samples/Badge.sample.tsx new file mode 100644 index 00000000..aa72b04a --- /dev/null +++ b/apps/playground/src/samples/Badge.sample.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from "react"; +import { badge } from "virtual:styleframe"; + +interface BadgeProps { + color?: + | "primary" + | "secondary" + | "success" + | "info" + | "warning" + | "error" + | "light" + | "dark" + | "neutral"; + variant?: "solid" | "outline" | "soft" | "subtle"; + size?: "xs" | "sm" | "md" | "lg" | "xl"; + children?: ReactNode; +} + +export default function Badge({ + color = "neutral", + variant = "solid", + size = "sm", + children, +}: BadgeProps) { + // `neutral` is the recipe default, so only set color for the others. + return ( + + {children} + + ); +} diff --git a/apps/playground/src/samples/Badge.styleframe.sample.ts b/apps/playground/src/samples/Badge.styleframe.sample.ts new file mode 100644 index 00000000..183a9bd6 --- /dev/null +++ b/apps/playground/src/samples/Badge.styleframe.sample.ts @@ -0,0 +1,19 @@ +import { styleframe } from "virtual:styleframe"; +import { useBadgeRecipe } from "@styleframe/theme"; + +const s = styleframe(); + +// Basic example configuration for the Badge recipe. +// Edit the base styles or default variants below to customize every . +// Anything you leave out falls back to the Styleframe theme defaults. +export const badgeRecipe = useBadgeRecipe(s, { + base: { + fontWeight: "@font-weight.medium", + borderRadius: "@border-radius.md", + }, + defaultVariants: { + color: "neutral", + variant: "solid", + size: "sm", + }, +}); diff --git a/apps/playground/src/samples/Button.sample.tsx b/apps/playground/src/samples/Button.sample.tsx new file mode 100644 index 00000000..9b163162 --- /dev/null +++ b/apps/playground/src/samples/Button.sample.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from "react"; +import { button } from "virtual:styleframe"; + +interface ButtonProps { + color?: + | "primary" + | "secondary" + | "success" + | "info" + | "warning" + | "error" + | "light" + | "dark" + | "neutral"; + variant?: "solid" | "outline" | "soft" | "subtle" | "ghost" | "link"; + size?: "xs" | "sm" | "md" | "lg" | "xl"; + disabled?: boolean; + children?: ReactNode; +} + +export default function Button({ + color = "primary", + variant = "solid", + size = "md", + disabled, + children, +}: ButtonProps) { + // `neutral` is the recipe default, so only set color for the others. + return ( + + ); +} diff --git a/apps/playground/src/samples/Button.sample.vue b/apps/playground/src/samples/Button.sample.vue deleted file mode 100644 index 1ff27c9c..00000000 --- a/apps/playground/src/samples/Button.sample.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/apps/playground/src/samples/Button.styleframe.sample.ts b/apps/playground/src/samples/Button.styleframe.sample.ts new file mode 100644 index 00000000..f0c96555 --- /dev/null +++ b/apps/playground/src/samples/Button.styleframe.sample.ts @@ -0,0 +1,19 @@ +import { styleframe } from "virtual:styleframe"; +import { useButtonRecipe } from "@styleframe/theme"; + +const s = styleframe(); + +// Basic example configuration for the Button recipe. +// Edit the base styles or default variants below to customize every