artharexian-ui 0.2.7 → 0.2.9

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 (35) hide show
  1. package/cli/index.mjs +1 -1
  2. package/package.json +2 -2
  3. package/src/components/CheckboxBase.vue +69 -0
  4. package/src/components/SwitchBase.vue +71 -0
  5. package/src/components/ThemeToggle.vue +30 -0
  6. package/src/components/button-base/ButtonBase.vue +82 -0
  7. package/src/components/button-base/index.stories.ts +108 -0
  8. package/src/components/button-base/index.ts +2 -0
  9. package/src/components/button-base/meta.json +10 -0
  10. package/src/components/button-base/style.css +2 -0
  11. package/src/components/button-base/types.ts +10 -0
  12. package/src/components/card-base/CardBase.vue +23 -0
  13. package/src/components/card-base/CardContent.vue +15 -0
  14. package/src/components/card-base/CardDescription.vue +18 -0
  15. package/src/components/card-base/CardFooter.vue +11 -0
  16. package/src/components/card-base/CardHeader.vue +14 -0
  17. package/src/components/card-base/CardTitle.vue +19 -0
  18. package/src/components/input-base/InputBase.vue +70 -0
  19. package/src/components/input-base/types.ts +7 -0
  20. package/src/components/range-base/RangeBase.vue +166 -0
  21. package/src/components/range-base/RangeOutput.vue +17 -0
  22. package/src/components/range-base/types.ts +16 -0
  23. package/src/components/tabs/TabsBase.vue +21 -0
  24. package/src/components/tabs/TabsIndicator.vue +18 -0
  25. package/src/components/tabs/TabsList.vue +28 -0
  26. package/src/components/tabs/TabsPanel.vue +16 -0
  27. package/src/components/tabs/TabsTab.vue +49 -0
  28. package/src/components/tabs/context.ts +12 -0
  29. package/src/components/tabs/types.ts +6 -0
  30. package/src/components/ui/button-base/ButtonBase.vue +82 -0
  31. package/src/components/ui/button-base/index.stories.ts +108 -0
  32. package/src/components/ui/button-base/index.ts +2 -0
  33. package/src/components/ui/button-base/meta.json +10 -0
  34. package/src/components/ui/button-base/style.css +2 -0
  35. package/src/components/ui/button-base/types.ts +10 -0
package/cli/index.mjs CHANGED
@@ -14,7 +14,7 @@ const component = args[1]
14
14
  const cwd = process.cwd()
15
15
 
16
16
  // Try installed package first, then fallback to local components (for development)
17
- let registryRoot = path.resolve(cwd, 'node_modules/artharexian-ui/registry')
17
+ let registryRoot = path.resolve(cwd, 'node_modules/artharexian-ui/src/components')
18
18
  if (!fs.existsSync(registryRoot)) {
19
19
  const localRegistry = path.resolve(__dirname, '../src/components')
20
20
  if (fs.existsSync(localRegistry)) {
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.2.7",
4
+ "version": "0.2.9",
5
5
  "license": "MIT",
6
6
  "private": false,
7
7
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "files": [
20
20
  "dist",
21
21
  "src/styles",
22
- "registry",
22
+ "src/components",
23
23
  "cli"
24
24
  ],
25
25
  "exports": {
@@ -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,71 @@
1
+ <script setup lang="ts">
2
+ const modelValue = defineModel({ default: false })
3
+ defineProps<{ isDisabled?: boolean }>()
4
+ </script>
5
+
6
+ <template>
7
+ <label :class="['wrapper', { 'is-disabled': isDisabled }]">
8
+ <input type="checkbox" v-model="modelValue" />
9
+ <div class="switch">
10
+ <div class="thumb" />
11
+ </div>
12
+ <slot />
13
+ </label>
14
+ </template>
15
+
16
+ <style scoped>
17
+ .wrapper {
18
+ display: inline-flex;
19
+ cursor: pointer;
20
+ }
21
+
22
+ input {
23
+ appearance: none;
24
+ &:focus-visible + .switch {
25
+ outline: 0.2rem solid var(--foreground);
26
+ outline-offset: 0.4rem;
27
+ }
28
+ &:checked + .switch {
29
+ background-color: var(--primary);
30
+ border: 0.1rem solid var(--primary);
31
+ }
32
+ &:checked + .switch .thumb {
33
+ transform: translateX(2.6rem);
34
+ &::before {
35
+ background-color: var(--primary);
36
+ }
37
+ }
38
+ }
39
+
40
+ .switch {
41
+ height: 2.2rem;
42
+ width: 4.8rem;
43
+ border-radius: calc(1px * Infinity);
44
+ background-color: var(--background);
45
+ box-shadow: var(--shadow-inset);
46
+ border: 0.1rem solid var(--color-highlight);
47
+ transition:
48
+ background-color 250ms ease-out,
49
+ box-shadow 250ms ease-out;
50
+ }
51
+
52
+ .thumb {
53
+ display: grid;
54
+ place-content: center;
55
+ height: 2rem;
56
+ aspect-ratio: 1;
57
+ border-radius: calc(1px * Infinity);
58
+ background-color: var(--background);
59
+ box-shadow: var(--shadow-inset);
60
+ transform: translateX(0);
61
+ transition: all 200ms ease-out;
62
+ border: 0.1rem solid var(--color-highlight);
63
+ &::before {
64
+ content: '';
65
+ width: 0.4rem;
66
+ aspect-ratio: 1;
67
+ background-color: var(--muted-foreground);
68
+ border-radius: calc((1px * Infinity));
69
+ }
70
+ }
71
+ </style>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import MoonIcon from '../assets/MoonIcon.vue'
3
+ import SunIcon from '../assets/SunIcon.vue'
4
+ import useTheme from '../composables/useTheme'
5
+ import ButtonBase from './button-base/ButtonBase.vue'
6
+
7
+ const { currentTheme, toggleTheme } = useTheme()
8
+ </script>
9
+
10
+ <template>
11
+ <ButtonBase
12
+ @click="toggleTheme()"
13
+ :aria-label="`Switch to ${currentTheme === 'dark' ? 'light' : 'dark'} theme`"
14
+ class="theme-toggle-btn"
15
+ shape="radius-circle"
16
+ >
17
+ <MoonIcon v-if="currentTheme === 'dark'" class="moon" />
18
+ <SunIcon v-else />
19
+ </ButtonBase>
20
+ </template>
21
+
22
+ <style scoped>
23
+ .theme-toggle-btn {
24
+ font-size: 1.5rem;
25
+ }
26
+ .moon {
27
+ height: 2rem;
28
+ width: 2rem;
29
+ }
30
+ </style>
@@ -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,2 @@
1
+ export { default } from './ButtonBase.vue'
2
+ export * from './types'
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "button-base",
3
+ "files": [
4
+ "ButtonBase.vue",
5
+ "types.ts",
6
+ "index.ts",
7
+ "style.css"
8
+ ],
9
+ "css": true
10
+ }
@@ -0,0 +1,2 @@
1
+ /* ButtonBase styles are scoped in the component */
2
+ /* This file is included for consistency with the registry pattern */
@@ -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,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
+ }
@@ -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,2 @@
1
+ export { default } from './ButtonBase.vue'
2
+ export * from './types'
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "button-base",
3
+ "files": [
4
+ "ButtonBase.vue",
5
+ "types.ts",
6
+ "index.ts",
7
+ "style.css"
8
+ ],
9
+ "css": true
10
+ }
@@ -0,0 +1,2 @@
1
+ /* ButtonBase styles are scoped in the component */
2
+ /* This file is included for consistency with the registry pattern */
@@ -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
+ }