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.
Files changed (30) hide show
  1. package/README.md +5 -0
  2. package/cli/index.mjs +119 -0
  3. package/cli/registry.json +57 -0
  4. package/package.json +61 -0
  5. package/src/components/button-base/ButtonBase.vue +82 -0
  6. package/src/components/button-base/index.stories.ts +108 -0
  7. package/src/components/button-base/types.ts +10 -0
  8. package/src/components/card-base/CardBase.vue +23 -0
  9. package/src/components/card-base/CardContent.vue +15 -0
  10. package/src/components/card-base/CardDescription.vue +18 -0
  11. package/src/components/card-base/CardFooter.vue +11 -0
  12. package/src/components/card-base/CardHeader.vue +14 -0
  13. package/src/components/card-base/CardTitle.vue +19 -0
  14. package/src/components/checkbox-base/CheckboxBase.vue +69 -0
  15. package/src/components/input-base/InputBase.vue +70 -0
  16. package/src/components/input-base/types.ts +7 -0
  17. package/src/components/range-base/RangeBase.vue +166 -0
  18. package/src/components/range-base/RangeOutput.vue +17 -0
  19. package/src/components/range-base/types.ts +16 -0
  20. package/src/components/switch-base/SwitchBase.vue +71 -0
  21. package/src/components/tabs/TabsBase.vue +21 -0
  22. package/src/components/tabs/TabsIndicator.vue +18 -0
  23. package/src/components/tabs/TabsList.vue +28 -0
  24. package/src/components/tabs/TabsPanel.vue +16 -0
  25. package/src/components/tabs/TabsTab.vue +49 -0
  26. package/src/components/tabs/context.ts +12 -0
  27. package/src/components/tabs/types.ts +6 -0
  28. package/src/components/theme-toggle/ThemeToggle.vue +30 -0
  29. package/src/styles/style.css +136 -0
  30. 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,11 @@
1
+ <template>
2
+ <footer class="footer">
3
+ <slot />
4
+ </footer>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .footer {
9
+ padding: 0 2.4rem 2.4rem 2.4rem;
10
+ }
11
+ </style>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <header class="header">
3
+ <slot />
4
+ </header>
5
+ </template>
6
+
7
+ <style scoped>
8
+ .header {
9
+ display: flex;
10
+ flex-direction: column;
11
+ padding: 2.4rem;
12
+ row-gap: 0.6rem;
13
+ }
14
+ </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>