@300codes/design-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +155 -0
  2. package/package.json +63 -0
  3. package/src/components/BaseIcon/BaseIcon.stories.ts +66 -0
  4. package/src/components/BaseIcon/BaseIcon.vue +96 -0
  5. package/src/components/BaseIcon/index.ts +2 -0
  6. package/src/components/BaseLabel/BaseLabel.stories.ts +114 -0
  7. package/src/components/BaseLabel/BaseLabel.vue +149 -0
  8. package/src/components/BaseLabel/index.ts +2 -0
  9. package/src/components/BaseTooltip/BaseTooltip.stories.ts +113 -0
  10. package/src/components/BaseTooltip/BaseTooltip.vue +123 -0
  11. package/src/components/BaseTooltip/index.ts +2 -0
  12. package/src/components/ButtonWithIcon/ButtonWithIcon.stories.ts +149 -0
  13. package/src/components/ButtonWithIcon/ButtonWithIcon.vue +77 -0
  14. package/src/components/ButtonWithIcon/index.ts +2 -0
  15. package/src/components/CheckboxInput/CheckboxInput.stories.ts +99 -0
  16. package/src/components/CheckboxInput/CheckboxInput.vue +176 -0
  17. package/src/components/CheckboxInput/index.ts +2 -0
  18. package/src/components/LabelInput/LabelInput.vue +111 -0
  19. package/src/components/LabelInput/index.ts +2 -0
  20. package/src/components/RadioInput/RadioInput.stories.ts +114 -0
  21. package/src/components/RadioInput/RadioInput.vue +174 -0
  22. package/src/components/RadioInput/index.ts +2 -0
  23. package/src/components/SearchInput/SearchInput.stories.ts +103 -0
  24. package/src/components/SearchInput/SearchInput.vue +83 -0
  25. package/src/components/SearchInput/index.ts +2 -0
  26. package/src/components/SelectInput/SelectInput.stories.ts +111 -0
  27. package/src/components/SelectInput/SelectInput.vue +497 -0
  28. package/src/components/SelectInput/index.ts +2 -0
  29. package/src/components/SelectInputField/SelectInputField.stories.ts +141 -0
  30. package/src/components/SelectInputField/SelectInputField.vue +64 -0
  31. package/src/components/SelectInputField/index.ts +2 -0
  32. package/src/components/SimpleButton/SimpleButton.stories.ts +143 -0
  33. package/src/components/SimpleButton/SimpleButton.vue +193 -0
  34. package/src/components/SimpleButton/index.ts +2 -0
  35. package/src/components/TabsList/TabsList.stories.ts +83 -0
  36. package/src/components/TabsList/TabsList.vue +156 -0
  37. package/src/components/TabsList/index.ts +2 -0
  38. package/src/components/TextInput/TextInput.stories.ts +125 -0
  39. package/src/components/TextInput/TextInput.vue +273 -0
  40. package/src/components/TextInput/components/InputIconButton.vue +54 -0
  41. package/src/components/TextInput/index.ts +2 -0
  42. package/src/components/TextInputField/TextInputField.stories.ts +133 -0
  43. package/src/components/TextInputField/TextInputField.vue +93 -0
  44. package/src/components/TextInputField/index.ts +2 -0
  45. package/src/components/index.ts +15 -0
  46. package/src/css/tokens.css +417 -0
  47. package/src/types/icon.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @300codes/design-system
