artharexian-ui 0.3.2 → 0.3.4

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/cli/index.mjs CHANGED
@@ -3,228 +3,144 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
5
5
  import { fileURLToPath } from 'node:url'
6
- import https from 'node:https'
7
6
 
8
7
  const __filename = fileURLToPath(import.meta.url)
9
8
  const __dirname = path.dirname(__filename)
10
9
 
11
- const args = process.argv.slice(2)
12
- const command = args[0]
13
- const component = args[1]
10
+ const REGISTRY_DIR = path.join(__dirname, '..', 'registry')
11
+ const REGISTRY_JSON = path.join(REGISTRY_DIR, 'registry.json')
14
12
 
15
13
  const cwd = process.env.INIT_CWD || process.cwd()
16
14
 
17
- // GitHub config
18
- const GITHUB_REPO = 'r2-h/artharexian-ui'
19
- const GITHUB_BRANCH = 'main'
20
- const COMPONENTS_PATH = 'src/components'
21
-
22
- const EXCLUDE_FILES = ['meta.json', 'index.stories.ts']
23
-
24
- // Fetch JSON from GitHub
25
- function fetchJSON(url) {
26
- return new Promise((resolve, reject) => {
27
- const options = {
28
- headers: {
29
- 'User-Agent': 'artharexian-ui-cli',
30
- 'Accept': 'application/vnd.github.v3+json',
31
- },
32
- }
33
- https
34
- .get(url, options, (res) => {
35
- if (res.statusCode !== 200) {
36
- reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`))
37
- return
38
- }
39
- let data = ''
40
- res.on('data', (chunk) => (data += chunk))
41
- res.on('end', () => {
42
- try {
43
- resolve(JSON.parse(data))
44
- } catch (e) {
45
- reject(new Error(`Failed to parse JSON: ${e.message}`))
46
- }
47
- })
48
- })
49
- .on('error', reject)
50
- })
51
- }
15
+ // ---------- utils ----------
52
16
 
53
- // Fetch file content from GitHub
54
- function fetchFile(url) {
55
- return new Promise((resolve, reject) => {
56
- https
57
- .get(url, (res) => {
58
- let data = ''
59
- res.on('data', (chunk) => (data += chunk))
60
- res.on('end', () => resolve(data))
61
- })
62
- .on('error', reject)
63
- })
17
+ function readRegistry() {
18
+ if (!fs.existsSync(REGISTRY_JSON)) {
19
+ console.error('Registry not found')
20
+ process.exit(1)
21
+ }
22
+ try {
23
+ return JSON.parse(fs.readFileSync(REGISTRY_JSON, 'utf8'))
24
+ } catch {
25
+ console.error('Invalid registry.json')
26
+ process.exit(1)
27
+ }
64
28
  }
65
29
 
66
- // Get component files list from GitHub API
67
- async function getComponentFiles(componentName) {
68
- const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/${COMPONENTS_PATH}/${componentName}?ref=${GITHUB_BRANCH}`
69
- const data = await fetchJSON(url)
70
- return data.filter((file) => file.type === 'file' && !EXCLUDE_FILES.includes(file.name))
30
+ function copyFileSafe(src, dest) {
31
+ if (!fs.existsSync(src)) {
32
+ console.error(`Missing file in registry: ${path.basename(src)}`)
33
+ process.exit(1)
34
+ }
35
+ if (fs.existsSync(dest)) {
36
+ const rel = path.relative(cwd, dest)
37
+ console.log(`skip ${rel} (exists)`)
38
+ return
39
+ }
40
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
41
+ fs.copyFileSync(src, dest)
42
+ const rel = path.relative(cwd, dest)
43
+ console.log(`add ${rel}`)
71
44
  }
72
45
 
73
- // Download and save file
74
- async function downloadFile(file, destPath) {
75
- const content = await fetchFile(file.download_url)
76
- fs.writeFileSync(destPath, content)
77
- }
46
+ // ---------- commands ----------
78
47
 
79
- // Copy directory locally (for development)
80
- function copyDir(src, dest) {
81
- fs.mkdirSync(dest, { recursive: true })
82
- for (const file of fs.readdirSync(src)) {
83
- if (EXCLUDE_FILES.includes(file)) continue
84
-
85
- const s = path.join(src, file)
86
- const d = path.join(dest, file)
87
- if (fs.statSync(s).isDirectory()) {
88
- copyDir(s, d)
89
- } else {
90
- fs.copyFileSync(s, d)
91
- }
92
- }
48
+ function listComponents() {
49
+ const registry = readRegistry()
50
+ console.log('Available components:\n')
51
+ Object.keys(registry).forEach((name) => {
52
+ console.log(` ${name}`)
53
+ })
93
54
  }
94
55
 
95
- // Add component from GitHub
96
- async function addComponent(componentName) {
97
- const destDir = path.resolve(cwd, 'src/components/ui', componentName)
98
- const stylesPath = path.resolve(cwd, 'src/styles')
56
+ function addComponent(name) {
57
+ const registry = readRegistry()
58
+ const entry = registry[name]
99
59
 
100
- // Auto-install styles if not present
101
- if (!fs.existsSync(stylesPath)) {
102
- console.log('Styles not found. Installing...\n')
103
- await copyStyles()
104
- console.log('')
60
+ if (!entry) {
61
+ console.error(`Component not found: ${name}\n`)
62
+ listComponents()
63
+ process.exit(1)
105
64
  }
106
65
 
107
- console.log(`Fetching ${componentName} from GitHub...`)
108
-
109
- try {
110
- const files = await getComponentFiles(componentName)
111
-
112
- if (files.length === 0) {
113
- console.error(`Error: Component not found: ${componentName}`)
114
- listComponents()
115
- process.exit(1)
116
- }
117
-
118
- fs.mkdirSync(destDir, { recursive: true })
119
-
120
- for (const file of files) {
121
- const destPath = path.join(destDir, file.name)
122
- await downloadFile(file, destPath)
123
- console.log(` ✔ ${file.name}`)
66
+ // Auto-install registry styles if not present
67
+ const stylesPath = path.join(cwd, 'src', 'styles')
68
+ const registryStyles = path.join(REGISTRY_DIR, 'styles')
69
+ if (!fs.existsSync(stylesPath) && fs.existsSync(registryStyles)) {
70
+ console.log('Styles not found. Installing...\n')
71
+ fs.mkdirSync(stylesPath, { recursive: true })
72
+ for (const file of fs.readdirSync(registryStyles)) {
73
+ copyFileSafe(path.join(registryStyles, file), path.join(stylesPath, file))
124
74
  }
125
-
126
- console.log(`\n✔ ${componentName} added to src/components/ui/${componentName}`)
127
- console.log('\nNext steps:')
128
- console.log(` 1. Import: import { ${toPascalCase(componentName)} } from '@/components/ui/${componentName}'`)
129
- console.log(' 2. Configure CSS variables (see docs)')
130
- } catch (error) {
131
- console.error(`Error: ${error.message}`)
132
- console.error('\nMake sure you have internet connection and the component exists.')
133
- process.exit(1)
75
+ console.log('')
134
76
  }
135
- }
136
77
 
137
- // List available components
138
- async function listComponents() {
139
- console.log('Available components:')
140
- try {
141
- const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/${COMPONENTS_PATH}?ref=${GITHUB_BRANCH}`
142
- const data = await fetchJSON(url)
143
- const components = data.filter((item) => item.type === 'dir').map((item) => item.name)
144
- components.forEach((c) => console.log(` - ${c}`))
145
- } catch (error) {
146
- console.error(` (unable to fetch: ${error.message})`)
147
- }
148
- }
78
+ const srcDir = path.join(REGISTRY_DIR, name)
79
+ const destDir = path.join(cwd, 'src/components/ui', name)
149
80
 
150
- // Copy styles from GitHub
151
- async function copyStyles() {
152
- const stylesDst = path.resolve(cwd, 'src/styles')
81
+ entry.files.forEach((file) => {
82
+ const src = path.join(srcDir, file)
83
+ const dest = path.join(destDir, file)
84
+ copyFileSafe(src, dest)
85
+ })
153
86
 
154
- console.log('Fetching styles from GitHub...')
87
+ console.log(`\n✔ ${name} installed`)
88
+ }
155
89
 
156
- try {
157
- const url = `https://api.github.com/repos/${GITHUB_REPO}/contents/src/styles?ref=${GITHUB_BRANCH}`
158
- const data = await fetchJSON(url)
159
- const cssFiles = data.filter((file) => file.type === 'file' && file.name.endsWith('.css'))
90
+ function init() {
91
+ const stylesDest = path.join(cwd, 'src', 'styles')
92
+ const registryStyles = path.join(REGISTRY_DIR, 'styles')
160
93
 
161
- fs.mkdirSync(stylesDst, { recursive: true })
94
+ if (!fs.existsSync(registryStyles)) {
95
+ console.log('No global styles in registry')
96
+ return
97
+ }
162
98
 
163
- for (const file of cssFiles) {
164
- const destPath = path.join(stylesDst, file.name)
165
- await downloadFile(file, destPath)
166
- console.log(` ✔ ${file.name}`)
167
- }
99
+ fs.mkdirSync(stylesDest, { recursive: true })
168
100
 
169
- console.log('✔ Styles installed')
170
- } catch (error) {
171
- console.error(`Error: ${error.message}`)
172
- process.exit(1)
101
+ for (const file of fs.readdirSync(registryStyles)) {
102
+ copyFileSafe(path.join(registryStyles, file), path.join(stylesDest, file))
173
103
  }
174
- }
175
104
 
176
- // Helper functions
177
- function capitalize(str) {
178
- return str.charAt(0).toUpperCase() + str.slice(1)
105
+ console.log('\n✔ styles installed')
179
106
  }
180
107
 
181
- function toPascalCase(str) {
182
- return str
183
- .split('-')
184
- .map((part) => capitalize(part))
185
- .join('')
186
- }
108
+ // ---------- CLI ----------
187
109
 
188
- // CLI commands
189
- if (!command) {
190
- console.log('Usage: artharexian-ui <add|eject|init|list> [component]')
191
- console.log('')
192
- console.log('Commands:')
193
- console.log(' add <component> Add a component to your project (from GitHub)')
194
- console.log(' eject <component> Alias for add (eject component source)')
195
- console.log(' init Initialize library (copy styles)')
196
- console.log(' list List available components')
197
- console.log('')
198
- console.log('Examples:')
199
- console.log(' npx artharexian-ui add button-base')
200
- console.log(' npx artharexian-ui list')
201
- console.log(' npx artharexian-ui init')
202
- process.exit(0)
203
- }
110
+ const args = process.argv.slice(2)
111
+ const command = args[0]
112
+ const component = args[1]
204
113
 
205
- if (command === 'init') {
206
- console.log('Initializing artharexian-ui...')
207
- await copyStyles()
208
- console.log('')
209
- console.log('✔ artharexian-ui initialized successfully!')
114
+ if (!command) {
115
+ console.log(`
116
+ artharexian-ui
117
+
118
+ Usage:
119
+ npx artharexian-ui init
120
+ npx artharexian-ui add <component>
121
+ npx artharexian-ui list
122
+ `)
210
123
  process.exit(0)
211
124
  }
212
125
 
213
126
  if (command === 'list') {
214
- await listComponents()
127
+ listComponents()
215
128
  process.exit(0)
216
129
  }
217
130
 
218
- if (command === 'add' || command === 'eject') {
131
+ if (command === 'add') {
219
132
  if (!component) {
220
- console.error('Error: Component name required')
221
- console.error('Usage: artharexian-ui add <component>')
133
+ console.error('Component name required')
222
134
  process.exit(1)
223
135
  }
224
- await addComponent(component)
136
+ addComponent(component)
137
+ process.exit(0)
138
+ }
139
+
140
+ if (command === 'init') {
141
+ init()
225
142
  process.exit(0)
226
143
  }
227
144
 
228
145
  console.error(`Unknown command: ${command}`)
229
- console.error('Run without arguments for usage info')
230
146
  process.exit(1)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artharexian-ui",
3
3
  "description": "Vue 3 UI component library",
4
- "version": "0.3.2",
4
+ "version": "0.3.4",
5
5
  "license": "MIT",
6
6
  "private": false,
7
7
  "type": "module",
@@ -14,20 +14,19 @@
14
14
  "artharexian-ui": "./cli/index.mjs"
15
15
  },
16
16
  "files": [
17
- "src/styles",
18
17
  "cli",
19
- "!**/*.stories.ts",
20
- "!**/meta.json"
18
+ "registry"
21
19
  ],
22
- "exports": {
23
- "./styles": "./src/styles/style.css"
20
+ "engines": {
21
+ "node": ">=18"
24
22
  },
25
23
  "scripts": {
26
24
  "dev": "vite",
27
25
  "format": "prettier --write .",
28
26
  "storybook": "storybook dev -p 6006",
29
27
  "build-storybook": "storybook build",
30
- "postinstall": "node ./cli/index.mjs init"
28
+ "build:registry": "node scripts/build-registry.mjs",
29
+ "prepublishOnly": "npm run build:registry"
31
30
  },
32
31
  "peerDependencies": {
33
32
  "vue": "^3.5.0"
@@ -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,2 @@
1
+ export { default } from './ButtonBase.vue'
2
+ export * from './types'
@@ -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,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>
@@ -0,0 +1,7 @@
1
+ export type InputProps = {
2
+ disabled?: boolean
3
+ cls?: { container?: string; error?: string; input?: string }
4
+ error?: string
5
+ defaultErrorMessage?: string
6
+ isPending?: boolean
7
+ }
@@ -0,0 +1,166 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ import { useVars } from '../../composables/useWars'
5
+ import { cssSizeToNumber, cssValueToUnit } from '../../utils/cssParser'
6
+ import { mergeDefaultProps } from '../../utils/mergeDefaultProps'
7
+ import type { RangeBaseProps, RangeBaseVars } from './types'
8
+
9
+ const {
10
+ min = 0,
11
+ max = 100,
12
+ variant = 'default',
13
+ hasThumb = true,
14
+ isVertical = false,
15
+ ...props
16
+ } = defineProps<RangeBaseProps>()
17
+
18
+ const vars = computed(() =>
19
+ mergeDefaultProps<RangeBaseVars>({ thumb: { size: '2rem' } }, props.vars),
20
+ )
21
+
22
+ const range = defineModel<number>({ default: 50 })
23
+
24
+ const progressPercent = computed(() => {
25
+ const percent = ((range.value - min) / (max - min)) * 100
26
+ const thumbSize = cssSizeToNumber(vars.value.thumb?.size)
27
+ const unit = cssValueToUnit(vars.value.thumb?.size)
28
+ const thumbOffset = (0.5 - percent / 100) * thumbSize
29
+
30
+ return hasThumb ? `calc(${percent}% + ${thumbOffset}${unit})` : `${percent}%`
31
+ })
32
+
33
+ const progressBackground = computed(() => {
34
+ const color = variant === 'secondary' ? 'var(--muted)' : 'var(--primary)'
35
+ const fade = `oklch(from ${color} l c h / 0.75)`
36
+ return `linear-gradient(${isVertical ? 'to bottom' : 'to left'}, ${color}, ${fade})`
37
+ })
38
+
39
+ const thumbSize = computed(() => {
40
+ if (!hasThumb) return '0px'
41
+ return typeof vars.value.thumb?.size === 'number'
42
+ ? `${vars.value.thumb?.size}px`
43
+ : vars.value.thumb?.size
44
+ })
45
+
46
+ const varsStyle = useVars('range', [vars.value])
47
+
48
+ const progressStyle = computed(() => {
49
+ return isVertical
50
+ ? { height: progressPercent.value, width: '100%' }
51
+ : { width: progressPercent.value, height: '100%' }
52
+ })
53
+ </script>
54
+
55
+ <template>
56
+ <div :class="['container', { 'is-vertical': isVertical }, cls?.container]" :style="varsStyle">
57
+ <div :class="['wrapper', cls?.wrapper]">
58
+ <div :class="['progress', cls?.progress]" :style="progressStyle" />
59
+
60
+ <input
61
+ v-model="range"
62
+ type="range"
63
+ :min="min"
64
+ :max="max"
65
+ :class="['range-input', { 'hide-thumb': !hasThumb }, cls?.input]"
66
+ />
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ .container {
73
+ width: 100%;
74
+ }
75
+ .container.is-vertical {
76
+ width: fit-content;
77
+ height: stretch;
78
+ display: flex;
79
+ flex-direction: vertical;
80
+ align-items: end;
81
+ & .wrapper {
82
+ width: var(--range-progress-width, 1.6rem);
83
+ height: var(--range-progress-height);
84
+ /* Меняем ориентацию через writing-mode, но БЕЗ appearance: slider-vertical */
85
+ writing-mode: bt-lr;
86
+ writing-mode: vertical-lr;
87
+ direction: rtl;
88
+ }
89
+ }
90
+ .wrapper {
91
+ box-shadow: var(--shadow-inset);
92
+ border-radius: calc(1px * Infinity);
93
+ width: var(--range-progress-width, 100%);
94
+ height: var(--range-progress-height, 1.6rem);
95
+ position: relative;
96
+ display: flex;
97
+ align-items: center;
98
+ background-color: var(--background);
99
+ transition:
100
+ background-color 250ms ease-out,
101
+ box-shadow 250ms ease-out;
102
+ }
103
+ .progress {
104
+ border-radius: calc(1px * Infinity);
105
+ background: v-bind(progressBackground);
106
+ position: absolute;
107
+ }
108
+
109
+ .range-input {
110
+ -webkit-appearance: none;
111
+ appearance: none;
112
+ background: none;
113
+ position: relative;
114
+ width: 100%;
115
+ height: 100%;
116
+ border-radius: calc(1px * Infinity);
117
+ outline-offset: 0.4rem;
118
+ &:focus-visible {
119
+ outline: 0.2rem solid var(--foreground);
120
+ }
121
+ }
122
+ .range-input::-webkit-slider-thumb {
123
+ -webkit-appearance: none;
124
+ cursor: grab;
125
+ height: v-bind(thumbSize);
126
+ aspect-ratio: 1;
127
+ border-radius: calc(1px * Infinity);
128
+ background-color: var(--background);
129
+ border: 1px solid var(--color-highlight);
130
+ box-shadow: var(--shadow-inset);
131
+ transition:
132
+ background-color 250ms ease-out,
133
+ box-shadow 250ms ease-out;
134
+ }
135
+ .range-input::-moz-range-thumb {
136
+ -webkit-appearance: none;
137
+ cursor: grab;
138
+ height: v-bind(thumbSize);
139
+ aspect-ratio: 1;
140
+ border-radius: calc(1px * Infinity);
141
+ background-color: var(--background);
142
+ border: 1px solid var(--color-highlight);
143
+ box-shadow: var(--shadow-inset);
144
+ transition:
145
+ background-color 250ms ease-out,
146
+ box-shadow 250ms ease-out;
147
+ }
148
+ .range-input.hide-thumb::-webkit-slider-thumb {
149
+ cursor: default;
150
+ visibility: hidden;
151
+ }
152
+ .range-input.hide-thumb::-moz-range-thumb {
153
+ cursor: default;
154
+ visibility: hidden;
155
+ }
156
+ .range-input:not(.hide-thumb):active::-webkit-slider-thumb {
157
+ cursor: grabbing;
158
+ }
159
+
160
+ .range-input:active::-webkit-slider-thumb {
161
+ box-shadow: var(--shadow-raised);
162
+ }
163
+ .range-input:active::-moz-range-thumb {
164
+ box-shadow: var(--shadow-raised);
165
+ }
166
+ </style>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ range: number; max: number }>()
3
+ </script>
4
+
5
+ <template>
6
+ <output class="output" :style="{ minWidth: `${String(max).length}ch` }">
7
+ {{ range }}
8
+ </output>
9
+ </template>
10
+
11
+ <style scoped>
12
+ .output {
13
+ font-size: 1.5rem;
14
+ font-weight: bold;
15
+ font-variant-numeric: tabular-nums;
16
+ }
17
+ </style>
@@ -0,0 +1,16 @@
1
+ import { type CSSProperties } from 'vue'
2
+
3
+ export type RangeBaseVars = {
4
+ progress?: { height?: CSSProperties['height']; width?: CSSProperties['width'] }
5
+ thumb?: { size?: CSSProperties['height'] }
6
+ }
7
+
8
+ export type RangeBaseProps = {
9
+ min?: number
10
+ max?: number
11
+ variant?: 'default' | 'secondary'
12
+ hasThumb?: boolean
13
+ cls?: { input?: string; progress?: string; wrapper?: string; container?: string }
14
+ vars?: RangeBaseVars
15
+ isVertical?: boolean
16
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "button-base": {
3
+ "files": [
4
+ "ButtonBase.vue",
5
+ "index.ts",
6
+ "types.ts"
7
+ ]
8
+ },
9
+ "card-base": {
10
+ "files": [
11
+ "CardBase.vue",
12
+ "CardContent.vue",
13
+ "CardDescription.vue",
14
+ "CardFooter.vue",
15
+ "CardHeader.vue",
16
+ "CardTitle.vue"
17
+ ]
18
+ },
19
+ "input-base": {
20
+ "files": [
21
+ "InputBase.vue",
22
+ "types.ts"
23
+ ]
24
+ },
25
+ "range-base": {
26
+ "files": [
27
+ "RangeBase.vue",
28
+ "RangeOutput.vue",
29
+ "types.ts"
30
+ ]
31
+ },
32
+ "tabs": {
33
+ "files": [
34
+ "context.ts",
35
+ "TabsBase.vue",
36
+ "TabsIndicator.vue",
37
+ "TabsList.vue",
38
+ "TabsPanel.vue",
39
+ "TabsTab.vue",
40
+ "types.ts"
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,21 @@
1
+ <script lang="ts" setup>
2
+ import { provide, readonly, ref } from 'vue'
3
+
4
+ import { TabsKey } from './context'
5
+
6
+ const props = defineProps<{ defaultValue?: string }>()
7
+ const activeTab = ref(props.defaultValue || '')
8
+
9
+ const setActiveTab = (value: string) => (activeTab.value = value)
10
+
11
+ provide(TabsKey, {
12
+ activeTab: readonly(activeTab),
13
+ setActiveTab,
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <section class="tabs-base">
19
+ <slot />
20
+ </section>
21
+ </template>
@@ -0,0 +1,18 @@
1
+ <script lang="ts" setup></script>
2
+
3
+ <template>
4
+ <div class="indicator">
5
+ <slot />
6
+ </div>
7
+ </template>
8
+
9
+ <style scoped>
10
+ .indicator {
11
+ position: absolute;
12
+ border-radius: var(--radius-sm);
13
+ background-color: var(--background);
14
+ box-shadow: var(--shadow-raised);
15
+ transition: all 0.2s;
16
+ border: 0.1rem solid var(--background);
17
+ }
18
+ </style>
@@ -0,0 +1,28 @@
1
+ <script lang="ts" setup></script>
2
+
3
+ <template>
4
+ <div class="list">
5
+ <slot />
6
+ </div>
7
+ </template>
8
+
9
+ <style scoped>
10
+ .list {
11
+ position: relative;
12
+ padding: 0.4rem;
13
+ display: inline-flex;
14
+ height: 4.8rem;
15
+ align-items: center;
16
+ justify-content: center;
17
+ gap: 1.6rem;
18
+ border-radius: var(--radius-2xl);
19
+ background: linear-gradient(
20
+ to left,
21
+ var(--background),
22
+ oklch(from var(--highlight) l c h / 0.75)
23
+ );
24
+ color: var(--muted-foreground);
25
+ box-shadow: var(--shadow-raised);
26
+ border: 0.3rem solid var(--background);
27
+ }
28
+ </style>
@@ -0,0 +1,16 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+
4
+ import { useTabsContext } from './context'
5
+
6
+ const props = defineProps<{ value: string }>()
7
+ const { activeTab } = useTabsContext()
8
+
9
+ const isActive = computed(() => activeTab.value === props.value)
10
+ </script>
11
+
12
+ <template>
13
+ <section v-if="isActive" class="content">
14
+ <slot />
15
+ </section>
16
+ </template>
@@ -0,0 +1,49 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue'
3
+
4
+ import { useTabsContext } from './context'
5
+
6
+ const props = defineProps<{ value: string }>()
7
+ const { activeTab, setActiveTab } = useTabsContext()
8
+
9
+ const selectTab = () => setActiveTab(props.value)
10
+
11
+ const isSelected = computed(() => activeTab.value === props.value || null)
12
+ </script>
13
+
14
+ <template>
15
+ <button :data-selected="isSelected" @click="selectTab" type="button">
16
+ <slot />
17
+ </button>
18
+ </template>
19
+
20
+ <style scoped>
21
+ button {
22
+ display: inline-flex;
23
+ justify-content: center;
24
+ align-items: center;
25
+ white-space: nowrap;
26
+ border-radius: var(--radius-xl);
27
+ padding: 0.8rem 1.6rem;
28
+ transition:
29
+ color 0.2s ease-in-out,
30
+ border-color 0.2s ease-in-out,
31
+ box-shadow 0.2s ease-in-out;
32
+ position: relative;
33
+ z-index: 10;
34
+ font-size: var(--text-sm);
35
+ font-weight: var(--font-weight-medium);
36
+ &:focus-visible {
37
+ outline: 0.2rem solid var(--foreground);
38
+ outline-offset: 0.2rem;
39
+ }
40
+ &:hover {
41
+ color: var(--foreground);
42
+ }
43
+ &[data-selected] {
44
+ background-color: var(--background);
45
+ color: var(--foreground);
46
+ box-shadow: var(--shadow-inset);
47
+ }
48
+ }
49
+ </style>
@@ -0,0 +1,12 @@
1
+ import { type InjectionKey, inject } from 'vue'
2
+
3
+ import type { TabsContext } from './types'
4
+
5
+ export const TabsKey: InjectionKey<TabsContext> = Symbol('TabsContext')
6
+
7
+ export function useTabsContext() {
8
+ const context = inject(TabsKey)
9
+ if (!context) throw new Error('Tabs components must be used within TabsBase')
10
+
11
+ return context
12
+ }
@@ -0,0 +1,6 @@
1
+ import type { Ref } from 'vue'
2
+
3
+ export type TabsContext = {
4
+ activeTab: Readonly<Ref<string>>
5
+ setActiveTab: (value: string) => void
6
+ }
File without changes
File without changes