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
50 changes: 50 additions & 0 deletions resources/js/components/ui/Text.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup>
import { computed, useSlots } from 'vue';
import { cva } from 'cva';
import { twMerge } from 'tailwind-merge';
const props = defineProps({
/** The element this component should render as */
as: { type: String, default: 'span' },
/** Controls the size of the text. Options: `xs`, `sm`, `base`, `lg` */
size: { type: String, default: 'base' },
/** Text to display */
text: { type: [String, Number, Boolean, null], default: null },
/** Controls the appearance of the text. Options: `default`, `strong`, `subtle`, `code`, `danger`, `success`, `warning` */
variant: { type: String, default: 'default' },
});
const slots = useSlots();
const hasDefaultSlot = !!slots.default;
const textClasses = computed(() => {
const classes = cva({
base: 'antialiased',
variants: {
variant: {
default: 'text-gray-900 dark:text-gray-50',
strong: 'font-semibold text-gray-900 dark:text-gray-50',
subtle: 'text-gray-600 dark:text-gray-600/90',
code: 'font-mono text-[0.9em] text-gray-900 dark:text-gray-50 bg-gray-600/10 dark:bg-white/10 rounded-sm px-1 py-0.5',
danger: 'text-red-600 dark:text-red-400',
success: 'text-green-600 dark:text-green-400',
warning: 'text-amber-600 dark:text-amber-400',
},
size: {
sm: 'text-xs',
base: 'text-sm',
lg: 'text-base',
},
},
})({ ...props });
return twMerge(classes);
});
</script>

<template>
<component :is="as" :class="textClasses" data-ui-text>
<slot v-if="hasDefaultSlot" />
<template v-else>{{ text }}</template>
</component>
</template>
1 change: 1 addition & 0 deletions resources/js/components/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export { default as TableRows } from './Table/Rows.vue';
export { default as TabList } from './Tabs/List.vue';
export { default as Tabs } from './Tabs/Tabs.vue';
export { default as TabTrigger } from './Tabs/Trigger.vue';
export { default as Text } from './Text.vue';
export { default as Textarea } from './Textarea.vue';
export { default as TimePicker } from './TimePicker/TimePicker.vue';
export { default as ToggleGroup } from './Toggle/Group.vue';
Expand Down
168 changes: 168 additions & 0 deletions resources/js/stories/Text.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type {Meta, StoryObj} from '@storybook/vue3';
import {Text} from '@ui';
import {computed} from 'vue';

const meta = {
title: 'Components/Text',
component: Text,
argTypes: {
size: {
control: 'select',
options: ['xs', 'sm', 'base', 'lg'],
},
variant: {
control: 'select',
options: ['default', 'strong', 'subtle', 'code', 'danger', 'success', 'warning'],
},
as: {
control: 'select',
options: ['span', 'p', 'div'],
},
},
} satisfies Meta<typeof Text>;

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

export const Default: Story = {
args: {
text: 'The quick brown fox jumps over the lazy dog.',
},
};

const introCode = `
<div class="flex flex-wrap gap-3 items-center">
<Text text="Default" />
<Text variant="strong" text="Strong" />
<Text variant="subtle" text="Subtle" />
<Text variant="code" text="code_example" />
</div>
`;

export const _DocsIntro: Story = {
tags: ['!dev'],
parameters: {
docs: {
source: { code: introCode },
},
},
render: () => ({
components: { Text },
template: introCode,
}),
};

export const Variants: Story = {
argTypes: {
variant: { control: { disable: true } },
text: { control: { disable: true } },
},
parameters: {
docs: {
source: {
code: `
<Text variant="default" text="Default" />
<Text variant="strong" text="Strong" />
<Text variant="subtle" text="Subtle" />
<Text variant="code" text="code_example" />
<Text variant="danger" text="Danger" />
<Text variant="success" text="Success" />
<Text variant="warning" text="Warning" />
`,
},
},
},
render: (args) => ({
components: { Text },
setup() {
const sharedProps = computed(() => {
const { variant, text, ...rest } = args;
return rest;
});
return { sharedProps };
},
template: `
<div class="flex flex-wrap gap-3 items-center">
<Text variant="default" text="Default" v-bind="sharedProps" />
<Text variant="strong" text="Strong" v-bind="sharedProps" />
<Text variant="subtle" text="Subtle" v-bind="sharedProps" />
<Text variant="code" text="code_example" v-bind="sharedProps" />
<Text variant="danger" text="Danger" v-bind="sharedProps" />
<Text variant="success" text="Success" v-bind="sharedProps" />
<Text variant="warning" text="Warning" v-bind="sharedProps" />
</div>
`,
}),
};

export const Sizes: Story = {
argTypes: {
size: { control: { disable: true } },
text: { control: { disable: true } },
},
parameters: {
docs: {
source: {
code: `
<Text size="lg" text="Large" />
<Text size="base" text="Base" />
<Text size="sm" text="Small" />
`,
},
},
},
render: (args) => ({
components: { Text },
setup() {
const sharedProps = computed(() => {
const { size, text, ...rest } = args;
return rest;
});
return { sharedProps };
},
template: `
<div class="flex flex-wrap gap-3 items-center">
<Text size="lg" text="Large" v-bind="sharedProps" />
<Text size="base" text="Base" v-bind="sharedProps" />
<Text size="sm" text="Small" v-bind="sharedProps" />
</div>
`,
}),
};

const inlineCode = `
<Text>Default with <Text variant="strong">strong</Text> and <Text variant="subtle">subtle</Text> inline</Text>
`;

export const _InlineDocs: Story = {
tags: ['!dev'],
parameters: {
docs: {
source: { code: inlineCode },
},
},
render: () => ({
components: { Text },
template: inlineCode,
}),
};

const paragraphCode = `
<div class="space-y-2">
<Text as="p">This is a paragraph of default text that could appear inside a widget or table description.</Text>
<Text as="p" variant="subtle">This is a subtle paragraph, useful for secondary information or metadata.</Text>
</div>
`;

export const _AsParagraph: Story = {
tags: ['!dev'],
parameters: {
docs: {
source: { code: paragraphCode },
},
},
render: () => ({
components: { Text },
template: paragraphCode,
}),
};
27 changes: 27 additions & 0 deletions resources/js/stories/docs/Text.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
import * as TextStories from '../Text.stories';

<Meta of={TextStories} />

# Text
A utility component for styling inline and block text with consistent variants and sizes. Useful inside tables, widgets, cards, and anywhere you need to quickly apply text styles without reaching for utility classes.
<Canvas of={TextStories._DocsIntro} sourceState={'shown'} />

## Variants
Use the `variant` prop to control the visual style of the text.
<Canvas of={TextStories.Variants} sourceState={'shown'} />

## Sizes
Use the `size` prop to control the text size.
<Canvas of={TextStories.Sizes} sourceState={'shown'} />

## Inline Composition
Since `Text` renders as a `span` by default, you can nest variants inline.
<Canvas of={TextStories._InlineDocs} sourceState={'shown'} />

## As Paragraph
Use the `as` prop to render as a `p` or any other element.
<Canvas of={TextStories._AsParagraph} sourceState={'shown'} />

## Arguments
<ArgTypes of={TextStories} />
Loading