2
+
3
+ A Vue 3 component library built on Tailwind CSS v4 and CSS custom properties. Ships as raw Vue SFCs — no compilation step, your bundler handles everything.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @300codes/design-system
9
+ ```
10
+
11
+ **Peer dependencies:** `vue ^3.5.0`
12
+
13
+ ---
14
+
15
+ ## Setup
16
+
17
+ ### 1. Tailwind CSS
18
+
19
+ Add the library to your Tailwind content scan in your main CSS file:
20
+
21
+ ```css
22
+ @import "tailwindcss";
23
+ @source "../node_modules/@300codes/design-system/src/**/*.{vue,ts}";
24
+ ```
25
+
26
+ ### 2. CSS Tokens
27
+
28
+ The library uses a three-layer CSS token system:
29
+
30
+ ```
31
+ component token → global token (Tailwind @theme) → hardcoded fallback
32
+ ```
33
+
34
+ Copy `tokens.css` from the library into your project and import it:
35
+
36
+ ```css
37
+ @import "tailwindcss";
38
+ @source "../node_modules/@300codes/design-system/src/**/*.{vue,ts}";
39
+ @import "./tokens.css";
40
+ ```
41
+
42
+ Customize tokens to match your brand:
43
+
44
+ ```css
45
+ /* tokens.css */
46
+ :root {
47
+ --simpleButton-bg: #your-color;
48
+ --simpleButton-fg: #ffffff;
49
+ --textInput-border: #d1d5db;
50
+ --textInput-focus-outline: #6366f1;
51
+ /* ... */
52
+ }
53
+ ```
54
+
55
+ The full token reference is in `node_modules/@300codes/design-system/src/css/tokens.css`.
56
+
57
+ ---
58
+
59
+ ## Usage
60
+
61
+ Components are imported via subpaths — import only what you use:
62
+
63
+ ```ts
64
+ import { SimpleButton } from '@300codes/design-system/simple-button'
65
+ import { TextInputField } from '@300codes/design-system/text-input-field'
66
+ import { SelectInputField } from '@300codes/design-system/select-input-field'
67
+ import { BaseIcon } from '@300codes/design-system/base-icon'
68
+ ```
69
+
70
+ ```vue
71
+ <template>
72
+ <SimpleButton variant="primary" size="md">Click me</SimpleButton>
73
+
74
+ <TextInputField label="Email" help-text="We'll never share your email." />
75
+
76
+ <SelectInputField
77
+ label="Country"
78
+ :options="[{ value: 'pl', label: 'Poland' }]"
79
+ />
80
+ </template>
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Components
86
+
87
+ | Import path | Component | Description |
88
+ |---|---|---|
89
+ | `/simple-button` | `SimpleButton` | Button & link. Variants: primary, secondary, tertiary. Sizes: sm, md. |
90
+ | `/button-with-icon` | `ButtonWithIcon` | SimpleButton with icon slot. |
91
+ | `/base-label` | `BaseLabel` | Badge/label. Variants: primary, secondary, tertiary. |
92
+ | `/base-icon` | `BaseIcon` | Dynamic SVG loader. |
93
+ | `/base-tooltip` | `BaseTooltip` | Tooltip with close button. Sizes: md, lg. |
94
+ | `/text-input` | `TextInput` | Text input with optional icons. Sizes: sm, md, lg. |
95
+ | `/text-input-field` | `TextInputField` | TextInput + label + help text + status icons. |
96
+ | `/select-input` | `SelectInput` | Dropdown (native sheet on mobile). Sizes: sm, md, lg. |
97
+ | `/select-input-field` | `SelectInputField` | SelectInput + label + help text. |
98
+ | `/checkbox-input` | `CheckboxInput` | Checkbox with label. Sizes: sm, md, lg. |
99
+ | `/radio-input` | `RadioInput` | Radio button with label. Sizes: sm, md, lg. |
100
+ | `/tabs-list` | `TabsList` | Tab navigation. Sizes: md, lg. |
101
+ | `/search-input` | `SearchInput` | Search-styled text input. |
102
+ | `/label-input` | `LabelInput` | Label + help text wrapper (for custom inputs). |
103
+
104
+ ---
105
+
106
+ ## Icons
107
+
108
+ ### Required icons
109
+
110
+ Several components rely on SVG icons served from `/icons/` in your public directory. You must provide these files:
111
+
112
+ | File | Used by |
113
+ |---|---|
114
+ | `chevron-down.svg` | `SelectInput` (dropdown arrow) |
115
+ | `search.svg` | `SearchInput` |
116
+ | `close.svg` | `BaseTooltip`, `SearchInput` |
117
+ | `close-circle.svg` | Input clear button |
118
+ | `check.svg` | `CheckboxInput` |
119
+ | `success.svg` | `LabelInput` (success state) |
120
+ | `error.svg` | `LabelInput` (error state) |
121
+ | `not-found.svg` | `BaseIcon` (fallback when icon is missing) |
122
+
123
+ You can copy all icons from `node_modules/@300codes/design-system/public/icons/` into your project's `public/icons/` folder:
124
+
125
+ ```bash
126
+ cp -r node_modules/@300codes/design-system/public/icons public/
127
+ ```
128
+
129
+ ### Using BaseIcon
130
+
131
+ `BaseIcon` dynamically loads any SVG from the `/icons/` path. You can add your own icons alongside the required ones:
132
+
133
+ ```vue
134
+ <BaseIcon name="search" size="md" />
135
+ <BaseIcon name="close" size="lg" />
136
+ <BaseIcon name="your-custom-icon" size="sm" />
137
+ ```
138
+
139
+ Available sizes: `2xs` · `xs` · `sm` · `md` · `lg` · `xl` · `2xl` · `3xl` · `auto`
140
+
141
+ By default icons are loaded from `/icons/{name}.svg`. If your icons live elsewhere, pass a custom `basePath` prop:
142
+
143
+ ```vue
144
+ <BaseIcon name="star" base-path="/assets/icons" size="md" />
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Storybook
150
+
151
+ ```bash
152
+ npm run storybook
153
+ ```
154
+
155
+ Opens at `http://localhost:6006` with live examples of all components.
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@300codes/design-system",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "files": [
6
+ "src/components",
7
+ "src/types",
8
+ "src/css"
9
+ ],
10
+ "exports": {
11
+ "./base-label": "./src/components/BaseLabel/index.ts",
12
+ "./simple-button": "./src/components/SimpleButton/index.ts",
13
+ "./base-icon": "./src/components/BaseIcon/index.ts",
14
+ "./button-with-icon": "./src/components/ButtonWithIcon/index.ts",
15
+ "./label-input": "./src/components/LabelInput/index.ts",
16
+ "./text-input": "./src/components/TextInput/index.ts",
17
+ "./text-input-field": "./src/components/TextInputField/index.ts",
18
+ "./select-input": "./src/components/SelectInput/index.ts",
19
+ "./select-input-field": "./src/components/SelectInputField/index.ts",
20
+ "./checkbox-input": "./src/components/CheckboxInput/index.ts",
21
+ "./radio-input": "./src/components/RadioInput/index.ts",
22
+ "./tabs-list": "./src/components/TabsList/index.ts",
23
+ "./search-input": "./src/components/SearchInput/index.ts",
24
+ "./base-tooltip": "./src/components/BaseTooltip/index.ts"
25
+ },
26
+ "scripts": {
27
+ "build": "vue-tsc --noEmit",
28
+ "storybook": "storybook dev -p 6006",
29
+ "build-storybook": "storybook build",
30
+ "lint": "eslint .",
31
+ "lint:fix": "eslint . --fix",
32
+ "format": "prettier --write .",
33
+ "type-check": "vue-tsc --noEmit"
34
+ },
35
+ "peerDependencies": {
36
+ "vue": "^3.5.0"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.0.0",
40
+ "@storybook/addon-docs": "10.3.5",
41
+ "@storybook/vue3": "^10.3.5",
42
+ "@storybook/vue3-vite": "^10.3.5",
43
+ "@types/node": "^20.0.0",
44
+ "@vitejs/plugin-vue": "^5.0.0",
45
+ "autoprefixer": "^10.4.0",
46
+ "eslint": "^9.0.0",
47
+ "eslint-plugin-storybook": "10.3.5",
48
+ "eslint-plugin-vue": "^9.0.0",
49
+ "globals": "^15.0.0",
50
+ "postcss": "^8.4.0",
51
+ "prettier": "^3.0.0",
52
+ "tailwindcss": "^4.2.2",
53
+ "typescript": "^5.4.0",
54
+ "typescript-eslint": "^8.0.0",
55
+ "vite": "^5.0.0",
56
+ "vue-eslint-parser": "^9.0.0",
57
+ "vue-tsc": "^2.0.0"
58
+ },
59
+ "dependencies": {
60
+ "@tailwindcss/vite": "^4.2.2",
61
+ "@vueuse/core": "^10.0.0"
62
+ }
63
+ }
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import type { ConcreteComponent } from 'vue';
3
+ import type { BaseIconProps } from './BaseIcon.vue';
4
+ import BaseIcon from './BaseIcon.vue';
5
+
6
+ const meta: Meta<BaseIconProps> = {
7
+ title: 'Components/BaseIcon',
8
+ component: BaseIcon as unknown as ConcreteComponent<BaseIconProps>,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ name: { control: 'text' },
12
+ iconPath: { control: 'text' },
13
+ size: {
14
+ control: 'select',
15
+ options: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'auto'],
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<BaseIconProps>;
22
+
23
+ export const Default: Story = {
24
+ args: { name: 'search', size: 'md', iconPath: '/icons' },
25
+ render: (args: BaseIconProps) => ({
26
+ components: { BaseIcon },
27
+ setup() { return { args }; },
28
+ template: '<BaseIcon v-bind="args" />',
29
+ }),
30
+ };
31
+
32
+ export const Search: Story = {
33
+ args: { name: 'search', size: 'md', iconPath: '/icons' },
34
+ render: (args: BaseIconProps) => ({
35
+ components: { BaseIcon },
36
+ setup() { return { args }; },
37
+ template: '<BaseIcon v-bind="args" />',
38
+ }),
39
+ };
40
+
41
+ export const NotFound: Story = {
42
+ args: { name: '', size: 'md', iconPath: '/icons' },
43
+ render: (args: BaseIconProps) => ({
44
+ components: { BaseIcon },
45
+ setup() { return { args }; },
46
+ template: '<BaseIcon v-bind="args" />',
47
+ }),
48
+ };
49
+
50
+ export const AllSizes: Story = {
51
+ render: () => ({
52
+ components: { BaseIcon },
53
+ template: `
54
+ <div class="flex items-end gap-4">
55
+ <BaseIcon name="search" size="2xs" />
56
+ <BaseIcon name="search" size="xs" />
57
+ <BaseIcon name="search" size="sm" />
58
+ <BaseIcon name="search" size="md" />
59
+ <BaseIcon name="search" size="lg" />
60
+ <BaseIcon name="search" size="xl" />
61
+ <BaseIcon name="search" size="2xl" />
62
+ <BaseIcon name="search" size="3xl" />
63
+ </div>
64
+ `,
65
+ }),
66
+ };
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, onServerPrefetch, ref, watch } from 'vue';
3
+ import type { IconSize } from '../../types/icon';
4
+
5
+ export interface BaseIconProps {
6
+ name: string;
7
+ iconPath?: string;
8
+ size?: IconSize;
9
+ }
10
+
11
+ const props = withDefaults(defineProps<BaseIconProps>(), {
12
+ iconPath: '/icons',
13
+ size: 'md',
14
+ });
15
+
16
+ const sizeClasses: Record<IconSize, string> = {
17
+ '2xs': 'w-3 min-w-3 h-3',
18
+ 'xs': 'w-4 min-w-4 h-4',
19
+ 'sm': 'w-5 min-w-5 h-5',
20
+ 'md': 'w-6 min-w-6 h-6',
21
+ 'lg': 'w-8 min-w-8 h-8',
22
+ 'xl': 'w-10 min-w-10 h-10',
23
+ '2xl': 'w-12 min-w-12 h-12',
24
+ '3xl': 'w-16 min-w-16 h-16',
25
+ 'auto': '',
26
+ };
27
+
28
+ const iconClasses = computed(() => sizeClasses[props.size]);
29
+
30
+ const svgHtml = ref('');
31
+ const spanRef = ref<HTMLElement | null>(null);
32
+ const isServer = typeof window === 'undefined';
33
+ const notFound = '<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M48,48V208H80a8,8,0,0,1,0,16H40a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8H80a8,8,0,0,1,0,16ZM216,32H176a8,8,0,0,0,0,16h32V208H176a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32Z"></path></svg>';
34
+
35
+ async function getSvgContent(name: string): Promise<string> {
36
+ const warnNotFound = () =>
37
+ console.warn(`[BaseIcon] Icon "${name}" not found at ${props.iconPath}/${name}.svg`);
38
+
39
+ if (isServer) {
40
+ const path = await import('path');
41
+ const fs = await import('fs/promises');
42
+
43
+ try {
44
+ const filePath = path.join(process.cwd(), 'public', props.iconPath, `${name}.svg`);
45
+ return await fs.readFile(filePath, 'utf8');
46
+ } catch {
47
+ warnNotFound();
48
+ return notFound;
49
+ }
50
+ } else {
51
+ try {
52
+ const res = await fetch(`${props.iconPath}/${name}.svg`);
53
+ if (res.ok) return await res.text();
54
+ throw new Error();
55
+ } catch {
56
+ warnNotFound();
57
+ return notFound;
58
+ }
59
+ }
60
+ }
61
+
62
+ if (isServer) {
63
+ onServerPrefetch(async () => {
64
+ svgHtml.value = await getSvgContent(props.name);
65
+ });
66
+ }
67
+
68
+ watch(
69
+ () => props.name,
70
+ async (newName) => {
71
+ svgHtml.value = await getSvgContent(newName);
72
+ },
73
+ );
74
+
75
+ onMounted(async () => {
76
+ if (!spanRef.value?.innerHTML.length) {
77
+ svgHtml.value = await getSvgContent(props.name);
78
+ }
79
+ });
80
+ </script>
81
+
82
+ <template>
83
+ <span
84
+ ref="spanRef"
85
+ :class="['baseIcon', 'inline-flex items-center justify-center', iconClasses]"
86
+ :aria-hidden="true"
87
+ v-html="svgHtml"
88
+ />
89
+ </template>
90
+
91
+ <style scoped>
92
+ :deep(svg) {
93
+ width: 100%;
94
+ height: 100%;
95
+ }
96
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as BaseIcon } from './BaseIcon.vue';
2
+ export type { BaseIconProps } from './BaseIcon.vue';
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import type { ConcreteComponent } from 'vue';
3
+ import type { BaseLabelProps } from './BaseLabel.vue';
4
+ import BaseLabel from './BaseLabel.vue';
5
+
6
+ const meta: Meta<BaseLabelProps> = {
7
+ title: 'Components/BaseLabel',
8
+ component: BaseLabel as unknown as ConcreteComponent<BaseLabelProps>,
9
+ tags: ['autodocs'],
10
+ argTypes: {
11
+ variant: {
12
+ control: 'select',
13
+ options: ['primary', 'secondary', 'tertiary'],
14
+ },
15
+ size: {
16
+ control: 'select',
17
+ options: ['sm', 'md'],
18
+ },
19
+ as: {
20
+ control: 'select',
21
+ options: ['span', 'button'],
22
+ },
23
+ disabled: { control: 'boolean' },
24
+ href: { control: 'text' },
25
+ target: { control: 'text' },
26
+ rel: { control: 'text' },
27
+ type: {
28
+ control: 'select',
29
+ options: ['button', 'submit', 'reset'],
30
+ },
31
+ },
32
+ };
33
+
34
+ export default meta;
35
+ type Story = StoryObj<BaseLabelProps>;
36
+
37
+ export const Primary: Story = {
38
+ args: { variant: 'primary', size: 'md' },
39
+ render: (args: BaseLabelProps) => ({
40
+ components: { BaseLabel },
41
+ setup() { return { args }; },
42
+ template: '<BaseLabel v-bind="args">Label</BaseLabel>',
43
+ }),
44
+ };
45
+
46
+ export const Secondary: Story = {
47
+ args: { variant: 'secondary', size: 'md' },
48
+ render: (args: BaseLabelProps) => ({
49
+ components: { BaseLabel },
50
+ setup() { return { args }; },
51
+ template: '<BaseLabel v-bind="args">Label</BaseLabel>',
52
+ }),
53
+ };
54
+
55
+ export const Tertiary: Story = {
56
+ args: { variant: 'tertiary', size: 'md' },
57
+ render: (args: BaseLabelProps) => ({
58
+ components: { BaseLabel },
59
+ setup() { return { args }; },
60
+ template: '<BaseLabel v-bind="args">Label</BaseLabel>',
61
+ }),
62
+ };
63
+
64
+ export const AsButton: Story = {
65
+ args: { variant: 'primary', size: 'md', as: 'button' },
66
+ render: (args: BaseLabelProps) => ({
67
+ components: { BaseLabel },
68
+ setup() { return { args }; },
69
+ template: '<BaseLabel v-bind="args">Clickable</BaseLabel>',
70
+ }),
71
+ };
72
+
73
+ export const AsLink: Story = {
74
+ args: { variant: 'primary', size: 'md', href: 'https://example.com', target: '_blank', rel: 'noopener noreferrer' },
75
+ render: (args: BaseLabelProps) => ({
76
+ components: { BaseLabel },
77
+ setup() { return { args }; },
78
+ template: '<BaseLabel v-bind="args">Link</BaseLabel>',
79
+ }),
80
+ };
81
+
82
+ export const Disabled: Story = {
83
+ args: { variant: 'primary', size: 'md', as: 'button', disabled: true },
84
+ render: (args: BaseLabelProps) => ({
85
+ components: { BaseLabel },
86
+ setup() { return { args }; },
87
+ template: '<BaseLabel v-bind="args">Disabled</BaseLabel>',
88
+ }),
89
+ };
90
+
91
+ export const Sizes: Story = {
92
+ render: () => ({
93
+ components: { BaseLabel },
94
+ template: `
95
+ <div class="flex items-center gap-4">
96
+ <BaseLabel variant="primary" size="md">md label</BaseLabel>
97
+ <BaseLabel variant="primary" size="sm">sm label</BaseLabel>
98
+ </div>
99
+ `,
100
+ }),
101
+ };
102
+
103
+ export const Variants: Story = {
104
+ render: () => ({
105
+ components: { BaseLabel },
106
+ template: `
107
+ <div class="flex items-center gap-4">
108
+ <BaseLabel variant="primary">Primary</BaseLabel>
109
+ <BaseLabel variant="secondary">Secondary</BaseLabel>
110
+ <BaseLabel variant="tertiary">Tertiary</BaseLabel>
111
+ </div>
112
+ `,
113
+ }),
114
+ };
@@ -0,0 +1,149 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'vue';
4
+
5
+ export interface BaseLabelProps {
6
+ variant?: 'primary' | 'secondary' | 'tertiary';
7
+ size?: 'sm' | 'md';
8
+ as?: 'span' | 'button';
9
+ href?: string;
10
+ target?: AnchorHTMLAttributes['target'];
11
+ rel?: AnchorHTMLAttributes['rel'];
12
+ disabled?: boolean;
13
+ type?: ButtonHTMLAttributes['type'];
14
+ }
15
+
16
+ const props = withDefaults(defineProps<BaseLabelProps>(), {
17
+ variant: 'primary',
18
+ size: 'md',
19
+ as: 'span',
20
+ disabled: false,
21
+ href: undefined,
22
+ target: undefined,
23
+ rel: undefined,
24
+ type: undefined,
25
+ });
26
+
27
+ const tag = computed(() => (props.href ? 'a' : props.as));
28
+
29
+ const isInteractive = computed(() => tag.value !== 'span');
30
+
31
+ const nativeAttrs = computed((): Record<string, unknown> => {
32
+ if (props.href) {
33
+ return {
34
+ href: props.disabled ? undefined : props.href,
35
+ target: props.target,
36
+ rel: props.rel,
37
+ 'aria-disabled': props.disabled ? 'true' : undefined,
38
+ tabindex: props.disabled ? -1 : undefined,
39
+ };
40
+ }
41
+
42
+ if (props.as === 'button') {
43
+ return {
44
+ type: props.type ?? 'button',
45
+ disabled: props.disabled,
46
+ };
47
+ }
48
+
49
+ return {};
50
+ });
51
+ </script>
52
+
53
+ <template>
54
+ <component
55
+ :is="tag"
56
+ v-bind="nativeAttrs"
57
+ :class="[
58
+ 'baseLabel',
59
+ 'no-underline inline-flex items-center justify-center',
60
+ `baseLabel--${props.variant}`,
61
+ `baseLabel--${props.size}`,
62
+ {
63
+ 'baseLabel--interactive': isInteractive,
64
+ 'baseLabel--disabled': props.disabled,
65
+ },
66
+ ]"
67
+ >
68
+ <slot />
69
+ </component>
70
+ </template>
71
+
72
+ <style scoped>
73
+ @reference "tailwindcss";
74
+
75
+ .baseLabel {
76
+ --_fg: var(--baseLabel-fg, #ffffff);
77
+ background-color: var(--baseLabel-bg, #0024d6);
78
+ color: var(--_fg);
79
+ border: var(--baseLabel-border-width, 1px) solid var(--baseLabel-border, transparent);
80
+ border-radius: var(--baseLabel-radius, 3.5rem);
81
+ padding: var(--baseLabel-py, 0.125rem) var(--baseLabel-px, 0.5rem);
82
+ min-height: var(--baseLabel-min-h, 1.5rem);
83
+ font-size: var(--baseLabel-font-size, 0.75rem);
84
+ font-weight: var(--baseLabel-font-weight, 600);
85
+ line-height: 1.2;
86
+ }
87
+
88
+ .baseLabel--interactive {
89
+ @apply outline-offset-2 cursor-pointer select-none;
90
+ }
91
+
92
+ .baseLabel--interactive:hover:not(.baseLabel--disabled) {
93
+ background-color: var(--baseLabel-bg-hover, #0e161b);
94
+ border-color: var(--baseLabel-border-hover, #0e161b);
95
+ color: var(--baseLabel-fg-hover, #ffffff);
96
+ }
97
+
98
+ .baseLabel--interactive:focus-visible {
99
+ outline: 2px solid var(--baseLabel-ring, #0066cc);
100
+ }
101
+
102
+ .baseLabel--disabled {
103
+ @apply cursor-not-allowed pointer-events-none opacity-50;
104
+ }
105
+
106
+ /* ── sm ── */
107
+
108
+ .baseLabel--sm {
109
+ min-height: var(--baseLabel-sm-min-h, 1.25rem);
110
+ }
111
+
112
+ /* ── secondary ── */
113
+
114
+ .baseLabel--secondary {
115
+ --_fg: var(--baseLabel-secondary-fg, #0024d6);
116
+ background-color: var(--baseLabel-secondary-bg, #ffffff);
117
+ color: var(--_fg);
118
+ border-color: var(--baseLabel-secondary-border, #0024d6);
119
+ }
120
+
121
+ .baseLabel--secondary.baseLabel--interactive:hover:not(.baseLabel--disabled) {
122
+ background-color: var(--baseLabel-secondary-bg-hover, #f3f4f6);
123
+ border-color: var(--baseLabel-secondary-border-hover, #0e161b);
124
+ color: var(--baseLabel-secondary-fg-hover, #0e161b);
125
+ }
126
+
127
+ .baseLabel--secondary.baseLabel--interactive:focus-visible {
128
+ outline: 2px solid var(--baseLabel-secondary-ring, #0066cc);
129
+ }
130
+
131
+ /* ── tertiary ── */
132
+
133
+ .baseLabel--tertiary {
134
+ --_fg: var(--baseLabel-tertiary-fg, #0e161b);
135
+ background-color: var(--baseLabel-tertiary-bg, transparent);
136
+ color: var(--_fg);
137
+ border-color: var(--baseLabel-tertiary-border, transparent);
138
+ }
139
+
140
+ .baseLabel--tertiary.baseLabel--interactive:hover:not(.baseLabel--disabled) {
141
+ background-color: var(--baseLabel-tertiary-bg-hover, #f3f4f6);
142
+ border-color: var(--baseLabel-tertiary-border-hover, #f3f4f6);
143
+ color: var(--baseLabel-tertiary-fg-hover, #0024d6);
144
+ }
145
+
146
+ .baseLabel--tertiary.baseLabel--interactive:focus-visible {
147
+ outline: 2px solid var(--baseLabel-tertiary-ring, #0066cc);
148
+ }
149
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as BaseLabel } from './BaseLabel.vue';
2
+ export type { BaseLabelProps } from './BaseLabel.vue';