rxn-ui 0.4.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.
- package/README.md +5 -0
- package/cli/index.mjs +119 -0
- package/cli/registry.json +57 -0
- package/package.json +61 -0
- package/src/components/button-base/ButtonBase.vue +82 -0
- package/src/components/button-base/index.stories.ts +108 -0
- package/src/components/button-base/types.ts +10 -0
- package/src/components/card-base/CardBase.vue +23 -0
- package/src/components/card-base/CardContent.vue +15 -0
- package/src/components/card-base/CardDescription.vue +18 -0
- package/src/components/card-base/CardFooter.vue +11 -0
- package/src/components/card-base/CardHeader.vue +14 -0
- package/src/components/card-base/CardTitle.vue +19 -0
- package/src/components/checkbox-base/CheckboxBase.vue +69 -0
- package/src/components/input-base/InputBase.vue +70 -0
- package/src/components/input-base/types.ts +7 -0
- package/src/components/range-base/RangeBase.vue +166 -0
- package/src/components/range-base/RangeOutput.vue +17 -0
- package/src/components/range-base/types.ts +16 -0
- package/src/components/switch-base/SwitchBase.vue +71 -0
- package/src/components/tabs/TabsBase.vue +21 -0
- package/src/components/tabs/TabsIndicator.vue +18 -0
- package/src/components/tabs/TabsList.vue +28 -0
- package/src/components/tabs/TabsPanel.vue +16 -0
- package/src/components/tabs/TabsTab.vue +49 -0
- package/src/components/tabs/context.ts +12 -0
- package/src/components/tabs/types.ts +6 -0
- package/src/components/theme-toggle/ThemeToggle.vue +30 -0
- package/src/styles/style.css +136 -0
- package/src/styles/variables.css +408 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Vue 3 + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
|
4
|
+
|
|
5
|
+
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node // Указывает системе запускать файл через Node.js (CLI-скрипт)
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url) // Получаем абсолютный путь к текущему файлу (аналог __filename)
|
|
9
|
+
const __dirname = path.dirname(__filename) // Получаем директорию текущего файла (аналог __dirname)
|
|
10
|
+
|
|
11
|
+
const COMPONENTS_DIR = path.join(__dirname, '..', 'src', 'components') // Путь к компонентам внутри пакета
|
|
12
|
+
const STYLES_DIR = path.join(__dirname, '..', 'src', 'styles') // Путь к стилям внутри пакета
|
|
13
|
+
const REGISTRY_JSON = path.join(__dirname, 'registry.json') // Путь к файлу registry.json
|
|
14
|
+
|
|
15
|
+
const cwd = process.cwd() // Текущая директория, где пользователь запустил CLI
|
|
16
|
+
|
|
17
|
+
function readRegistry() {
|
|
18
|
+
if (!fs.existsSync(REGISTRY_JSON)) {
|
|
19
|
+
// Проверяем, существует ли registry.json
|
|
20
|
+
console.error('Registry not found')
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(fs.readFileSync(REGISTRY_JSON, 'utf8'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function detectProject() {
|
|
27
|
+
// Функция для автоопределения структуры проекта
|
|
28
|
+
const hasSrc = fs.existsSync(path.join(cwd, 'src')) // Проверяем, есть ли папка src
|
|
29
|
+
return {
|
|
30
|
+
components: hasSrc ? 'src/components/ui' : 'components/ui',
|
|
31
|
+
styles: hasSrc ? 'src/styles' : 'styles',
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function copyDir(src, dest) {
|
|
36
|
+
// Рекурсивная функция копирования папки
|
|
37
|
+
fs.mkdirSync(dest, { recursive: true }) // Создаём папку назначения (если её нет)
|
|
38
|
+
for (const file of fs.readdirSync(src)) {
|
|
39
|
+
// Перебираем все файлы и папки в исходной директории
|
|
40
|
+
const s = path.join(src, file) // Формируем полный путь к исходному файлу
|
|
41
|
+
const d = path.join(dest, file) // Формируем полный путь к файлу назначения
|
|
42
|
+
if (fs.statSync(s).isDirectory()) {
|
|
43
|
+
// Если это папка
|
|
44
|
+
copyDir(s, d) // Рекурсивно копируем её содержимое
|
|
45
|
+
} else {
|
|
46
|
+
// Если это файл
|
|
47
|
+
if (!fs.existsSync(d)) {
|
|
48
|
+
// Проверяем, не существует ли файл уже
|
|
49
|
+
fs.copyFileSync(s, d) // Копируем файл
|
|
50
|
+
console.log('add', path.relative(cwd, d)) // Выводим относительный путь добавленного файла
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function init() {
|
|
57
|
+
const configPath = path.join(cwd, 'rxn-ui.json') // Путь к конфигурационному файлу
|
|
58
|
+
if (fs.existsSync(configPath)) {
|
|
59
|
+
console.log('✔ already initialized')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const config = detectProject()
|
|
64
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) // Сохраняем конфиг в файл
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(STYLES_DIR)) {
|
|
67
|
+
copyDir(STYLES_DIR, path.join(cwd, config.styles)) // Копируем стили в проект пользователя
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('✔ rxn-ui initialized')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function add(name) {
|
|
74
|
+
const registry = readRegistry() // Читаем registry.json
|
|
75
|
+
const entry = registry[name] // Получаем запись компонента по имени
|
|
76
|
+
|
|
77
|
+
if (!entry) {
|
|
78
|
+
// Если компонент не найден
|
|
79
|
+
console.error('Component not found:', name)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const configPath = path.join(cwd, 'rxn-ui.json') // Путь к конфигу
|
|
84
|
+
if (!fs.existsSync(configPath)) {
|
|
85
|
+
console.log('Initializing...\n') // Сообщаем, что запускается инициализация
|
|
86
|
+
init() // Выполняем init автоматически
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')) // Загружаем конфигурацию
|
|
90
|
+
|
|
91
|
+
const src = path.join(COMPONENTS_DIR, name) // Путь к компоненту в пакете
|
|
92
|
+
const dest = path.join(cwd, config.components, name) // Путь установки компонента в проекте
|
|
93
|
+
|
|
94
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
95
|
+
for (const file of entry.files) {
|
|
96
|
+
const s = path.join(src, file)
|
|
97
|
+
const d = path.join(dest, file)
|
|
98
|
+
if (fs.existsSync(s)) {
|
|
99
|
+
fs.copyFileSync(s, d)
|
|
100
|
+
console.log('add', path.relative(cwd, d))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n✔ ${name} added`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const [cmd, arg] = process.argv.slice(2) // Получаем аргументы командной строки
|
|
108
|
+
|
|
109
|
+
if (cmd === 'init') init()
|
|
110
|
+
else if (cmd === 'add') add(arg)
|
|
111
|
+
else {
|
|
112
|
+
console.log(`
|
|
113
|
+
rxn-ui
|
|
114
|
+
|
|
115
|
+
Usage:
|
|
116
|
+
npx rxn-ui init
|
|
117
|
+
npx rxn-ui add button
|
|
118
|
+
`)
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"button-base": {
|
|
3
|
+
"files": [
|
|
4
|
+
"ButtonBase.vue",
|
|
5
|
+
"types.ts"
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"card-base": {
|
|
9
|
+
"files": [
|
|
10
|
+
"CardBase.vue",
|
|
11
|
+
"CardContent.vue",
|
|
12
|
+
"CardDescription.vue",
|
|
13
|
+
"CardFooter.vue",
|
|
14
|
+
"CardHeader.vue",
|
|
15
|
+
"CardTitle.vue"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"checkbox-base": {
|
|
19
|
+
"files": [
|
|
20
|
+
"CheckboxBase.vue"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"input-base": {
|
|
24
|
+
"files": [
|
|
25
|
+
"InputBase.vue",
|
|
26
|
+
"types.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"range-base": {
|
|
30
|
+
"files": [
|
|
31
|
+
"RangeBase.vue",
|
|
32
|
+
"RangeOutput.vue",
|
|
33
|
+
"types.ts"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"switch-base": {
|
|
37
|
+
"files": [
|
|
38
|
+
"SwitchBase.vue"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"tabs": {
|
|
42
|
+
"files": [
|
|
43
|
+
"context.ts",
|
|
44
|
+
"TabsBase.vue",
|
|
45
|
+
"TabsIndicator.vue",
|
|
46
|
+
"TabsList.vue",
|
|
47
|
+
"TabsPanel.vue",
|
|
48
|
+
"TabsTab.vue",
|
|
49
|
+
"types.ts"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"theme-toggle": {
|
|
53
|
+
"files": [
|
|
54
|
+
"ThemeToggle.vue"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rxn-ui",
|
|
3
|
+
"description": "Vue 3 UI component library",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"author": "Artur Hareksian <artharexian@gmail.com> (https://github.com/r2-h)",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/r2-h/artharexian-ui.git"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"rxn-ui": "./cli/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"cli",
|
|
18
|
+
"src/components",
|
|
19
|
+
"src/styles"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "vite",
|
|
26
|
+
"format": "prettier --write .",
|
|
27
|
+
"storybook": "storybook dev -p 6006",
|
|
28
|
+
"build-storybook": "storybook build"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"vue": "^3.5.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@chromatic-com/storybook": "^5.0.1",
|
|
35
|
+
"@storybook/addon-a11y": "^10.2.10",
|
|
36
|
+
"@storybook/addon-docs": "^10.2.10",
|
|
37
|
+
"@storybook/addon-vitest": "^10.2.10",
|
|
38
|
+
"@storybook/vue3-vite": "^10.2.10",
|
|
39
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
40
|
+
"@types/node": "^24.10.1",
|
|
41
|
+
"@vitejs/plugin-vue": "^6.0.2",
|
|
42
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
44
|
+
"@vue/tsconfig": "^0.8.1",
|
|
45
|
+
"eslint": "^10.0.0",
|
|
46
|
+
"eslint-plugin-storybook": "^10.2.10",
|
|
47
|
+
"playwright": "^1.58.2",
|
|
48
|
+
"prettier": "^3.8.1",
|
|
49
|
+
"storybook": "^10.2.10",
|
|
50
|
+
"typescript": "~5.9.3",
|
|
51
|
+
"vite": "^7.3.1",
|
|
52
|
+
"vitest": "^4.0.18",
|
|
53
|
+
"vue": "^3.5.25",
|
|
54
|
+
"vue-tsc": "^3.1.5"
|
|
55
|
+
},
|
|
56
|
+
"eslintConfig": {
|
|
57
|
+
"extends": [
|
|
58
|
+
"plugin:storybook/recommended"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ButtonProps } from './types'
|
|
3
|
+
|
|
4
|
+
const { variant = 'default', shape = 'radius-default', is = 'button' } = defineProps<ButtonProps>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<component :is :class="['btn', shape, variant]" :aria-busy="isPending">
|
|
9
|
+
<slot>button</slot>
|
|
10
|
+
</component>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
.btn {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
text-wrap: nowrap;
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
border: 0.3rem solid var(--color-border);
|
|
22
|
+
background: var(--background);
|
|
23
|
+
box-shadow: var(--shadow-raised);
|
|
24
|
+
transition:
|
|
25
|
+
scale 0.2s ease-in-out,
|
|
26
|
+
background-color 0.2s ease-in-out,
|
|
27
|
+
color 0.2s ease-in-out,
|
|
28
|
+
box-shadow 0.1s ease-in-out,
|
|
29
|
+
border-color 0.1s ease-in-out;
|
|
30
|
+
|
|
31
|
+
&:active {
|
|
32
|
+
box-shadow: var(--shadow-inset);
|
|
33
|
+
scale: 97%;
|
|
34
|
+
}
|
|
35
|
+
&:focus-visible {
|
|
36
|
+
outline: 0.2rem solid var(--foreground);
|
|
37
|
+
outline-offset: 0.2rem;
|
|
38
|
+
}
|
|
39
|
+
&:hover:not(:disabled) {
|
|
40
|
+
opacity: 0.85;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.radius-default {
|
|
45
|
+
border-radius: var(--radius-xl);
|
|
46
|
+
padding-inline: 1.6rem;
|
|
47
|
+
aspect-ratio: 1;
|
|
48
|
+
height: 4.8rem;
|
|
49
|
+
}
|
|
50
|
+
.radius-circle {
|
|
51
|
+
border-radius: 5rem;
|
|
52
|
+
padding: 1rem;
|
|
53
|
+
min-width: 5rem;
|
|
54
|
+
aspect-ratio: 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.primary {
|
|
58
|
+
color: var(--primary);
|
|
59
|
+
background-image: linear-gradient(
|
|
60
|
+
to top left,
|
|
61
|
+
color-mix(in oklch, var(--primary), transparent 97%),
|
|
62
|
+
color-mix(in oklch, var(--primary), transparent 78%)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.default {
|
|
67
|
+
background-image: linear-gradient(
|
|
68
|
+
to top left,
|
|
69
|
+
color-mix(in oklch, var(--muted), transparent 97%),
|
|
70
|
+
color-mix(in oklch, var(--muted), transparent 5%)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.danger {
|
|
75
|
+
color: var(--color-danger);
|
|
76
|
+
background-image: linear-gradient(
|
|
77
|
+
to top left,
|
|
78
|
+
color-mix(in oklch, var(--color-danger), transparent 97%),
|
|
79
|
+
color-mix(in oklch, var(--color-danger), transparent 78%)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
|
|
3
|
+
import ButtonBase from './ButtonBase.vue'
|
|
4
|
+
import type { ButtonProps } from './types'
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'components/ButtonBase',
|
|
8
|
+
component: ButtonBase,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['primary', 'default', 'danger'] satisfies ButtonProps['variant'][],
|
|
14
|
+
},
|
|
15
|
+
shape: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['radius-default', 'radius-circle'] satisfies ButtonProps['shape'][],
|
|
18
|
+
},
|
|
19
|
+
type: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: ['button', 'submit', 'reset'] satisfies ButtonProps['type'][],
|
|
22
|
+
},
|
|
23
|
+
is: {
|
|
24
|
+
control: 'select',
|
|
25
|
+
options: ['button', 'a'],
|
|
26
|
+
},
|
|
27
|
+
// @ts-ignore: attribute
|
|
28
|
+
disabled: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
},
|
|
31
|
+
isPending: {
|
|
32
|
+
control: 'boolean',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} satisfies Meta<typeof ButtonBase>
|
|
36
|
+
|
|
37
|
+
export default meta
|
|
38
|
+
type Story = StoryObj<typeof meta>
|
|
39
|
+
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
default: 'Button',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Primary: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
default: 'Primary Button',
|
|
49
|
+
variant: 'primary',
|
|
50
|
+
isPending: false,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const Danger: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
default: 'Danger Button',
|
|
57
|
+
variant: 'danger',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const CircleShape: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
default: '✓',
|
|
64
|
+
shape: 'radius-circle',
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Disabled: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
default: 'Disabled',
|
|
71
|
+
// @ts-ignore: attribute
|
|
72
|
+
disabled: true,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Pending: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
default: 'Loading...',
|
|
79
|
+
isPending: true,
|
|
80
|
+
variant: 'primary',
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const AsLink: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
default: 'Link Button',
|
|
87
|
+
is: 'a',
|
|
88
|
+
// @ts-ignore: attribute
|
|
89
|
+
href: '#',
|
|
90
|
+
variant: 'primary',
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const AllVariants: Story = {
|
|
95
|
+
render: () => ({
|
|
96
|
+
components: { ButtonBase },
|
|
97
|
+
template: `
|
|
98
|
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
99
|
+
<ButtonBase variant="default">Default</ButtonBase>
|
|
100
|
+
<ButtonBase variant="primary">Primary</ButtonBase>
|
|
101
|
+
<ButtonBase variant="danger">Danger</ButtonBase>
|
|
102
|
+
<ButtonBase shape="radius-circle">✓</ButtonBase>
|
|
103
|
+
<ButtonBase disabled>Disabled</ButtonBase>
|
|
104
|
+
<ButtonBase isPending variant="primary">Pending</ButtonBase>
|
|
105
|
+
</div>
|
|
106
|
+
`,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ButtonVariant = 'primary' | 'default' | 'danger'
|
|
2
|
+
export type ButtonShape = 'radius-default' | 'radius-circle'
|
|
3
|
+
|
|
4
|
+
export type ButtonProps = {
|
|
5
|
+
isPending?: boolean
|
|
6
|
+
variant?: ButtonVariant
|
|
7
|
+
shape?: ButtonShape
|
|
8
|
+
type?: 'button' | 'submit' | 'reset'
|
|
9
|
+
is?: 'button' | 'a'
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { variant = 'raised' } = defineProps<{ variant?: 'raised' | 'inset' }>()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<section :class="['card', variant]">
|
|
7
|
+
<slot />
|
|
8
|
+
</section>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<style scoped>
|
|
12
|
+
.card {
|
|
13
|
+
border-radius: var(--radius-2xl);
|
|
14
|
+
border: 0.3rem solid var(--color-border);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.raised {
|
|
18
|
+
box-shadow: var(--shadow-raised);
|
|
19
|
+
}
|
|
20
|
+
.inset {
|
|
21
|
+
box-shadow: var(--shadow-inset);
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
withDefaults(defineProps<{ as?: 'div' | 'section' | 'ul' | 'main' | 'ol' }>(), { as: 'main' })
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<component :is="as" class="content">
|
|
7
|
+
<slot />
|
|
8
|
+
</component>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<style scoped>
|
|
12
|
+
.content {
|
|
13
|
+
padding: 0 2.4rem 2.4rem 2.4rem;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{ as?: 'p' | 'div' | 'span' }>(), {
|
|
3
|
+
as: 'p',
|
|
4
|
+
})
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<component :is="as" class="description">
|
|
9
|
+
<slot />
|
|
10
|
+
</component>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
.description {
|
|
15
|
+
font-size: var(--text-sm);
|
|
16
|
+
color: var(--muted-foreground);
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' }>(), {
|
|
3
|
+
as: 'h2',
|
|
4
|
+
})
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<component :is="as" class="title">
|
|
9
|
+
<slot />
|
|
10
|
+
</component>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<style scoped>
|
|
14
|
+
.title {
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
line-height: 1;
|
|
17
|
+
letter-spacing: var(--tracking-tight);
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CheckIcon from '../assets/CheckIcon.vue'
|
|
5
|
+
|
|
6
|
+
withDefaults(defineProps<{ isDisabled?: boolean; icon?: Component }>(), { icon: CheckIcon })
|
|
7
|
+
const modelValue = defineModel({ default: false })
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<label :class="['wrapper', { 'is-disabled': isDisabled }]">
|
|
13
|
+
<input type="checkbox" v-model="modelValue" v-bind="$attrs" />
|
|
14
|
+
<div class="checkbox">
|
|
15
|
+
<component :is="icon" class="indicator" />
|
|
16
|
+
</div>
|
|
17
|
+
<slot />
|
|
18
|
+
</label>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<style scoped>
|
|
22
|
+
.wrapper {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
input {
|
|
28
|
+
appearance: none;
|
|
29
|
+
|
|
30
|
+
&:focus-visible + .checkbox {
|
|
31
|
+
outline: 0.2rem solid var(--foreground);
|
|
32
|
+
outline-offset: 0.4rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&:checked + .checkbox {
|
|
36
|
+
color: var(--primary);
|
|
37
|
+
|
|
38
|
+
background-image: linear-gradient(
|
|
39
|
+
to top left,
|
|
40
|
+
color-mix(in oklch, var(--primary), transparent 95%),
|
|
41
|
+
color-mix(in oklch, var(--primary), transparent 75%)
|
|
42
|
+
);
|
|
43
|
+
box-shadow: var(--shadow-raised);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&:checked + .checkbox .indicator {
|
|
47
|
+
display: block;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.checkbox {
|
|
52
|
+
display: grid;
|
|
53
|
+
place-content: center;
|
|
54
|
+
width: 2rem;
|
|
55
|
+
height: 2rem;
|
|
56
|
+
border-radius: var(--radius-sm);
|
|
57
|
+
border: 0.1rem solid var(--color-highlight);
|
|
58
|
+
background-color: var(--background);
|
|
59
|
+
box-shadow: var(--shadow-inset);
|
|
60
|
+
transition:
|
|
61
|
+
background-color 250ms ease-out,
|
|
62
|
+
color 250ms ease-out,
|
|
63
|
+
box-shadow 250ms ease-out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.indicator {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { InputProps } from './types'
|
|
3
|
+
|
|
4
|
+
const model = defineModel<string>({ required: false })
|
|
5
|
+
const {
|
|
6
|
+
disabled = false,
|
|
7
|
+
error = '',
|
|
8
|
+
defaultErrorMessage = '',
|
|
9
|
+
isPending = false,
|
|
10
|
+
...props
|
|
11
|
+
} = defineProps<InputProps>()
|
|
12
|
+
|
|
13
|
+
defineOptions({ inheritAttrs: false })
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div :class="['container', cls?.container]">
|
|
18
|
+
<input
|
|
19
|
+
v-bind="{ ...$attrs, ...props }"
|
|
20
|
+
:disabled="isPending || disabled"
|
|
21
|
+
:class="[{ 'input-error': error, pending: isPending }, cls?.input]"
|
|
22
|
+
@input="model = ($event.target as HTMLInputElement)?.value"
|
|
23
|
+
/>
|
|
24
|
+
<span v-if="error" :class="['error-info', cls?.error]">{{ defaultErrorMessage || error }}</span>
|
|
25
|
+
<span v-else :class="['error-info native-error', cls?.error]">
|
|
26
|
+
{{ defaultErrorMessage || error }}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.container {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
width: 100%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
input {
|
|
39
|
+
border: 0.1rem solid var(--color-highlight);
|
|
40
|
+
background: var(--background);
|
|
41
|
+
box-shadow: var(--shadow-inset);
|
|
42
|
+
padding: 0.8rem 1.2rem;
|
|
43
|
+
font-size: var(--text-sm);
|
|
44
|
+
border-radius: var(--radius-md);
|
|
45
|
+
&:focus-visible {
|
|
46
|
+
outline: 0.2rem solid var(--foreground);
|
|
47
|
+
outline-offset: 0.3rem;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.container:has(input:user-invalid) .native-error {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.error-info {
|
|
56
|
+
font-size: 0.8rem;
|
|
57
|
+
color: var(--color-danger);
|
|
58
|
+
margin-top: 0.4rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.native-error {
|
|
62
|
+
display: none;
|
|
63
|
+
}
|
|
64
|
+
.pending {
|
|
65
|
+
cursor: progress;
|
|
66
|
+
}
|
|
67
|
+
.input-error {
|
|
68
|
+
border-color: var(--color-danger);
|
|
69
|
+
}
|
|
70
|
+
</style>
|