Skip to content

Commit a2cb018

Browse files
[6.x] Add a Text component (#14247)
* Add a text component * Tweak and simplify variations * Fix tests * but like for realisies --------- Co-authored-by: Jack McDade <jack@jackmcdade.com>
1 parent d62b8f6 commit a2cb018

7 files changed

Lines changed: 250 additions & 0 deletions

File tree

packages/cms/src/ui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const {
114114
TabProvider,
115115
Tabs,
116116
TabTrigger,
117+
Text,
117118
Textarea,
118119
TimePicker,
119120
ToggleGroup,

resources/js/bootstrap/cms/ui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export {
114114
TabProvider,
115115
Tabs,
116116
TabTrigger,
117+
Text,
117118
Textarea,
118119
TimePicker,
119120
ToggleGroup,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script setup>
2+
import { computed, useSlots } from 'vue';
3+
import { cva } from 'cva';
4+
import { twMerge } from 'tailwind-merge';
5+
6+
const props = defineProps({
7+
/** The element this component should render as */
8+
as: { type: String, default: 'span' },
9+
/** Controls the size of the text. Options: `xs`, `sm`, `base`, `lg` */
10+
size: { type: String, default: 'base' },
11+
/** Text to display */
12+
text: { type: [String, Number, Boolean, null], default: null },
13+
/** Controls the appearance of the text. Options: `default`, `strong`, `subtle`, `code`, `danger`, `success`, `warning` */
14+
variant: { type: String, default: 'default' },
15+
});
16+
17+
const slots = useSlots();
18+
const hasDefaultSlot = !!slots.default;
19+
20+
const textClasses = computed(() => {
21+
const classes = cva({
22+
base: 'antialiased',
23+
variants: {
24+
variant: {
25+
default: 'text-gray-900 dark:text-gray-50',
26+
strong: 'font-semibold text-gray-900 dark:text-gray-50',
27+
subtle: 'text-gray-600 dark:text-gray-600/90',
28+
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',
29+
danger: 'text-red-600 dark:text-red-400',
30+
success: 'text-green-600 dark:text-green-400',
31+
warning: 'text-amber-600 dark:text-amber-400',
32+
},
33+
size: {
34+
xs: 'text-2xs',
35+
sm: 'text-xs',
36+
base: 'text-sm',
37+
lg: 'text-base',
38+
},
39+
},
40+
})({ ...props });
41+
42+
return twMerge(classes);
43+
});
44+
</script>
45+
46+
<template>
47+
<component :is="as" :class="textClasses" data-ui-text>
48+
<slot v-if="hasDefaultSlot" />
49+
<template v-else>{{ text }}</template>
50+
</component>
51+
</template>

resources/js/components/ui/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export { default as TableRows } from './Table/Rows.vue';
8282
export { default as TabList } from './Tabs/List.vue';
8383
export { default as Tabs } from './Tabs/Tabs.vue';
8484
export { default as TabTrigger } from './Tabs/Trigger.vue';
85+
export { default as Text } from './Text.vue';
8586
export { default as Textarea } from './Textarea.vue';
8687
export { default as TimePicker } from './TimePicker/TimePicker.vue';
8788
export { default as ToggleGroup } from './Toggle/Group.vue';
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type {Meta, StoryObj} from '@storybook/vue3';
2+
import {Text} from '@ui';
3+
import {computed} from 'vue';
4+
5+
const meta = {
6+
title: 'Components/Text',
7+
component: Text,
8+
argTypes: {
9+
size: {
10+
control: 'select',
11+
options: ['xs', 'sm', 'base', 'lg'],
12+
},
13+
variant: {
14+
control: 'select',
15+
options: ['default', 'strong', 'subtle', 'code', 'danger', 'success', 'warning'],
16+
},
17+
as: {
18+
control: 'select',
19+
options: ['span', 'p', 'div'],
20+
},
21+
},
22+
} satisfies Meta<typeof Text>;
23+
24+
export default meta;
25+
type Story = StoryObj<typeof meta>;
26+
27+
export const Default: Story = {
28+
args: {
29+
text: 'The quick brown fox jumps over the lazy dog.',
30+
},
31+
};
32+
33+
const introCode = `
34+
<div class="flex flex-wrap gap-3 items-center">
35+
<Text text="Default" />
36+
<Text variant="strong" text="Strong" />
37+
<Text variant="subtle" text="Subtle" />
38+
<Text variant="code" text="code_example" />
39+
</div>
40+
`;
41+
42+
export const _DocsIntro: Story = {
43+
tags: ['!dev'],
44+
parameters: {
45+
docs: {
46+
source: { code: introCode },
47+
},
48+
},
49+
render: () => ({
50+
components: { Text },
51+
template: introCode,
52+
}),
53+
};
54+
55+
export const Variants: Story = {
56+
argTypes: {
57+
variant: { control: { disable: true } },
58+
text: { control: { disable: true } },
59+
},
60+
parameters: {
61+
docs: {
62+
source: {
63+
code: `
64+
<Text variant="default" text="Default" />
65+
<Text variant="strong" text="Strong" />
66+
<Text variant="subtle" text="Subtle" />
67+
<Text variant="code" text="code_example" />
68+
<Text variant="danger" text="Danger" />
69+
<Text variant="success" text="Success" />
70+
<Text variant="warning" text="Warning" />
71+
`,
72+
},
73+
},
74+
},
75+
render: (args) => ({
76+
components: { Text },
77+
setup() {
78+
const sharedProps = computed(() => {
79+
const { variant, text, ...rest } = args;
80+
return rest;
81+
});
82+
return { sharedProps };
83+
},
84+
template: `
85+
<div class="flex flex-wrap gap-3 items-center">
86+
<Text variant="default" text="Default" v-bind="sharedProps" />
87+
<Text variant="strong" text="Strong" v-bind="sharedProps" />
88+
<Text variant="subtle" text="Subtle" v-bind="sharedProps" />
89+
<Text variant="code" text="code_example" v-bind="sharedProps" />
90+
<Text variant="danger" text="Danger" v-bind="sharedProps" />
91+
<Text variant="success" text="Success" v-bind="sharedProps" />
92+
<Text variant="warning" text="Warning" v-bind="sharedProps" />
93+
</div>
94+
`,
95+
}),
96+
};
97+
98+
export const Sizes: Story = {
99+
argTypes: {
100+
size: { control: { disable: true } },
101+
text: { control: { disable: true } },
102+
},
103+
parameters: {
104+
docs: {
105+
source: {
106+
code: `
107+
<Text size="lg" text="Large" />
108+
<Text size="base" text="Base" />
109+
<Text size="sm" text="Small" />
110+
`,
111+
},
112+
},
113+
},
114+
render: (args) => ({
115+
components: { Text },
116+
setup() {
117+
const sharedProps = computed(() => {
118+
const { size, text, ...rest } = args;
119+
return rest;
120+
});
121+
return { sharedProps };
122+
},
123+
template: `
124+
<div class="flex flex-wrap gap-3 items-center">
125+
<Text size="lg" text="Large" v-bind="sharedProps" />
126+
<Text size="base" text="Base" v-bind="sharedProps" />
127+
<Text size="sm" text="Small" v-bind="sharedProps" />
128+
</div>
129+
`,
130+
}),
131+
};
132+
133+
const inlineCode = `
134+
<Text>Default with <Text variant="strong">strong</Text> and <Text variant="subtle">subtle</Text> inline</Text>
135+
`;
136+
137+
export const _InlineDocs: Story = {
138+
tags: ['!dev'],
139+
parameters: {
140+
docs: {
141+
source: { code: inlineCode },
142+
},
143+
},
144+
render: () => ({
145+
components: { Text },
146+
template: inlineCode,
147+
}),
148+
};
149+
150+
const paragraphCode = `
151+
<div class="space-y-2">
152+
<Text as="p">This is a paragraph of default text that could appear inside a widget or table description.</Text>
153+
<Text as="p" variant="subtle">This is a subtle paragraph, useful for secondary information or metadata.</Text>
154+
</div>
155+
`;
156+
157+
export const _AsParagraph: Story = {
158+
tags: ['!dev'],
159+
parameters: {
160+
docs: {
161+
source: { code: paragraphCode },
162+
},
163+
},
164+
render: () => ({
165+
components: { Text },
166+
template: paragraphCode,
167+
}),
168+
};

resources/js/stories/docs/Text.mdx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
2+
import * as TextStories from '../Text.stories';
3+
4+
<Meta of={TextStories} />
5+
6+
# Text
7+
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.
8+
<Canvas of={TextStories._DocsIntro} sourceState={'shown'} />
9+
10+
## Variants
11+
Use the `variant` prop to control the visual style of the text.
12+
<Canvas of={TextStories.Variants} sourceState={'shown'} />
13+
14+
## Sizes
15+
Use the `size` prop to control the text size.
16+
<Canvas of={TextStories.Sizes} sourceState={'shown'} />
17+
18+
## Inline Composition
19+
Since `Text` renders as a `span` by default, you can nest variants inline.
20+
<Canvas of={TextStories._InlineDocs} sourceState={'shown'} />
21+
22+
## As Paragraph
23+
Use the `as` prop to render as a `p` or any other element.
24+
<Canvas of={TextStories._AsParagraph} sourceState={'shown'} />
25+
26+
## Arguments
27+
<ArgTypes of={TextStories} />

resources/js/tests/Package.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ it('exports ui', async () => {
192192
'TabContent',
193193
'TabList',
194194
'TabTrigger',
195+
'Text',
195196
'Stack',
196197
'StackClose',
197198
'StackContent',

0 commit comments

Comments
 (0)