From f94cece3b52f5400c18a0cc118ecee0bd710e36e Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Sun, 5 Apr 2026 11:36:46 +0700 Subject: [PATCH 1/7] feat: Add Tooltip recipe with arrow sub-recipe and Storybook stories Add tooltip content bubble and arrow recipes with light/dark/neutral colors, solid/soft/subtle variants, and sm/md/lg sizes. Includes freeform slot support for rich HTML content, preview grids, and tests. --- .../components/components/tooltip/Tooltip.vue | 28 ++ .../tooltip/preview/TooltipGrid.vue | 23 + .../tooltip/preview/TooltipSizeGrid.vue | 23 + .../stories/components/tooltip.stories.ts | 161 ++++++ .../stories/components/tooltip.styleframe.ts | 40 ++ theme/src/recipes/index.ts | 1 + theme/src/recipes/tooltip/index.ts | 1 + .../recipes/tooltip/useTooltipRecipe.test.ts | 460 ++++++++++++++++++ theme/src/recipes/tooltip/useTooltipRecipe.ts | 317 ++++++++++++ 9 files changed, 1054 insertions(+) create mode 100644 apps/storybook/src/components/components/tooltip/Tooltip.vue create mode 100644 apps/storybook/src/components/components/tooltip/preview/TooltipGrid.vue create mode 100644 apps/storybook/src/components/components/tooltip/preview/TooltipSizeGrid.vue create mode 100644 apps/storybook/stories/components/tooltip.stories.ts create mode 100644 apps/storybook/stories/components/tooltip.styleframe.ts create mode 100644 theme/src/recipes/tooltip/index.ts create mode 100644 theme/src/recipes/tooltip/useTooltipRecipe.test.ts create mode 100644 theme/src/recipes/tooltip/useTooltipRecipe.ts diff --git a/apps/storybook/src/components/components/tooltip/Tooltip.vue b/apps/storybook/src/components/components/tooltip/Tooltip.vue new file mode 100644 index 0000000..fede121 --- /dev/null +++ b/apps/storybook/src/components/components/tooltip/Tooltip.vue @@ -0,0 +1,28 @@ + + + diff --git a/apps/storybook/src/components/components/tooltip/preview/TooltipGrid.vue b/apps/storybook/src/components/components/tooltip/preview/TooltipGrid.vue new file mode 100644 index 0000000..c29a493 --- /dev/null +++ b/apps/storybook/src/components/components/tooltip/preview/TooltipGrid.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/storybook/src/components/components/tooltip/preview/TooltipSizeGrid.vue b/apps/storybook/src/components/components/tooltip/preview/TooltipSizeGrid.vue new file mode 100644 index 0000000..3d51fbc --- /dev/null +++ b/apps/storybook/src/components/components/tooltip/preview/TooltipSizeGrid.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/storybook/stories/components/tooltip.stories.ts b/apps/storybook/stories/components/tooltip.stories.ts new file mode 100644 index 0000000..42e9a8a --- /dev/null +++ b/apps/storybook/stories/components/tooltip.stories.ts @@ -0,0 +1,161 @@ +import type { Meta, StoryObj } from "@storybook/vue3-vite"; + +import Tooltip from "@/components/components/tooltip/Tooltip.vue"; +import TooltipGrid from "@/components/components/tooltip/preview/TooltipGrid.vue"; +import TooltipSizeGrid from "@/components/components/tooltip/preview/TooltipSizeGrid.vue"; + +const colors = ["light", "dark", "neutral"] as const; +const variants = ["solid", "soft", "subtle"] as const; +const sizes = ["sm", "md", "lg"] as const; + +const meta = { + title: "Theme/Recipes/Tooltip", + component: Tooltip, + tags: ["autodocs"], + parameters: { + layout: "padded", + }, + argTypes: { + color: { + control: "select", + options: colors, + description: "The color variant of the tooltip", + }, + variant: { + control: "select", + options: variants, + description: "The visual style variant", + }, + size: { + control: "select", + options: sizes, + description: "The size of the tooltip", + }, + label: { + control: "text", + description: "The text content of the tooltip", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + color: "dark", + variant: "solid", + size: "md", + label: "Tooltip", + }, +}; + +export const AllVariants: StoryObj = { + render: () => ({ + components: { TooltipGrid }, + template: "", + }), +}; + +export const AllSizes: StoryObj = { + render: () => ({ + components: { TooltipSizeGrid }, + template: "", + }), +}; + +export const Freeform: StoryObj = { + render: (args) => ({ + components: { Tooltip }, + setup() { + return { args }; + }, + template: ` +
+ +
+ Freeform Tooltip +

Tooltips can contain text of virtually any size. This is an example of a freeform tooltip with rich content.

+
+
+ +
+ Light Freeform +

This is a light freeform tooltip with formatted text and bold content.

+
+
+
+ `, + }), + args: { + color: "dark", + variant: "solid", + size: "md", + }, +}; + +// Individual color stories +export const Light: Story = { + args: { + color: "light", + label: "Light Tooltip", + }, +}; + +export const Dark: Story = { + args: { + color: "dark", + label: "Dark Tooltip", + }, +}; + +export const Neutral: Story = { + args: { + color: "neutral", + label: "Neutral Tooltip", + }, +}; + +// Variant stories +export const Solid: Story = { + args: { + variant: "solid", + label: "Solid Tooltip", + }, +}; + +export const Soft: Story = { + args: { + variant: "soft", + label: "Soft Tooltip", + }, +}; + +export const Subtle: Story = { + args: { + variant: "subtle", + label: "Subtle Tooltip", + }, +}; + +// Size stories +export const Small: Story = { + args: { + size: "sm", + label: "Small Tooltip", + }, +}; + +export const Medium: Story = { + args: { + size: "md", + label: "Medium Tooltip", + }, +}; + +export const Large: Story = { + args: { + size: "lg", + label: "Large Tooltip", + }, +}; diff --git a/apps/storybook/stories/components/tooltip.styleframe.ts b/apps/storybook/stories/components/tooltip.styleframe.ts new file mode 100644 index 0000000..177880e --- /dev/null +++ b/apps/storybook/stories/components/tooltip.styleframe.ts @@ -0,0 +1,40 @@ +import { useTooltipRecipe, useTooltipArrowRecipe } from "@styleframe/theme"; +import { styleframe } from "virtual:styleframe"; + +const s = styleframe(); +const { selector } = s; + +// Initialize tooltip recipes +export const tooltip = useTooltipRecipe(s); +export const tooltipArrow = useTooltipArrowRecipe(s); + +// Container styles for story layout +selector(".tooltip-grid", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.md", + padding: "@spacing.md", + alignItems: "center", +}); + +selector(".tooltip-section", { + display: "flex", + flexDirection: "column", + gap: "@spacing.lg", + padding: "@spacing.md", +}); + +selector(".tooltip-row", { + display: "flex", + flexWrap: "wrap", + gap: "@spacing.sm", + alignItems: "center", +}); + +selector(".tooltip-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 7bd04bf..0446ad1 100644 --- a/theme/src/recipes/index.ts +++ b/theme/src/recipes/index.ts @@ -3,3 +3,4 @@ export * from "./button"; export * from "./button-group"; export * from "./callout"; export * from "./card"; +export * from "./tooltip"; diff --git a/theme/src/recipes/tooltip/index.ts b/theme/src/recipes/tooltip/index.ts new file mode 100644 index 0000000..0f7b679 --- /dev/null +++ b/theme/src/recipes/tooltip/index.ts @@ -0,0 +1 @@ +export * from "./useTooltipRecipe"; diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.test.ts b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts new file mode 100644 index 0000000..990c1c2 --- /dev/null +++ b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts @@ -0,0 +1,460 @@ +import { styleframe } from "@styleframe/core"; +import { useDarkModifier } from "../../modifiers/useMediaPreferenceModifiers"; +import { useTooltipRecipe, useTooltipArrowRecipe } from "./useTooltipRecipe"; + +function createInstance() { + const s = styleframe(); + for (const name of [ + "display", + "alignItems", + "borderWidth", + "borderStyle", + "borderColor", + "fontWeight", + "fontSize", + "lineHeight", + "paddingTop", + "paddingBottom", + "paddingLeft", + "paddingRight", + "borderRadius", + "boxShadow", + "zIndex", + "maxWidth", + "background", + "color", + "width", + "height", + ]) { + s.utility(name, ({ value }) => ({ [name]: value })); + } + useDarkModifier(s); + return s; +} + +describe("useTooltipRecipe", () => { + it("should create a recipe with correct metadata", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(recipe.type).toBe("recipe"); + expect(recipe.name).toBe("tooltip"); + }); + + it("should have correct base styles", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(recipe.base).toEqual({ + display: "inline-flex", + alignItems: "center", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "transparent", + fontWeight: "@font-weight.medium", + fontSize: "@font-size.sm", + lineHeight: "@line-height.normal", + paddingTop: "@0.375", + paddingBottom: "@0.375", + paddingLeft: "@0.625", + paddingRight: "@0.625", + borderRadius: "@border-radius.md", + boxShadow: "@box-shadow.sm", + zIndex: "@z-index.popover", + maxWidth: "240px", + }); + }); + + describe("variants", () => { + it("should have all color variants", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(Object.keys(recipe.variants!.color)).toEqual([ + "light", + "dark", + "neutral", + ]); + }); + + it("should have all style variants", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(Object.keys(recipe.variants!.variant)).toEqual([ + "solid", + "soft", + "subtle", + ]); + }); + + it("should have size variants with correct styles", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(recipe.variants!.size).toEqual({ + sm: { + fontSize: "@font-size.xs", + paddingTop: "@0.25", + paddingBottom: "@0.25", + paddingLeft: "@0.5", + paddingRight: "@0.5", + borderRadius: "@border-radius.sm", + }, + md: { + fontSize: "@font-size.sm", + paddingTop: "@0.375", + paddingBottom: "@0.375", + paddingLeft: "@0.625", + paddingRight: "@0.625", + borderRadius: "@border-radius.md", + }, + lg: { + fontSize: "@font-size.md", + paddingTop: "@0.5", + paddingBottom: "@0.5", + paddingLeft: "@0.75", + paddingRight: "@0.75", + borderRadius: "@border-radius.md", + }, + }); + }); + }); + + it("should have correct default variants", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + expect(recipe.defaultVariants).toEqual({ + color: "dark", + variant: "solid", + size: "md", + }); + }); + + describe("compound variants", () => { + it("should have 9 compound variants total", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + // 3 colors × 3 variants = 9 + expect(recipe.compoundVariants).toHaveLength(9); + }); + + it("should have correct dark/solid compound variant", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + const darkSolid = recipe.compoundVariants!.find( + (cv) => cv.match.color === "dark" && cv.match.variant === "solid", + ); + + expect(darkSolid).toEqual({ + match: { color: "dark", variant: "solid" }, + css: { + background: "@color.gray-900", + color: "@color.text-inverted", + borderColor: "@color.gray-800", + "&:dark": { + background: "@color.gray-900", + color: "@color.text", + borderColor: "@color.gray-800", + }, + }, + }); + }); + + it("should have correct light/solid compound variant", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + const lightSolid = recipe.compoundVariants!.find( + (cv) => cv.match.color === "light" && cv.match.variant === "solid", + ); + + expect(lightSolid).toEqual({ + match: { color: "light", variant: "solid" }, + css: { + background: "@color.white", + color: "@color.text", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.white", + color: "@color.text-inverted", + borderColor: "@color.gray-200", + }, + }, + }); + }); + + it("should have correct neutral/solid compound variant with adaptive dark mode", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + const neutralSolid = recipe.compoundVariants!.find( + (cv) => cv.match.color === "neutral" && cv.match.variant === "solid", + ); + + expect(neutralSolid).toEqual({ + match: { color: "neutral", variant: "solid" }, + css: { + background: "@color.white", + color: "@color.text", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.gray-900", + color: "@color.white", + borderColor: "@color.gray-800", + }, + }, + }); + }); + + it("should have correct neutral/subtle compound variant with adaptive dark mode", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s); + + const neutralSubtle = recipe.compoundVariants!.find( + (cv) => cv.match.color === "neutral" && cv.match.variant === "subtle", + ); + + expect(neutralSubtle).toEqual({ + match: { color: "neutral", variant: "subtle" }, + css: { + background: "@color.gray-100", + color: "@color.gray-700", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-800", + color: "@color.gray-300", + borderColor: "@color.gray-600", + }, + }, + }); + }); + }); + + describe("config overrides", () => { + it("should allow overriding base styles", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s, { + base: { maxWidth: "320px" }, + }); + + expect(recipe.base!.maxWidth).toBe("320px"); + expect(recipe.base!.display).toBe("inline-flex"); + }); + }); + + describe("filter", () => { + it("should filter color variants", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s, { + filter: { color: ["dark", "light"] }, + }); + + expect(Object.keys(recipe.variants!.color)).toEqual(["light", "dark"]); + }); + + it("should prune compound variants when filtering colors", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s, { + filter: { color: ["dark"] }, + }); + + expect( + recipe.compoundVariants!.every((cv) => cv.match.color === "dark"), + ).toBe(true); + expect(recipe.compoundVariants).toHaveLength(3); + }); + + it("should filter variant axis", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s, { + filter: { variant: ["solid", "soft"] }, + }); + + expect(Object.keys(recipe.variants!.variant)).toEqual(["solid", "soft"]); + expect( + recipe.compoundVariants!.every( + (cv) => cv.match.variant === "solid" || cv.match.variant === "soft", + ), + ).toBe(true); + }); + + it("should adjust default variants when filtered out", () => { + const s = createInstance(); + const recipe = useTooltipRecipe(s, { + filter: { color: ["light"] }, + }); + + expect(recipe.defaultVariants?.color).toBeUndefined(); + expect(recipe.defaultVariants?.variant).toBe("solid"); + expect(recipe.defaultVariants?.size).toBe("md"); + }); + }); +}); + +describe("useTooltipArrowRecipe", () => { + it("should create a recipe with correct metadata", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + expect(recipe.type).toBe("recipe"); + expect(recipe.name).toBe("tooltip-arrow"); + }); + + it("should have correct base styles", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + expect(recipe.base).toEqual({ + width: "8px", + height: "8px", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "transparent", + }); + }); + + describe("variants", () => { + it("should have all color variants", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + expect(Object.keys(recipe.variants!.color)).toEqual([ + "light", + "dark", + "neutral", + ]); + }); + + it("should have all style variants", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + expect(Object.keys(recipe.variants!.variant)).toEqual([ + "solid", + "soft", + "subtle", + ]); + }); + }); + + it("should have correct default variants", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + expect(recipe.defaultVariants).toEqual({ + color: "dark", + variant: "solid", + }); + }); + + describe("compound variants", () => { + it("should have 9 compound variants total", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + // 3 colors × 3 variants = 9 + expect(recipe.compoundVariants).toHaveLength(9); + }); + + it("should have correct dark/solid compound variant", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + const darkSolid = recipe.compoundVariants!.find( + (cv) => cv.match.color === "dark" && cv.match.variant === "solid", + ); + + expect(darkSolid).toEqual({ + match: { color: "dark", variant: "solid" }, + css: { + background: "@color.gray-900", + borderColor: "@color.gray-800", + "&:dark": { + background: "@color.gray-900", + borderColor: "@color.gray-800", + }, + }, + }); + }); + + it("should have correct neutral/solid compound variant with adaptive dark mode", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + const neutralSolid = recipe.compoundVariants!.find( + (cv) => cv.match.color === "neutral" && cv.match.variant === "solid", + ); + + expect(neutralSolid).toEqual({ + match: { color: "neutral", variant: "solid" }, + css: { + background: "@color.white", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.gray-900", + borderColor: "@color.gray-800", + }, + }, + }); + }); + + it("should have correct neutral/subtle compound variant", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s); + + const neutralSubtle = recipe.compoundVariants!.find( + (cv) => cv.match.color === "neutral" && cv.match.variant === "subtle", + ); + + expect(neutralSubtle).toEqual({ + match: { color: "neutral", variant: "subtle" }, + css: { + background: "@color.gray-100", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-800", + borderColor: "@color.gray-600", + }, + }, + }); + }); + }); + + describe("config overrides", () => { + it("should allow overriding base styles", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s, { + base: { width: "12px" }, + }); + + expect(recipe.base!.width).toBe("12px"); + expect(recipe.base!.height).toBe("8px"); + }); + }); + + describe("filter", () => { + it("should filter color variants", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s, { + filter: { color: ["dark"] }, + }); + + expect(Object.keys(recipe.variants!.color)).toEqual(["dark"]); + expect(recipe.compoundVariants).toHaveLength(3); + }); + + it("should adjust default variants when filtered out", () => { + const s = createInstance(); + const recipe = useTooltipArrowRecipe(s, { + filter: { color: ["light"] }, + }); + + expect(recipe.defaultVariants?.color).toBeUndefined(); + expect(recipe.defaultVariants?.variant).toBe("solid"); + }); + }); +}); diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.ts b/theme/src/recipes/tooltip/useTooltipRecipe.ts new file mode 100644 index 0000000..8f51772 --- /dev/null +++ b/theme/src/recipes/tooltip/useTooltipRecipe.ts @@ -0,0 +1,317 @@ +import { createUseRecipe } from "../../utils/createUseRecipe"; + +/** + * Tooltip content bubble recipe. + * Supports color (light, dark, neutral), variant, and size axes. + */ +export const useTooltipRecipe = createUseRecipe("tooltip", { + base: { + display: "inline-flex", + alignItems: "center", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "transparent", + fontWeight: "@font-weight.medium", + fontSize: "@font-size.sm", + lineHeight: "@line-height.normal", + paddingTop: "@0.375", + paddingBottom: "@0.375", + paddingLeft: "@0.625", + paddingRight: "@0.625", + borderRadius: "@border-radius.md", + boxShadow: "@box-shadow.sm", + zIndex: "@z-index.popover", + maxWidth: "240px", + }, + variants: { + color: { + light: {}, + dark: {}, + neutral: {}, + }, + variant: { + solid: {}, + soft: {}, + subtle: {}, + }, + size: { + sm: { + fontSize: "@font-size.xs", + paddingTop: "@0.25", + paddingBottom: "@0.25", + paddingLeft: "@0.5", + paddingRight: "@0.5", + borderRadius: "@border-radius.sm", + }, + md: { + fontSize: "@font-size.sm", + paddingTop: "@0.375", + paddingBottom: "@0.375", + paddingLeft: "@0.625", + paddingRight: "@0.625", + borderRadius: "@border-radius.md", + }, + lg: { + fontSize: "@font-size.md", + paddingTop: "@0.5", + paddingBottom: "@0.5", + paddingLeft: "@0.75", + paddingRight: "@0.75", + borderRadius: "@border-radius.md", + }, + }, + }, + compoundVariants: [ + // Light color (neutral light-mode values, fixed across themes) + { + match: { color: "light" as const, variant: "solid" as const }, + css: { + background: "@color.white", + color: "@color.text", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.white", + color: "@color.text-inverted", + borderColor: "@color.gray-200", + }, + }, + }, + { + match: { color: "light" as const, variant: "soft" as const }, + css: { + background: "@color.gray-100", + color: "@color.gray-700", + "&:dark": { + background: "@color.gray-100", + color: "@color.gray-700", + }, + }, + }, + { + match: { color: "light" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-100", + color: "@color.gray-700", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-100", + color: "@color.gray-700", + borderColor: "@color.gray-300", + }, + }, + }, + + // Dark color (neutral dark-mode values, fixed across themes) + { + match: { color: "dark" as const, variant: "solid" as const }, + css: { + background: "@color.gray-900", + color: "@color.text-inverted", + borderColor: "@color.gray-800", + "&:dark": { + background: "@color.gray-900", + color: "@color.text", + borderColor: "@color.gray-800", + }, + }, + }, + { + match: { color: "dark" as const, variant: "soft" as const }, + css: { + background: "@color.gray-800", + color: "@color.gray-300", + "&:dark": { + background: "@color.gray-800", + color: "@color.gray-300", + }, + }, + }, + { + match: { color: "dark" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-800", + color: "@color.gray-300", + borderColor: "@color.gray-600", + "&:dark": { + background: "@color.gray-800", + color: "@color.gray-300", + borderColor: "@color.gray-600", + }, + }, + }, + + // Neutral color (adaptive: light in light mode, dark in dark mode) + { + match: { color: "neutral" as const, variant: "solid" as const }, + css: { + background: "@color.white", + color: "@color.text", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.gray-900", + color: "@color.white", + borderColor: "@color.gray-800", + }, + }, + }, + { + match: { color: "neutral" as const, variant: "soft" as const }, + css: { + background: "@color.gray-100", + color: "@color.gray-700", + "&:dark": { + background: "@color.gray-800", + color: "@color.gray-300", + }, + }, + }, + { + match: { color: "neutral" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-100", + color: "@color.gray-700", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-800", + color: "@color.gray-300", + borderColor: "@color.gray-600", + }, + }, + }, + ], + defaultVariants: { + color: "dark", + variant: "solid", + size: "md", + }, +}); + +/** + * Tooltip arrow recipe. + * Arrow background and border match the tooltip content. + */ +export const useTooltipArrowRecipe = createUseRecipe("tooltip-arrow", { + base: { + width: "8px", + height: "8px", + borderWidth: "@border-width.thin", + borderStyle: "@border-style.solid", + borderColor: "transparent", + }, + variants: { + color: { + light: {}, + dark: {}, + neutral: {}, + }, + variant: { + solid: {}, + soft: {}, + subtle: {}, + }, + }, + compoundVariants: [ + // Light color (fixed across themes) + { + match: { color: "light" as const, variant: "solid" as const }, + css: { + background: "@color.white", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.white", + borderColor: "@color.gray-200", + }, + }, + }, + { + match: { color: "light" as const, variant: "soft" as const }, + css: { + background: "@color.gray-100", + "&:dark": { + background: "@color.gray-100", + }, + }, + }, + { + match: { color: "light" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-100", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-100", + borderColor: "@color.gray-300", + }, + }, + }, + + // Dark color (fixed across themes) + { + match: { color: "dark" as const, variant: "solid" as const }, + css: { + background: "@color.gray-900", + borderColor: "@color.gray-800", + "&:dark": { + background: "@color.gray-900", + borderColor: "@color.gray-800", + }, + }, + }, + { + match: { color: "dark" as const, variant: "soft" as const }, + css: { + background: "@color.gray-800", + "&:dark": { + background: "@color.gray-800", + }, + }, + }, + { + match: { color: "dark" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-800", + borderColor: "@color.gray-600", + "&:dark": { + background: "@color.gray-800", + borderColor: "@color.gray-600", + }, + }, + }, + + // Neutral color (adaptive) + { + match: { color: "neutral" as const, variant: "solid" as const }, + css: { + background: "@color.white", + borderColor: "@color.gray-200", + "&:dark": { + background: "@color.gray-900", + borderColor: "@color.gray-800", + }, + }, + }, + { + match: { color: "neutral" as const, variant: "soft" as const }, + css: { + background: "@color.gray-100", + "&:dark": { + background: "@color.gray-800", + }, + }, + }, + { + match: { color: "neutral" as const, variant: "subtle" as const }, + css: { + background: "@color.gray-100", + borderColor: "@color.gray-300", + "&:dark": { + background: "@color.gray-800", + borderColor: "@color.gray-600", + }, + }, + }, + ], + defaultVariants: { + color: "dark", + variant: "solid", + }, +}); From f253b356dc269d7e3134004809979c68d8600c61 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 6 Apr 2026 12:46:56 +0700 Subject: [PATCH 2/7] feat: Improve Tooltip arrow with CSS border triangle and cross-namespace variable references Refactor tooltip arrow from box-based to CSS border triangle technique with proper color matching via compound variants. Add cross-namespace variable reference support in utility factory and move recipe setup before recipe creation for correct variable registration order. --- .../components/components/tooltip/Tooltip.vue | 18 +- .../stories/components/tooltip.styleframe.ts | 12 + engine/core/src/tokens/utility.ts | 20 +- theme/src/recipes/tooltip/useTooltipRecipe.ts | 275 +++++++++++------- .../transforms/useTransformUtility.ts | 10 + theme/src/utils/createUseRecipe.ts | 8 +- 6 files changed, 232 insertions(+), 111 deletions(-) diff --git a/apps/storybook/src/components/components/tooltip/Tooltip.vue b/apps/storybook/src/components/components/tooltip/Tooltip.vue index fede121..2c5dc13 100644 --- a/apps/storybook/src/components/components/tooltip/Tooltip.vue +++ b/apps/storybook/src/components/components/tooltip/Tooltip.vue @@ -1,6 +1,6 @@ diff --git a/apps/storybook/stories/components/tooltip.styleframe.ts b/apps/storybook/stories/components/tooltip.styleframe.ts index 177880e..3bd5319 100644 --- a/apps/storybook/stories/components/tooltip.styleframe.ts +++ b/apps/storybook/stories/components/tooltip.styleframe.ts @@ -37,4 +37,16 @@ selector(".tooltip-label", { minWidth: "80px", }); +selector(".tooltip-wrapper", { + position: "relative", + display: "inline-flex", + flexDirection: "column", + alignItems: "center", +}); + +selector(".tooltip-arrow-position", { + bottom: "calc(@tooltip.arrow.size * -1)", + left: "calc(50% - 5px)", +}); + export default s; diff --git a/engine/core/src/tokens/utility.ts b/engine/core/src/tokens/utility.ts index eb7791b..ca79ee5 100644 --- a/engine/core/src/tokens/utility.ts +++ b/engine/core/src/tokens/utility.ts @@ -64,6 +64,16 @@ function createNamespaceAutogenerate( }; } } + + // Check if variable exists with its exact name (cross-namespace reference) + if (root.variables.some((v) => v.name === variableName)) { + return { + [variableName]: { + type: "reference", + name: variableName, + } satisfies Reference, + }; + } } // No variable found — fall back to first namespace as default @@ -200,7 +210,15 @@ export function createUtilityFunction(parent: Container, root: Root) { } if (!found) { - value = key; + // Check if variable exists with exact key name (cross-namespace reference) + if (root.variables.some((v) => v.name === key)) { + value = { + type: "reference", + name: key, + } satisfies Reference; + } else { + value = key; + } } } } diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.ts b/theme/src/recipes/tooltip/useTooltipRecipe.ts index 8f51772..763b999 100644 --- a/theme/src/recipes/tooltip/useTooltipRecipe.ts +++ b/theme/src/recipes/tooltip/useTooltipRecipe.ts @@ -20,7 +20,7 @@ export const useTooltipRecipe = createUseRecipe("tooltip", { paddingRight: "@0.625", borderRadius: "@border-radius.md", boxShadow: "@box-shadow.sm", - zIndex: "@z-index.popover", + zIndex: "@z-index.tooltip", maxWidth: "240px", }, variants: { @@ -190,128 +190,199 @@ export const useTooltipRecipe = createUseRecipe("tooltip", { * Tooltip arrow recipe. * Arrow background and border match the tooltip content. */ -export const useTooltipArrowRecipe = createUseRecipe("tooltip-arrow", { - base: { - width: "8px", - height: "8px", - borderWidth: "@border-width.thin", - borderStyle: "@border-style.solid", - borderColor: "transparent", - }, - variants: { - color: { - light: {}, - dark: {}, - neutral: {}, +export const useTooltipArrowRecipe = createUseRecipe( + "tooltip-arrow", + { + base: { + width: "0", + height: "0", + borderLeftWidth: "calc(@tooltip.arrow.size + 1px)", + borderLeftStyle: "@border-style.solid", + borderLeftColor: "transparent", + borderRightWidth: "calc(@tooltip.arrow.size + 1px)", + borderRightStyle: "@border-style.solid", + borderRightColor: "transparent", + borderTopWidth: "calc(@tooltip.arrow.size + 1px)", + borderTopStyle: "@border-style.solid", + borderTopColor: "red", + position: "absolute", + zIndex: "@z-index.tooltip", + "&:after": { + borderLeftWidth: "@tooltip.arrow.size", + borderLeftStyle: "@border-style.solid", + borderLeftColor: "transparent", + borderRightWidth: "@tooltip.arrow.size", + borderRightStyle: "@border-style.solid", + borderRightColor: "transparent", + borderTopWidth: "@tooltip.arrow.size", + borderTopStyle: "@border-style.solid", + borderTopColor: "blue", + position: "absolute", + left: "calc(@tooltip.arrow.size * -1)", + top: "calc(@tooltip.arrow.size * -1 - 1px)", + zIndex: "0", + }, }, - variant: { - solid: {}, - soft: {}, - subtle: {}, + variants: { + color: { + light: {}, + dark: {}, + neutral: {}, + }, + variant: { + solid: {}, + soft: {}, + subtle: {}, + }, }, - }, - compoundVariants: [ - // Light color (fixed across themes) - { - match: { color: "light" as const, variant: "solid" as const }, - css: { - background: "@color.white", - borderColor: "@color.gray-200", - "&:dark": { - background: "@color.white", - borderColor: "@color.gray-200", + compoundVariants: [ + // Light color (fixed across themes) + { + match: { color: "light" as const, variant: "solid" as const }, + css: { + borderTopColor: "@color.gray-200", + "&:after": { + borderTopColor: "@color.white", + }, + "&:dark": { + borderTopColor: "@color.gray-200", + }, + "&:dark:after": { + borderTopColor: "@color.white", + }, }, }, - }, - { - match: { color: "light" as const, variant: "soft" as const }, - css: { - background: "@color.gray-100", - "&:dark": { - background: "@color.gray-100", + { + match: { color: "light" as const, variant: "soft" as const }, + css: { + borderTopColor: "@color.gray-100", + "&:after": { + borderTopColor: "@color.gray-100", + }, + "&:dark": { + borderTopColor: "@color.gray-100", + }, + "&:dark:after": { + borderTopColor: "@color.gray-100", + }, }, }, - }, - { - match: { color: "light" as const, variant: "subtle" as const }, - css: { - background: "@color.gray-100", - borderColor: "@color.gray-300", - "&:dark": { - background: "@color.gray-100", - borderColor: "@color.gray-300", + { + match: { color: "light" as const, variant: "subtle" as const }, + css: { + borderTopColor: "@color.gray-300", + "&:after": { + borderTopColor: "@color.gray-100", + }, + "&:dark": { + borderTopColor: "@color.gray-300", + }, + "&:dark:after": { + borderTopColor: "@color.gray-100", + }, }, }, - }, - // Dark color (fixed across themes) - { - match: { color: "dark" as const, variant: "solid" as const }, - css: { - background: "@color.gray-900", - borderColor: "@color.gray-800", - "&:dark": { - background: "@color.gray-900", - borderColor: "@color.gray-800", + // Dark color (fixed across themes) + { + match: { color: "dark" as const, variant: "solid" as const }, + css: { + borderTopColor: "@color.gray-800", + "&:after": { + borderTopColor: "@color.gray-900", + }, + "&:dark": { + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-900", + }, }, }, - }, - { - match: { color: "dark" as const, variant: "soft" as const }, - css: { - background: "@color.gray-800", - "&:dark": { - background: "@color.gray-800", + { + match: { color: "dark" as const, variant: "soft" as const }, + css: { + borderTopColor: "@color.gray-800", + "&:after": { + borderTopColor: "@color.gray-800", + }, + "&:dark": { + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-800", + }, }, }, - }, - { - match: { color: "dark" as const, variant: "subtle" as const }, - css: { - background: "@color.gray-800", - borderColor: "@color.gray-600", - "&:dark": { - background: "@color.gray-800", - borderColor: "@color.gray-600", + { + match: { color: "dark" as const, variant: "subtle" as const }, + css: { + borderTopColor: "@color.gray-600", + "&:after": { + borderTopColor: "@color.gray-800", + }, + "&:dark": { + borderTopColor: "@color.gray-600", + }, + "&:dark:after": { + borderTopColor: "@color.gray-800", + }, }, }, - }, - // Neutral color (adaptive) - { - match: { color: "neutral" as const, variant: "solid" as const }, - css: { - background: "@color.white", - borderColor: "@color.gray-200", - "&:dark": { - background: "@color.gray-900", - borderColor: "@color.gray-800", + // Neutral color (adaptive) + { + match: { color: "neutral" as const, variant: "solid" as const }, + css: { + borderTopColor: "@color.gray-200", + "&:after": { + borderTopColor: "@color.white", + }, + "&:dark": { + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-900", + }, }, }, - }, - { - match: { color: "neutral" as const, variant: "soft" as const }, - css: { - background: "@color.gray-100", - "&:dark": { - background: "@color.gray-800", + { + match: { color: "neutral" as const, variant: "soft" as const }, + css: { + borderTopColor: "@color.gray-100", + "&:after": { + borderTopColor: "@color.gray-100", + }, + "&:dark": { + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-800", + }, }, }, - }, - { - match: { color: "neutral" as const, variant: "subtle" as const }, - css: { - background: "@color.gray-100", - borderColor: "@color.gray-300", - "&:dark": { - background: "@color.gray-800", - borderColor: "@color.gray-600", + { + match: { color: "neutral" as const, variant: "subtle" as const }, + css: { + borderTopColor: "@color.gray-300", + "&:after": { + borderTopColor: "@color.gray-100", + }, + "&:dark": { + borderTopColor: "@color.gray-600", + }, + "&:dark:after": { + borderTopColor: "@color.gray-800", + }, }, }, + ], + defaultVariants: { + color: "dark", + variant: "solid", }, - ], - defaultVariants: { - color: "dark", - variant: "solid", }, -}); + (s) => { + s.variable("tooltip.arrow.size", "5px"); + }, +); diff --git a/theme/src/utilities/transforms/useTransformUtility.ts b/theme/src/utilities/transforms/useTransformUtility.ts index 1602926..fac4d69 100644 --- a/theme/src/utilities/transforms/useTransformUtility.ts +++ b/theme/src/utilities/transforms/useTransformUtility.ts @@ -181,3 +181,13 @@ export const useTranslateZUtility = createUseUtility( "translate3d(var(--transform-translate-x), var(--transform-translate-y), var(--transform-translate-z)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y))", }), ); + +/** + * Create transform utility classes. + */ +export const useTransformUtility = createUseUtility( + "transform", + ({ value }) => ({ + transform: value, + }), +); diff --git a/theme/src/utils/createUseRecipe.ts b/theme/src/utils/createUseRecipe.ts index 50b8b1e..0cf051b 100644 --- a/theme/src/utils/createUseRecipe.ts +++ b/theme/src/utils/createUseRecipe.ts @@ -116,6 +116,10 @@ export function createUseRecipe< s: Styleframe, options?: DeepPartial> & { filter?: F }, ): Recipe> { + if (setup) { + setup(s); + } + const { filter, ...configOverrides } = options ?? {}; const merged = defu(configOverrides, defaults) as RecipeConfig; const filtered = filter ? applyFilter(merged, filter) : merged; @@ -124,10 +128,6 @@ export function createUseRecipe< ...filtered, }) as Recipe>; - if (setup) { - setup(s); - } - return recipe; }; } From 63c06ca8750c9976e223517e4e207d39351539b8 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 7 Apr 2026 10:47:01 +0700 Subject: [PATCH 3/7] fix: Update tooltip arrow tests for CSS border triangle implementation Update test expectations to match the new border-triangle arrow technique, register missing borderLeft/Right/Top utilities and after modifier in test helper, and fix zIndex reference from popover to tooltip. --- .../recipes/tooltip/useTooltipRecipe.test.ts | 87 +++++++++++++++---- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.test.ts b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts index 990c1c2..1d85d30 100644 --- a/theme/src/recipes/tooltip/useTooltipRecipe.test.ts +++ b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts @@ -1,5 +1,6 @@ import { styleframe } from "@styleframe/core"; import { useDarkModifier } from "../../modifiers/useMediaPreferenceModifiers"; +import { useAfterModifier } from "../../modifiers/usePseudoElementModifiers"; import { useTooltipRecipe, useTooltipArrowRecipe } from "./useTooltipRecipe"; function createInstance() { @@ -25,10 +26,23 @@ function createInstance() { "color", "width", "height", + "borderLeftWidth", + "borderLeftStyle", + "borderLeftColor", + "borderRightWidth", + "borderRightStyle", + "borderRightColor", + "borderTopWidth", + "borderTopStyle", + "borderTopColor", + "position", + "left", + "top", ]) { s.utility(name, ({ value }) => ({ [name]: value })); } useDarkModifier(s); + useAfterModifier(s); return s; } @@ -60,7 +74,7 @@ describe("useTooltipRecipe", () => { paddingRight: "@0.625", borderRadius: "@border-radius.md", boxShadow: "@box-shadow.sm", - zIndex: "@z-index.popover", + zIndex: "@z-index.tooltip", maxWidth: "240px", }); }); @@ -309,11 +323,34 @@ describe("useTooltipArrowRecipe", () => { const recipe = useTooltipArrowRecipe(s); expect(recipe.base).toEqual({ - width: "8px", - height: "8px", - borderWidth: "@border-width.thin", - borderStyle: "@border-style.solid", - borderColor: "transparent", + width: "0", + height: "0", + borderLeftWidth: "calc(@tooltip.arrow.size + 1px)", + borderLeftStyle: "@border-style.solid", + borderLeftColor: "transparent", + borderRightWidth: "calc(@tooltip.arrow.size + 1px)", + borderRightStyle: "@border-style.solid", + borderRightColor: "transparent", + borderTopWidth: "calc(@tooltip.arrow.size + 1px)", + borderTopStyle: "@border-style.solid", + borderTopColor: "red", + position: "absolute", + zIndex: "@z-index.tooltip", + "&:after": { + borderLeftWidth: "@tooltip.arrow.size", + borderLeftStyle: "@border-style.solid", + borderLeftColor: "transparent", + borderRightWidth: "@tooltip.arrow.size", + borderRightStyle: "@border-style.solid", + borderRightColor: "transparent", + borderTopWidth: "@tooltip.arrow.size", + borderTopStyle: "@border-style.solid", + borderTopColor: "blue", + position: "absolute", + left: "calc(@tooltip.arrow.size * -1)", + top: "calc(@tooltip.arrow.size * -1 - 1px)", + zIndex: "0", + }, }); }); @@ -371,11 +408,15 @@ describe("useTooltipArrowRecipe", () => { expect(darkSolid).toEqual({ match: { color: "dark", variant: "solid" }, css: { - background: "@color.gray-900", - borderColor: "@color.gray-800", + borderTopColor: "@color.gray-800", + "&:after": { + borderTopColor: "@color.gray-900", + }, "&:dark": { - background: "@color.gray-900", - borderColor: "@color.gray-800", + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-900", }, }, }); @@ -392,11 +433,15 @@ describe("useTooltipArrowRecipe", () => { expect(neutralSolid).toEqual({ match: { color: "neutral", variant: "solid" }, css: { - background: "@color.white", - borderColor: "@color.gray-200", + borderTopColor: "@color.gray-200", + "&:after": { + borderTopColor: "@color.white", + }, "&:dark": { - background: "@color.gray-900", - borderColor: "@color.gray-800", + borderTopColor: "@color.gray-800", + }, + "&:dark:after": { + borderTopColor: "@color.gray-900", }, }, }); @@ -413,11 +458,15 @@ describe("useTooltipArrowRecipe", () => { expect(neutralSubtle).toEqual({ match: { color: "neutral", variant: "subtle" }, css: { - background: "@color.gray-100", - borderColor: "@color.gray-300", + borderTopColor: "@color.gray-300", + "&:after": { + borderTopColor: "@color.gray-100", + }, "&:dark": { - background: "@color.gray-800", - borderColor: "@color.gray-600", + borderTopColor: "@color.gray-600", + }, + "&:dark:after": { + borderTopColor: "@color.gray-800", }, }, }); @@ -432,7 +481,7 @@ describe("useTooltipArrowRecipe", () => { }); expect(recipe.base!.width).toBe("12px"); - expect(recipe.base!.height).toBe("8px"); + expect(recipe.base!.height).toBe("0"); }); }); From bf1fbb490fbdea50295fbc46dd854dd623198d44 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 7 Apr 2026 10:49:03 +0700 Subject: [PATCH 4/7] fix: Replace hardcoded 5px with @tooltip.arrow.size token reference --- apps/storybook/stories/components/tooltip.styleframe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/storybook/stories/components/tooltip.styleframe.ts b/apps/storybook/stories/components/tooltip.styleframe.ts index 3bd5319..088de6c 100644 --- a/apps/storybook/stories/components/tooltip.styleframe.ts +++ b/apps/storybook/stories/components/tooltip.styleframe.ts @@ -46,7 +46,7 @@ selector(".tooltip-wrapper", { selector(".tooltip-arrow-position", { bottom: "calc(@tooltip.arrow.size * -1)", - left: "calc(50% - 5px)", + left: "calc(50% - @tooltip.arrow.size)", }); export default s; From f4946d4078985dd519557e4d2a41bb2a84928d75 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 7 Apr 2026 10:51:34 +0700 Subject: [PATCH 5/7] fix: Replace debug colors red/blue with transparent in arrow recipe base --- theme/src/recipes/tooltip/useTooltipRecipe.test.ts | 4 ++-- theme/src/recipes/tooltip/useTooltipRecipe.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.test.ts b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts index 1d85d30..e404076 100644 --- a/theme/src/recipes/tooltip/useTooltipRecipe.test.ts +++ b/theme/src/recipes/tooltip/useTooltipRecipe.test.ts @@ -333,7 +333,7 @@ describe("useTooltipArrowRecipe", () => { borderRightColor: "transparent", borderTopWidth: "calc(@tooltip.arrow.size + 1px)", borderTopStyle: "@border-style.solid", - borderTopColor: "red", + borderTopColor: "transparent", position: "absolute", zIndex: "@z-index.tooltip", "&:after": { @@ -345,7 +345,7 @@ describe("useTooltipArrowRecipe", () => { borderRightColor: "transparent", borderTopWidth: "@tooltip.arrow.size", borderTopStyle: "@border-style.solid", - borderTopColor: "blue", + borderTopColor: "transparent", position: "absolute", left: "calc(@tooltip.arrow.size * -1)", top: "calc(@tooltip.arrow.size * -1 - 1px)", diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.ts b/theme/src/recipes/tooltip/useTooltipRecipe.ts index 763b999..bd5f077 100644 --- a/theme/src/recipes/tooltip/useTooltipRecipe.ts +++ b/theme/src/recipes/tooltip/useTooltipRecipe.ts @@ -204,7 +204,7 @@ export const useTooltipArrowRecipe = createUseRecipe( borderRightColor: "transparent", borderTopWidth: "calc(@tooltip.arrow.size + 1px)", borderTopStyle: "@border-style.solid", - borderTopColor: "red", + borderTopColor: "transparent", position: "absolute", zIndex: "@z-index.tooltip", "&:after": { @@ -216,7 +216,7 @@ export const useTooltipArrowRecipe = createUseRecipe( borderRightColor: "transparent", borderTopWidth: "@tooltip.arrow.size", borderTopStyle: "@border-style.solid", - borderTopColor: "blue", + borderTopColor: "transparent", position: "absolute", left: "calc(@tooltip.arrow.size * -1)", top: "calc(@tooltip.arrow.size * -1 - 1px)", From 277578fdb7236b55c67b96b67b912c8874da07ed Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 7 Apr 2026 10:54:14 +0700 Subject: [PATCH 6/7] fix: Add { default: true } to tooltip.arrow.size variable --- theme/src/recipes/tooltip/useTooltipRecipe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/src/recipes/tooltip/useTooltipRecipe.ts b/theme/src/recipes/tooltip/useTooltipRecipe.ts index bd5f077..123b026 100644 --- a/theme/src/recipes/tooltip/useTooltipRecipe.ts +++ b/theme/src/recipes/tooltip/useTooltipRecipe.ts @@ -383,6 +383,6 @@ export const useTooltipArrowRecipe = createUseRecipe( }, }, (s) => { - s.variable("tooltip.arrow.size", "5px"); + s.variable("tooltip.arrow.size", "5px", { default: true }); }, ); From e7e387c40aeccb85efd678c4ab515f5c05d076fd Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 7 Apr 2026 11:19:22 +0700 Subject: [PATCH 7/7] chore: Add changeset for tooltip recipe --- .changeset/add-tooltip-recipe.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/add-tooltip-recipe.md diff --git a/.changeset/add-tooltip-recipe.md b/.changeset/add-tooltip-recipe.md new file mode 100644 index 0000000..e9c9ebe --- /dev/null +++ b/.changeset/add-tooltip-recipe.md @@ -0,0 +1,11 @@ +--- +"@styleframe/theme": minor +"styleframe": minor +--- + +Add Tooltip recipe with arrow sub-recipe and transform utility + +- Add `useTooltipRecipe` with size (`sm`, `md`, `lg`), variant (`solid`, `soft`, `subtle`), and color (`light`, `dark`, `neutral`) variants +- Add `useTooltipArrowRecipe` with CSS border triangle implementation using `@tooltip.arrow.size` variable and `&:after` pseudo-element for border/fill separation +- Add `useTransformUtility` for arbitrary `transform` CSS property values +- Add Tooltip storybook components, grid previews, and stories including freeform rich content example