From 035c23890b8cbc8990e21deb41a175dd0e2215be Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 17:14:41 +0700 Subject: [PATCH 1/6] feat: Add Placeholder recipe with dashed border and size variants --- .../components/placeholder/Placeholder.vue | 44 ++++++ .../preview/PlaceholderSizeGrid.vue | 18 +++ .../stories/components/placeholder.stories.ts | 84 +++++++++++ .../components/placeholder.styleframe.ts | 37 +++++ theme/src/recipes/index.ts | 1 + theme/src/recipes/placeholder/index.ts | 1 + .../placeholder/usePlaceholderRecipe.test.ts | 134 ++++++++++++++++++ .../placeholder/usePlaceholderRecipe.ts | 49 +++++++ 8 files changed, 368 insertions(+) create mode 100644 apps/storybook/src/components/components/placeholder/Placeholder.vue create mode 100644 apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue create mode 100644 apps/storybook/stories/components/placeholder.stories.ts create mode 100644 apps/storybook/stories/components/placeholder.styleframe.ts create mode 100644 theme/src/recipes/placeholder/index.ts create mode 100644 theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts create mode 100644 theme/src/recipes/placeholder/usePlaceholderRecipe.ts diff --git a/apps/storybook/src/components/components/placeholder/Placeholder.vue b/apps/storybook/src/components/components/placeholder/Placeholder.vue new file mode 100644 index 00000000..4b385b83 --- /dev/null +++ b/apps/storybook/src/components/components/placeholder/Placeholder.vue @@ -0,0 +1,44 @@ + + + diff --git a/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue b/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue new file mode 100644 index 00000000..46c8e86c --- /dev/null +++ b/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue @@ -0,0 +1,18 @@ + + + diff --git a/apps/storybook/stories/components/placeholder.stories.ts b/apps/storybook/stories/components/placeholder.stories.ts new file mode 100644 index 00000000..b8747730 --- /dev/null +++ b/apps/storybook/stories/components/placeholder.stories.ts @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from "@storybook/vue3-vite"; + +import Placeholder from "../../src/components/components/placeholder/Placeholder.vue"; +import PlaceholderSizeGrid from "../../src/components/components/placeholder/preview/PlaceholderSizeGrid.vue"; + +const sizes = ["sm", "md", "lg"] as const; + +const meta = { + title: "Theme/Recipes/Placeholder", + component: Placeholder, + tags: ["autodocs"], + parameters: { + layout: "padded", + }, + argTypes: { + size: { + control: "select", + options: sizes, + description: "The size of the placeholder", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + size: "md", + }, + render: (args) => ({ + components: { Placeholder }, + setup() { + return { args }; + }, + template: 'Placeholder content', + }), +}; + +export const AllSizes: StoryObj = { + render: () => ({ + components: { PlaceholderSizeGrid }, + template: "", + }), +}; + +export const Small: Story = { + args: { + size: "sm", + }, + render: (args) => ({ + components: { Placeholder }, + setup() { + return { args }; + }, + template: 'Small placeholder', + }), +}; + +export const Medium: Story = { + args: { + size: "md", + }, + render: (args) => ({ + components: { Placeholder }, + setup() { + return { args }; + }, + template: 'Medium placeholder', + }), +}; + +export const Large: Story = { + args: { + size: "lg", + }, + render: (args) => ({ + components: { Placeholder }, + setup() { + return { args }; + }, + template: 'Large placeholder', + }), +}; diff --git a/apps/storybook/stories/components/placeholder.styleframe.ts b/apps/storybook/stories/components/placeholder.styleframe.ts new file mode 100644 index 00000000..a8cafadf --- /dev/null +++ b/apps/storybook/stories/components/placeholder.styleframe.ts @@ -0,0 +1,37 @@ +import { usePlaceholderRecipe } from "@styleframe/theme"; +import { styleframe } from "virtual:styleframe"; + +const s = styleframe(); +const { selector } = s; + +export const placeholder = usePlaceholderRecipe(s); + +selector(".placeholder-grid", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.md", + padding: "@spacing.md", + alignItems: "flex-start", +}); + +selector(".placeholder-section", { + display: "flex", + flexDirection: "column", + gap: "@spacing.lg", + padding: "@spacing.md", +}); + +selector(".placeholder-row", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.sm", + alignItems: "flex-start", +}); + +selector(".placeholder-label", { + fontSize: "@font-size.sm", + fontWeight: "@font-weight.semibold", + minWidth: "80px", +}); + +export default s; diff --git a/theme/src/recipes/index.ts b/theme/src/recipes/index.ts index 527982f0..ce339c76 100644 --- a/theme/src/recipes/index.ts +++ b/theme/src/recipes/index.ts @@ -5,3 +5,4 @@ export * from "./callout"; export * from "./card"; export * from "./modal"; export * from "./nav"; +export * from "./placeholder"; diff --git a/theme/src/recipes/placeholder/index.ts b/theme/src/recipes/placeholder/index.ts new file mode 100644 index 00000000..0c3a9e03 --- /dev/null +++ b/theme/src/recipes/placeholder/index.ts @@ -0,0 +1 @@ +export * from "./usePlaceholderRecipe"; diff --git a/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts b/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts new file mode 100644 index 00000000..eb5125b3 --- /dev/null +++ b/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts @@ -0,0 +1,134 @@ +import { styleframe } from "@styleframe/core"; +import { useDarkModifier } from "../../modifiers/useMediaPreferenceModifiers"; +import { usePlaceholderRecipe } from "./usePlaceholderRecipe"; + +function createInstance() { + const s = styleframe(); + for (const name of [ + "display", + "alignItems", + "justifyContent", + "borderWidth", + "borderStyle", + "borderColor", + "borderRadius", + "overflow", + "position", + "opacity", + "paddingTop", + "paddingBottom", + "paddingLeft", + "paddingRight", + ]) { + s.utility(name, ({ value }) => ({ [name]: value })); + } + useDarkModifier(s); + return s; +} + +describe("usePlaceholderRecipe", () => { + it("should create a recipe with correct metadata", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s); + + expect(recipe.type).toBe("recipe"); + expect(recipe.name).toBe("placeholder"); + }); + + it("should have correct base styles", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s); + + expect(recipe.base).toEqual({ + display: "flex", + alignItems: "center", + justifyContent: "center", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.dashed", + borderColor: "@color.gray-300", + borderRadius: "@border-radius.md", + overflow: "hidden", + position: "relative", + opacity: "0.75", + "&:dark": { + borderColor: "@color.gray-600", + }, + }); + }); + + describe("variants", () => { + it("should have size variants with correct styles", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s); + + expect(recipe.variants!.size).toEqual({ + sm: { + paddingTop: "@0.5", + paddingBottom: "@0.5", + paddingLeft: "@0.5", + paddingRight: "@0.5", + }, + md: { + paddingTop: "@1", + paddingBottom: "@1", + paddingLeft: "@1", + paddingRight: "@1", + }, + lg: { + paddingTop: "@1.5", + paddingBottom: "@1.5", + paddingLeft: "@1.5", + paddingRight: "@1.5", + }, + }); + }); + }); + + it("should have correct default variants", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s); + + expect(recipe.defaultVariants).toEqual({ + size: "md", + }); + }); + + it("should have no compound variants", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s); + + expect(recipe.compoundVariants).toHaveLength(0); + }); + + describe("config overrides", () => { + it("should allow overriding base styles", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s, { + base: { display: "inline-flex" }, + }); + + expect(recipe.base!.display).toBe("inline-flex"); + expect(recipe.base!.alignItems).toBe("center"); + }); + }); + + describe("filter", () => { + it("should filter size variants", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s, { + filter: { size: ["sm", "lg"] }, + }); + + expect(Object.keys(recipe.variants!.size)).toEqual(["sm", "lg"]); + }); + + it("should adjust default variants when filtered out", () => { + const s = createInstance(); + const recipe = usePlaceholderRecipe(s, { + filter: { size: ["sm", "lg"] }, + }); + + expect(recipe.defaultVariants?.size).toBeUndefined(); + }); + }); +}); diff --git a/theme/src/recipes/placeholder/usePlaceholderRecipe.ts b/theme/src/recipes/placeholder/usePlaceholderRecipe.ts new file mode 100644 index 00000000..965b0c83 --- /dev/null +++ b/theme/src/recipes/placeholder/usePlaceholderRecipe.ts @@ -0,0 +1,49 @@ +import { createUseRecipe } from "../../utils/createUseRecipe"; + +/** + * Placeholder recipe for visual placeholder containers. + * A simple utility/display component with dashed border and crosshatch pattern. + */ +export const usePlaceholderRecipe = createUseRecipe("placeholder", { + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.dashed", + borderColor: "@color.gray-300", + borderRadius: "@border-radius.md", + overflow: "hidden", + position: "relative", + opacity: "0.75", + "&:dark": { + borderColor: "@color.gray-600", + }, + }, + variants: { + size: { + sm: { + paddingTop: "@0.5", + paddingBottom: "@0.5", + paddingLeft: "@0.5", + paddingRight: "@0.5", + }, + md: { + paddingTop: "@1", + paddingBottom: "@1", + paddingLeft: "@1", + paddingRight: "@1", + }, + lg: { + paddingTop: "@1.5", + paddingBottom: "@1.5", + paddingLeft: "@1.5", + paddingRight: "@1.5", + }, + }, + }, + compoundVariants: [], + defaultVariants: { + size: "md", + }, +}); From aae044003ac865bee0d8018ea514f853014f05f8 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 18:02:02 +0700 Subject: [PATCH 2/6] refactor: Simplify Placeholder recipe to base-only with CSS hatch pattern Remove size variants and SVG crosshatch pattern in favor of a single base-only recipe with a CSS repeating-linear-gradient hatch background. Simplifies the component, stories, and tests accordingly. --- .../components/placeholder/Placeholder.vue | 37 +-------- .../preview/PlaceholderSizeGrid.vue | 18 ----- .../stories/components/placeholder.stories.ts | 65 +--------------- .../components/placeholder.styleframe.ts | 29 ------- .../placeholder/usePlaceholderRecipe.test.ts | 77 ++----------------- .../placeholder/usePlaceholderRecipe.ts | 32 ++------ 6 files changed, 16 insertions(+), 242 deletions(-) delete mode 100644 apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue diff --git a/apps/storybook/src/components/components/placeholder/Placeholder.vue b/apps/storybook/src/components/components/placeholder/Placeholder.vue index 4b385b83..8bb796b1 100644 --- a/apps/storybook/src/components/components/placeholder/Placeholder.vue +++ b/apps/storybook/src/components/components/placeholder/Placeholder.vue @@ -1,44 +1,11 @@ diff --git a/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue b/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue deleted file mode 100644 index 46c8e86c..00000000 --- a/apps/storybook/src/components/components/placeholder/preview/PlaceholderSizeGrid.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/apps/storybook/stories/components/placeholder.stories.ts b/apps/storybook/stories/components/placeholder.stories.ts index b8747730..d91418c1 100644 --- a/apps/storybook/stories/components/placeholder.stories.ts +++ b/apps/storybook/stories/components/placeholder.stories.ts @@ -1,9 +1,6 @@ import type { Meta, StoryObj } from "@storybook/vue3-vite"; import Placeholder from "../../src/components/components/placeholder/Placeholder.vue"; -import PlaceholderSizeGrid from "../../src/components/components/placeholder/preview/PlaceholderSizeGrid.vue"; - -const sizes = ["sm", "md", "lg"] as const; const meta = { title: "Theme/Recipes/Placeholder", @@ -12,73 +9,15 @@ const meta = { parameters: { layout: "padded", }, - argTypes: { - size: { - control: "select", - options: sizes, - description: "The size of the placeholder", - }, - }, } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = { - args: { - size: "md", - }, - render: (args) => ({ - components: { Placeholder }, - setup() { - return { args }; - }, - template: 'Placeholder content', - }), -}; - -export const AllSizes: StoryObj = { render: () => ({ - components: { PlaceholderSizeGrid }, - template: "", - }), -}; - -export const Small: Story = { - args: { - size: "sm", - }, - render: (args) => ({ - components: { Placeholder }, - setup() { - return { args }; - }, - template: 'Small placeholder', - }), -}; - -export const Medium: Story = { - args: { - size: "md", - }, - render: (args) => ({ - components: { Placeholder }, - setup() { - return { args }; - }, - template: 'Medium placeholder', - }), -}; - -export const Large: Story = { - args: { - size: "lg", - }, - render: (args) => ({ components: { Placeholder }, - setup() { - return { args }; - }, - template: 'Large placeholder', + template: + 'Placeholder content', }), }; diff --git a/apps/storybook/stories/components/placeholder.styleframe.ts b/apps/storybook/stories/components/placeholder.styleframe.ts index a8cafadf..e98a2612 100644 --- a/apps/storybook/stories/components/placeholder.styleframe.ts +++ b/apps/storybook/stories/components/placeholder.styleframe.ts @@ -2,36 +2,7 @@ import { usePlaceholderRecipe } from "@styleframe/theme"; import { styleframe } from "virtual:styleframe"; const s = styleframe(); -const { selector } = s; export const placeholder = usePlaceholderRecipe(s); -selector(".placeholder-grid", { - display: "flex", - flexWrap: "wrap", - gap: "@spacing.md", - padding: "@spacing.md", - alignItems: "flex-start", -}); - -selector(".placeholder-section", { - display: "flex", - flexDirection: "column", - gap: "@spacing.lg", - padding: "@spacing.md", -}); - -selector(".placeholder-row", { - display: "flex", - flexWrap: "wrap", - gap: "@spacing.sm", - alignItems: "flex-start", -}); - -selector(".placeholder-label", { - fontSize: "@font-size.sm", - fontWeight: "@font-weight.semibold", - minWidth: "80px", -}); - export default s; diff --git a/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts b/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts index eb5125b3..0a1f7d12 100644 --- a/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts +++ b/theme/src/recipes/placeholder/usePlaceholderRecipe.test.ts @@ -13,12 +13,9 @@ function createInstance() { "borderColor", "borderRadius", "overflow", - "position", "opacity", - "paddingTop", - "paddingBottom", - "paddingLeft", - "paddingRight", + "padding", + "backgroundImage", ]) { s.utility(name, ({ value }) => ({ [name]: value })); } @@ -48,58 +45,18 @@ describe("usePlaceholderRecipe", () => { borderColor: "@color.gray-300", borderRadius: "@border-radius.md", overflow: "hidden", - position: "relative", opacity: "0.75", + padding: "@1", + backgroundImage: + "repeating-linear-gradient(-45deg, transparent, transparent 7px, rgba(0, 0, 0, 0.04) 7px, rgba(0, 0, 0, 0.04) 8px)", "&:dark": { borderColor: "@color.gray-600", + backgroundImage: + "repeating-linear-gradient(-45deg, transparent, transparent 7px, rgba(255, 255, 255, 0.04) 7px, rgba(255, 255, 255, 0.04) 8px)", }, }); }); - describe("variants", () => { - it("should have size variants with correct styles", () => { - const s = createInstance(); - const recipe = usePlaceholderRecipe(s); - - expect(recipe.variants!.size).toEqual({ - sm: { - paddingTop: "@0.5", - paddingBottom: "@0.5", - paddingLeft: "@0.5", - paddingRight: "@0.5", - }, - md: { - paddingTop: "@1", - paddingBottom: "@1", - paddingLeft: "@1", - paddingRight: "@1", - }, - lg: { - paddingTop: "@1.5", - paddingBottom: "@1.5", - paddingLeft: "@1.5", - paddingRight: "@1.5", - }, - }); - }); - }); - - it("should have correct default variants", () => { - const s = createInstance(); - const recipe = usePlaceholderRecipe(s); - - expect(recipe.defaultVariants).toEqual({ - size: "md", - }); - }); - - it("should have no compound variants", () => { - const s = createInstance(); - const recipe = usePlaceholderRecipe(s); - - expect(recipe.compoundVariants).toHaveLength(0); - }); - describe("config overrides", () => { it("should allow overriding base styles", () => { const s = createInstance(); @@ -111,24 +68,4 @@ describe("usePlaceholderRecipe", () => { expect(recipe.base!.alignItems).toBe("center"); }); }); - - describe("filter", () => { - it("should filter size variants", () => { - const s = createInstance(); - const recipe = usePlaceholderRecipe(s, { - filter: { size: ["sm", "lg"] }, - }); - - expect(Object.keys(recipe.variants!.size)).toEqual(["sm", "lg"]); - }); - - it("should adjust default variants when filtered out", () => { - const s = createInstance(); - const recipe = usePlaceholderRecipe(s, { - filter: { size: ["sm", "lg"] }, - }); - - expect(recipe.defaultVariants?.size).toBeUndefined(); - }); - }); }); diff --git a/theme/src/recipes/placeholder/usePlaceholderRecipe.ts b/theme/src/recipes/placeholder/usePlaceholderRecipe.ts index 965b0c83..3a153fe8 100644 --- a/theme/src/recipes/placeholder/usePlaceholderRecipe.ts +++ b/theme/src/recipes/placeholder/usePlaceholderRecipe.ts @@ -14,36 +14,14 @@ export const usePlaceholderRecipe = createUseRecipe("placeholder", { borderColor: "@color.gray-300", borderRadius: "@border-radius.md", overflow: "hidden", - position: "relative", opacity: "0.75", + padding: "@1", + backgroundImage: + "repeating-linear-gradient(-45deg, transparent, transparent 7px, rgba(0, 0, 0, 0.04) 7px, rgba(0, 0, 0, 0.04) 8px)", "&:dark": { borderColor: "@color.gray-600", + backgroundImage: + "repeating-linear-gradient(-45deg, transparent, transparent 7px, rgba(255, 255, 255, 0.04) 7px, rgba(255, 255, 255, 0.04) 8px)", }, }, - variants: { - size: { - sm: { - paddingTop: "@0.5", - paddingBottom: "@0.5", - paddingLeft: "@0.5", - paddingRight: "@0.5", - }, - md: { - paddingTop: "@1", - paddingBottom: "@1", - paddingLeft: "@1", - paddingRight: "@1", - }, - lg: { - paddingTop: "@1.5", - paddingBottom: "@1.5", - paddingLeft: "@1.5", - paddingRight: "@1.5", - }, - }, - }, - compoundVariants: [], - defaultVariants: { - size: "md", - }, }); From 83472bdc4e3557d7f53e027367dff91db22a56e0 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 18:02:18 +0700 Subject: [PATCH 3/6] docs: Add Placeholder recipe documentation page --- .../02.composables/09.placeholder.md | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 apps/docs/content/docs/06.components/02.composables/09.placeholder.md diff --git a/apps/docs/content/docs/06.components/02.composables/09.placeholder.md b/apps/docs/content/docs/06.components/02.composables/09.placeholder.md new file mode 100644 index 00000000..009b6555 --- /dev/null +++ b/apps/docs/content/docs/06.components/02.composables/09.placeholder.md @@ -0,0 +1,248 @@ +--- +title: Placeholder +description: A visual placeholder container with a dashed border and hatch pattern for layout prototyping, wireframing, and empty states. Uses the recipe system with dark mode support. +--- + +## Overview + +The **Placeholder** is a visual container element used for layout prototyping, wireframing, and representing areas where content will eventually appear. It uses a single recipe: `usePlaceholderRecipe()` which creates a centered flex container with a dashed border and a subtle hatch background pattern. The recipe has no variant axes — it provides a single, consistent appearance with automatic dark mode support. + +The Placeholder recipe integrates directly with the default [design tokens preset](/docs/design-tokens/presets) and generates type-safe utility classes at build time with zero runtime CSS. + +## Why use the Placeholder recipe? + +The Placeholder recipe helps you: + +- **Prototype layouts quickly**: Drop in a placeholder container to visualize spacing and structure before real content is ready. +- **Communicate intent**: The dashed border and hatch pattern make it visually obvious that the area is a placeholder, not final content. +- **Support dark mode automatically**: The hatch pattern and border color adapt to dark mode without any extra configuration. +- **Customize without forking**: Override base styles through the options API to adjust the appearance for your specific use case. +- **Stay type-safe**: Full TypeScript support with the Styleframe recipe system. +- **Integrate with your tokens**: Every value references the design tokens preset, so theme changes propagate automatically. + +## Usage + +::steps{level="4"} + +#### Register the recipe + +Add the Placeholder recipe to a local Styleframe instance. The global `styleframe.config.ts` provides design tokens and utilities, while the component-level file registers the recipe itself: + +:::code-tree{default-value="src/components/placeholder.styleframe.ts"} + +```ts [src/components/placeholder.styleframe.ts] +import { styleframe } from 'virtual:styleframe'; +import { usePlaceholderRecipe } from '@styleframe/theme'; + +const s = styleframe(); + +const placeholder = usePlaceholderRecipe(s); + +export default s; +``` + +```ts [styleframe.config.ts] +import { styleframe } from 'styleframe'; +import { useDesignTokensPreset, useUtilitiesPreset } from '@styleframe/theme'; + +const s = styleframe(); + +useDesignTokensPreset(s); +useUtilitiesPreset(s); + +export default s; +``` + +::: + +#### Build the component + +Import the `placeholder` runtime function from the virtual module and call it with no arguments to compute the class name: + +:::tabs +::::tabs-item{icon="i-devicon-react" label="React"} + +```ts [src/components/Placeholder.tsx] +import { placeholder } from "virtual:styleframe"; + +interface PlaceholderProps { + children?: React.ReactNode; + className?: string; +} + +export function Placeholder({ children, className }: PlaceholderProps) { + return ( +
+ {children} +
+ ); +} +``` + +:::: +::::tabs-item{icon="i-devicon-vuejs" label="Vue"} + +```vue [src/components/Placeholder.vue] + + + +``` + +:::: +::: + +#### See it in action + +:::story-preview +--- +story: theme-recipes-placeholder--default +panel: true +--- +::: + +:: + +## Appearance + +The Placeholder recipe has a single, fixed appearance with no color, variant, or size axes. It renders as a centered flex container with: + +- A **dashed border** using `@color.gray-300` (light mode) or `@color.gray-600` (dark mode) +- A **hatch background pattern** created with a repeating 45° linear gradient +- **Medium border radius** (`@border-radius.md`) +- **Reduced opacity** (0.75) to visually distinguish it from real content +- **One unit of padding** (`@1`) + +The hatch pattern uses semi-transparent diagonal lines (`rgba(0, 0, 0, 0.04)` in light mode, `rgba(255, 255, 255, 0.04)` in dark mode) so it works on any background color. + +::note +**Good to know:** The Placeholder recipe does not include size variants. To control dimensions, apply size utilities or inline styles directly on the element (e.g., `class="_height:8"` or `style="height: 200px"`). +:: + +## Anatomy + +The Placeholder recipe is a single recipe with no sub-parts: + +| Part | Recipe | Role | +|------|--------|------| +| **Container** | `usePlaceholderRecipe()` | Centered flex container with dashed border and hatch pattern | + +```html + +
Placeholder content
+``` + +::tip +**Pro tip:** Since the Placeholder has no inherent dimensions, set a height or width on the element to define the space it occupies. This makes it useful for previewing different layout configurations. +:: + +## Accessibility + +- **Decorative element.** Placeholders are visual aids for development and prototyping. If they appear in production, ensure they have `aria-hidden="true"` or are replaced with meaningful content before shipping. +- **Verify contrast ratios.** The default opacity of 0.75 combined with the dashed border provides sufficient visual distinction. If you override the opacity or colors, verify the placeholder remains distinguishable from surrounding content. + +## Customization + +### Overriding Defaults + +The composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change: + +```ts [src/components/placeholder.styleframe.ts] +import { styleframe } from 'virtual:styleframe'; +import { usePlaceholderRecipe } from '@styleframe/theme'; + +const s = styleframe(); + +const placeholder = usePlaceholderRecipe(s, { + base: { + borderRadius: '@border-radius.lg', + borderColor: '@color.gray-400', + opacity: '0.5', + }, +}); + +export default s; +``` + +## API Reference + +### `usePlaceholderRecipe(s, options?)` + +Creates the placeholder container recipe with a dashed border, hatch background pattern, and centered flex layout. + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `s` | `Styleframe` | The Styleframe instance | +| `options` | `DeepPartial` | Optional overrides for the recipe configuration | +| `options.base` | `VariantDeclarationsBlock` | Custom base styles for the placeholder container | + +**Variants:** + +The Placeholder recipe has no variant axes. The runtime function is called with no arguments: + +```ts +const classes = placeholder(); +``` + +**Base styles:** + +| Property | Value | Description | +|----------|-------|-------------| +| `display` | `flex` | Flex container | +| `alignItems` | `center` | Vertically center content | +| `justifyContent` | `center` | Horizontally center content | +| `borderWidth` | `@border-width.thin` | Thin border | +| `borderStyle` | `@border-style.dashed` | Dashed border style | +| `borderColor` | `@color.gray-300` | Light gray border (light mode) | +| `borderRadius` | `@border-radius.md` | Medium border radius | +| `overflow` | `hidden` | Clip overflowing content | +| `opacity` | `0.75` | Slightly transparent | +| `padding` | `@1` | One spacing unit of padding | +| `backgroundImage` | Crosshatch pattern | 45° repeating linear gradients | + +**Dark mode overrides:** + +| Property | Value | Description | +|----------|-------|-------------| +| `borderColor` | `@color.gray-600` | Lighter gray border for dark backgrounds | +| `backgroundImage` | Crosshatch pattern (white) | Same pattern with `rgba(255, 255, 255, 0.04)` stripes | + +[Learn more about recipes →](/docs/api/recipes) + +## Best Practices + +- **Set explicit dimensions**: The Placeholder has no inherent width or height. Always set dimensions using utility classes or inline styles so the placeholder represents the space it's standing in for. +- **Use for prototyping, not production**: Placeholders are development aids. Replace them with real content or meaningful empty states before shipping to users. +- **Override base styles for branding**: If your design system uses a different placeholder convention (e.g., dotted borders, different colors), override the `base` styles in the recipe options rather than wrapping with extra CSS. +- **Combine with text labels**: Add descriptive text inside the placeholder (e.g., "Hero Image" or "Sidebar Widget") to communicate what content belongs in each area. + +## FAQ + +::accordion + +:::accordion-item{label="Why doesn't the Placeholder recipe have size or color variants?" icon="i-lucide-circle-help"} +Placeholders are visual aids with a single, recognizable appearance. Adding variants would complicate a component whose purpose is to be a simple stand-in. Control dimensions through utility classes or inline styles, and override colors through the `base` option if needed. +::: + +:::accordion-item{label="How do I control the size of a placeholder?" icon="i-lucide-circle-help"} +Apply size utilities directly on the element. For example, use `class="_height:8 _width:full"` to set the height to 8 spacing units and the width to 100%. You can also use inline styles for specific pixel or percentage values. +::: + +:::accordion-item{label="Can I use the Placeholder recipe without the design tokens preset?" icon="i-lucide-circle-help"} +The Placeholder recipe references design tokens like `@color.gray-300`, `@border-radius.md`, and `@border-width.thin` through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use `useDesignTokensPreset(s)`, but you can also define the required tokens manually. +::: + +:::accordion-item{label="How does the hatch pattern work?" icon="i-lucide-circle-help"} +The pattern is created with a single `repeating-linear-gradient` at 45°. The gradient alternates between transparent and a semi-transparent stripe (`rgba(0, 0, 0, 0.04)` in light mode, `rgba(255, 255, 255, 0.04)` in dark mode), producing diagonal lines across the container. +::: + +:: From f54529234dad93d9236528007f78b6f236d009b9 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 20:29:41 +0700 Subject: [PATCH 4/6] chore: Add write-docs Claude skill for documentation generation --- .claude/skills/write-docs/SKILL.md | 113 +++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .claude/skills/write-docs/SKILL.md diff --git a/.claude/skills/write-docs/SKILL.md b/.claude/skills/write-docs/SKILL.md new file mode 100644 index 00000000..06a3266f --- /dev/null +++ b/.claude/skills/write-docs/SKILL.md @@ -0,0 +1,113 @@ +--- +name: write-docs +description: Generate a new documentation page by studying existing doc conventions and applying them to undocumented source code. Asks the user for reference pages, source code, and task description, then produces a conformant doc page in 4 phases. +--- + +# Write Docs + +Generate a documentation page that matches the style and structure of existing docs, grounded 100% in source code. + +## Instructions + +You are a senior technical writer with 15+ years of experience at developer-facing companies (Stripe, Vercel, Supabase). You specialize in producing documentation that developers can scan, understand, and act on in under 60 seconds. You generate new documentation pages by studying the conventions of an existing docs site and applying them consistently to undocumented code. You have deep expertise in reading source code to extract accurate behavior, parameters, return values, and edge cases — and you never invent functionality that isn't present in the code. + +### Step 1: Gather Inputs + +Before starting, ask the user for three inputs using AskUserQuestion or by requesting them directly: + +1. **Reference pages** — 1-3 existing documentation pages from the same docs site. These define the voice, structure, formatting conventions, and depth to follow. Ask: "Which existing doc pages should I use as reference? Provide file paths or paste the content." +2. **Source code** — The code that needs to be documented. This is the single source of truth for technical accuracy. Ask: "What source code should I document? Provide file paths or paste the content." +3. **Task** — Any specific instructions for the documentation page (optional, defaults to "Generate a documentation page for the provided source code"). Ask: "Any specific task instructions? (e.g., 'Focus on the public API only', 'Include migration guide from v1')" + +Read all referenced files before proceeding. + +### Step 2: Execute the 4-Phase Process + +Work through these phases in order. Output each phase in its own tagged block. + +**Phase 1: Pattern Extraction** — `` + +Analyze the reference pages and extract the documentation conventions. Identify: + +- Page structure (what sections appear, in what order) +- Heading hierarchy and naming style (e.g., verb-first like "Create a widget" vs. noun-first like "Widget creation") +- How parameters/props/options are documented (table, list, inline) +- Code example conventions (language, length, comments, imports shown or hidden) +- Tone and voice markers (second-person? imperative? level of formality?) +- Use of admonitions/callouts (notes, warnings, tips — and their placement) +- How links, cross-references, and prerequisites are handled +- Any frontmatter, metadata, or structural boilerplate + +Present this as a concise bullet list. This becomes the style guide for Phase 3. + +**Phase 2: Code Analysis** — `` + +Read the source code thoroughly and extract: + +- **Purpose**: What does this code do? What problem does it solve? +- **Public API surface**: Every exported function, class, method, component, hook, type, or constant that a consumer would interact with. +- **For each public API item**: + - Signature (parameters, types, defaults) + - Return value / emitted output + - Side effects (network calls, state mutations, file I/O, event emissions) + - Error cases and thrown exceptions + - Required vs. optional parameters + - Constraints or validations applied to inputs +- **Dependencies / prerequisites**: What must be installed, configured, or imported for this to work? +- **Edge cases**: Boundary conditions, null handling, empty states, concurrency concerns. +- **Relationships**: How does this code connect to other parts of the system (if apparent from imports or type references)? + +Flag anything unclear or ambiguous with `[VERIFY: brief description of uncertainty]`. +Do NOT infer behavior that isn't evident in the code. If the code doesn't show what happens on error, don't invent an error message — flag it. + +**Phase 3: Documentation Page** — `` + +Using the conventions from Phase 1 and the technical facts from Phase 2, write the complete documentation page. Follow these rules: + +- **Match the reference pattern exactly** — same section order, heading style, formatting conventions, and depth of detail. The new page should be indistinguishable in style from the reference pages. +- **Lead with purpose** — Start with a 1-2 sentence description of what this does and when a developer would use it. +- **Show prerequisites before usage** — Dependencies, required setup, or configuration come before the first code example. +- **Every non-trivial concept gets a code example** — Examples must be: + - Syntactically correct and runnable as-is + - Derived ONLY from the actual code behavior (no invented APIs) + - Annotated with inline comments for non-obvious lines + - Consistent with the language/framework conventions in the reference pages +- **Document every public API item** using the same format as the reference pages (table, list, or whatever pattern identified in Phase 1). +- **Include edge cases and error handling** as a dedicated section or inline callouts, matching reference page conventions. +- **No filler** — Remove "simply," "just," "obviously," "it should be noted that," "as you can see." +- **Preserve all `[VERIFY]` flags** from Phase 2 so the author can review them. + +**Phase 4: Conformance Check** — `` + +Compare the generated page against the reference pages and list: + +- Any structural deviations (missing sections, reordered sections, different heading style) +- Any formatting inconsistencies (different parameter documentation style, code block conventions) +- Any `[VERIFY]` flags that remain and need human review +- A confidence rating (High / Medium / Low) for the overall accuracy of the page based on code clarity. + +### Step 3: Quality Checklist + +Before finalizing, verify against these criteria: + +- [ ] Page structure matches reference pages section-for-section +- [ ] Heading style matches reference convention (verb-first vs. noun-first, capitalization) +- [ ] Every documented behavior is traceable to a specific part of the source code +- [ ] No parameters, return values, or behaviors were invented beyond what the code shows +- [ ] All code examples are syntactically correct and use real API surfaces from the source +- [ ] Prerequisites and warnings appear BEFORE the steps they apply to +- [ ] Technical terms are defined on first use (or linked, matching reference convention) +- [ ] All `[VERIFY]` flags are preserved for human review +- [ ] Tone and voice match the reference pages + +### Step 4: Write the File + +Write the final documentation page to the appropriate location in the project. Ask the user where to save if the location is not obvious from context. + +## Constraints + +- **Source code is the single source of truth.** Do not infer, assume, or invent any behavior, parameter, return value, or error not present in the code. +- **Reference pages define the form.** Do not introduce structural or stylistic patterns not present in the reference pages (e.g., don't add a "Changelog" section if references don't have one). +- **Flag uncertainty, don't fill gaps.** Use `[VERIFY: ...]` for anything ambiguous rather than guessing. It is better to leave a gap flagged than to document something inaccurate. +- **Maintain the same markup format** as the reference pages (Markdown, MDX, RST, etc.). +- **Do not hallucinate cross-references.** Only link to pages/sections that are either in the reference pages or explicitly imported/referenced in the code. From 599536704a74b4d4ab7091a6cba74a86479fdf3a Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 20:39:45 +0700 Subject: [PATCH 5/6] chore: Add changeset for Placeholder recipe --- .changeset/add-placeholder-recipe.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/add-placeholder-recipe.md diff --git a/.changeset/add-placeholder-recipe.md b/.changeset/add-placeholder-recipe.md new file mode 100644 index 00000000..2597c545 --- /dev/null +++ b/.changeset/add-placeholder-recipe.md @@ -0,0 +1,9 @@ +--- +"@styleframe/theme": minor +"styleframe": minor +--- + +Add Placeholder recipe with dashed border and CSS hatch pattern + +- Add `usePlaceholderRecipe` as a base-only recipe (no variants) with a dashed border, hatch background pattern, and automatic dark mode support +- Add Placeholder documentation page with usage examples, API reference, and FAQ From 17245ebdfc3a9f1b8c0bbaa00307ae5a8c84c70a Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Apr 2026 20:42:36 +0700 Subject: [PATCH 6/6] chore: Update Caliber state and score history --- .caliber/.caliber-state.json | 4 ++++ .caliber/score-history.jsonl | 1 + 2 files changed, 5 insertions(+) create mode 100644 .caliber/.caliber-state.json diff --git a/.caliber/.caliber-state.json b/.caliber/.caliber-state.json new file mode 100644 index 00000000..cb93fd8a --- /dev/null +++ b/.caliber/.caliber-state.json @@ -0,0 +1,4 @@ +{ + "lastRefreshSha": "83472bdc4e3557d7f53e027367dff91db22a56e0", + "lastRefreshTimestamp": "2026-04-08T11:15:52.252Z" +} diff --git a/.caliber/score-history.jsonl b/.caliber/score-history.jsonl index f06cba1c..12dc75c8 100644 --- a/.caliber/score-history.jsonl +++ b/.caliber/score-history.jsonl @@ -6,3 +6,4 @@ {"timestamp":"2026-04-08T05:10:50.424Z","score":84,"grade":"B","targetAgent":["claude","codex"],"trigger":"score"} {"timestamp":"2026-04-08T05:12:21.696Z","score":89,"grade":"A","targetAgent":["claude","codex"],"trigger":"score"} {"timestamp":"2026-04-08T05:26:01.216Z","score":92,"grade":"A","targetAgent":["claude","codex"],"trigger":"score"} +{"timestamp":"2026-04-08T13:41:32.000Z","score":89,"grade":"A","targetAgent":["claude","codex"],"trigger":"score"}