Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/add-tooltip-recipe.md
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions apps/storybook/src/components/components/tooltip/Tooltip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from "vue";
import { tooltip, tooltipArrow } from "virtual:styleframe";

const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
label?: string;
}>(),
{},
);

const classes = computed(() =>
tooltip({
color: props.color,
variant: props.variant,
size: props.size,
}),
);

const arrowClasses = computed(() =>
tooltipArrow({
color: props.color,
variant: props.variant,
}),
);
</script>

<template>
<div class="tooltip-wrapper">
<span :class="classes">
<slot>{{ props.label }}</slot>
</span>
<span :class="[arrowClasses, 'tooltip-arrow-position']" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import Tooltip from "../Tooltip.vue";

const colors = ["light", "dark", "neutral"] as const;
const variants = ["solid", "soft", "subtle"] as const;
</script>

<template>
<div class="tooltip-section">
<div v-for="variant in variants" :key="variant">
<div class="tooltip-label">{{ variant }}</div>
<div class="tooltip-row">
<Tooltip
v-for="color in colors"
:key="`${variant}-${color}`"
:color="color"
:variant="variant"
:label="color"
/>
</div>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import Tooltip from "../Tooltip.vue";

const colors = ["light", "dark", "neutral"] as const;
const sizes = ["sm", "md", "lg"] as const;
</script>

<template>
<div class="tooltip-section">
<div v-for="size in sizes" :key="size">
<div class="tooltip-label">{{ size }}</div>
<div class="tooltip-row">
<Tooltip
v-for="color in colors"
:key="`${size}-${color}`"
:color="color"
:size="size"
:label="color"
/>
</div>
</div>
</div>
</template>
161 changes: 161 additions & 0 deletions apps/storybook/stories/components/tooltip.stories.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Tooltip>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
color: "dark",
variant: "solid",
size: "md",
label: "Tooltip",
},
};

export const AllVariants: StoryObj = {
render: () => ({
components: { TooltipGrid },
template: "<TooltipGrid />",
}),
};

export const AllSizes: StoryObj = {
render: () => ({
components: { TooltipSizeGrid },
template: "<TooltipSizeGrid />",
}),
};

export const Freeform: StoryObj = {
render: (args) => ({
components: { Tooltip },
setup() {
return { args };
},
template: `
<div class="tooltip-row">
<Tooltip v-bind="args">
<div style="max-width: 240px">
<strong>Freeform Tooltip</strong>
<p style="margin: 4px 0 0; font-weight: normal;">Tooltips can contain text of virtually any size. This is an example of a freeform tooltip with rich content.</p>
</div>
</Tooltip>
<Tooltip v-bind="args" color="light">
<div style="max-width: 240px">
<strong>Light Freeform</strong>
<p style="margin: 4px 0 0; font-weight: normal;">This is a light freeform tooltip with <u>formatted</u> text and <strong>bold</strong> content.</p>
</div>
</Tooltip>
</div>
`,
}),
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",
},
};
52 changes: 52 additions & 0 deletions apps/storybook/stories/components/tooltip.styleframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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",
});

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% - @tooltip.arrow.size)",
});

export default s;
20 changes: 19 additions & 1 deletion engine/core/src/tokens/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
};
}
}

// No variable found — fall back to first namespace as default
Expand Down Expand Up @@ -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<string>;
} else {
value = key;
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions theme/src/recipes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./callout";
export * from "./card";
export * from "./modal";
export * from "./nav";
export * from "./tooltip";
1 change: 1 addition & 0 deletions theme/src/recipes/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useTooltipRecipe";
Loading
Loading