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 +93 -177
- package/package.json +6 -7
- package/registry/button-base/ButtonBase.vue +82 -0
- package/registry/button-base/index.ts +2 -0
- package/registry/button-base/types.ts +10 -0
- package/registry/card-base/CardBase.vue +23 -0
- package/registry/card-base/CardContent.vue +15 -0
- package/registry/card-base/CardDescription.vue +18 -0
- package/registry/card-base/CardFooter.vue +11 -0
- package/registry/card-base/CardHeader.vue +14 -0
- package/registry/card-base/CardTitle.vue +19 -0
- package/registry/input-base/InputBase.vue +70 -0
- package/registry/input-base/types.ts +7 -0
- package/registry/range-base/RangeBase.vue +166 -0
- package/registry/range-base/RangeOutput.vue +17 -0
- package/registry/range-base/types.ts +16 -0
- package/registry/registry.json +43 -0
- package/registry/tabs/TabsBase.vue +21 -0
- package/registry/tabs/TabsIndicator.vue +18 -0
- package/registry/tabs/TabsList.vue +28 -0
- package/registry/tabs/TabsPanel.vue +16 -0
- package/registry/tabs/TabsTab.vue +49 -0
- package/registry/tabs/context.ts +12 -0
- package/registry/tabs/types.ts +6 -0
- /package/{src → registry}/styles/style.css +0 -0
- /package/{src → registry}/styles/variables.css +0 -0
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
|
|
12
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const stylesPath = path.resolve(cwd, 'src/styles')
|
|
56
|
+
function addComponent(name) {
|
|
57
|
+
const registry = readRegistry()
|
|
58
|
+
const entry = registry[name]
|
|
99
59
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
87
|
+
console.log(`\n✔ ${name} installed`)
|
|
88
|
+
}
|
|
155
89
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
94
|
+
if (!fs.existsSync(registryStyles)) {
|
|
95
|
+
console.log('No global styles in registry')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
162
98
|
|
|
163
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
177
|
-
function capitalize(str) {
|
|
178
|
-
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
105
|
+
console.log('\n✔ styles installed')
|
|
179
106
|
}
|
|
180
107
|
|
|
181
|
-
|
|
182
|
-
return str
|
|
183
|
-
.split('-')
|
|
184
|
-
.map((part) => capitalize(part))
|
|
185
|
-
.join('')
|
|
186
|
-
}
|
|
108
|
+
// ---------- CLI ----------
|
|
187
109
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
206
|
-
console.log(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
127
|
+
listComponents()
|
|
215
128
|
process.exit(0)
|
|
216
129
|
}
|
|
217
130
|
|
|
218
|
-
if (command === 'add'
|
|
131
|
+
if (command === 'add') {
|
|
219
132
|
if (!component) {
|
|
220
|
-
console.error('
|
|
221
|
-
console.error('Usage: artharexian-ui add <component>')
|
|
133
|
+
console.error('Component name required')
|
|
222
134
|
process.exit(1)
|
|
223
135
|
}
|
|
224
|
-
|
|
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.
|
|
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
|
-
"
|
|
20
|
-
"!**/meta.json"
|
|
18
|
+
"registry"
|
|
21
19
|
],
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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
|
-
"
|
|
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,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,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,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
|
+
}
|
|
File without changes
|
|
File without changes
|