rxn-ui 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +5 -0
  2. package/cli/index.mjs +119 -0
  3. package/cli/registry.json +57 -0
  4. package/package.json +61 -0
  5. package/src/components/button-base/ButtonBase.vue +82 -0
  6. package/src/components/button-base/index.stories.ts +108 -0
  7. package/src/components/button-base/types.ts +10 -0
  8. package/src/components/card-base/CardBase.vue +23 -0
  9. package/src/components/card-base/CardContent.vue +15 -0
  10. package/src/components/card-base/CardDescription.vue +18 -0
  11. package/src/components/card-base/CardFooter.vue +11 -0
  12. package/src/components/card-base/CardHeader.vue +14 -0
  13. package/src/components/card-base/CardTitle.vue +19 -0
  14. package/src/components/checkbox-base/CheckboxBase.vue +69 -0
  15. package/src/components/input-base/InputBase.vue +70 -0
  16. package/src/components/input-base/types.ts +7 -0
  17. package/src/components/range-base/RangeBase.vue +166 -0
  18. package/src/components/range-base/RangeOutput.vue +17 -0
  19. package/src/components/range-base/types.ts +16 -0
  20. package/src/components/switch-base/SwitchBase.vue +71 -0
  21. package/src/components/tabs/TabsBase.vue +21 -0
  22. package/src/components/tabs/TabsIndicator.vue +18 -0
  23. package/src/components/tabs/TabsList.vue +28 -0
  24. package/src/components/tabs/TabsPanel.vue +16 -0
  25. package/src/components/tabs/TabsTab.vue +49 -0
  26. package/src/components/tabs/context.ts +12 -0
  27. package/src/components/tabs/types.ts +6 -0
  28. package/src/components/theme-toggle/ThemeToggle.vue +30 -0
  29. package/src/styles/style.css +136 -0
  30. package/src/styles/variables.css +408 -0
@@ -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,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,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,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,136 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@14..36,300;14..36,400;14..36,500;14..36,600;14..36,700&display=swap');
2
+ @import 'variables.css';
3
+ @layer theme, base, components, utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ &[data-theme='light'] {
8
+ color-scheme: light;
9
+ }
10
+ &[data-theme='dark'] {
11
+ color-scheme: dark;
12
+ }
13
+
14
+ color-scheme: light dark;
15
+ scroll-behavior: smooth;
16
+ font-size: 10px;
17
+ font-family: 'DM Sans', sans-serif;
18
+ }
19
+
20
+ body {
21
+ background: var(--background);
22
+ color: var(--foreground);
23
+ -webkit-font-smoothing: antialiased;
24
+ font-size: 1.6rem;
25
+ }
26
+
27
+ *,
28
+ *::before,
29
+ *::after {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ border: 0 solid;
34
+ }
35
+
36
+ :disabled,
37
+ [aria-busy='true'],
38
+ .is-disabled {
39
+ pointer-events: none;
40
+ color: var(--color-neutral-500);
41
+ opacity: 0.45;
42
+ }
43
+
44
+ h1,
45
+ h2,
46
+ h3,
47
+ h4,
48
+ h5,
49
+ h6 {
50
+ text-wrap: balance;
51
+ font-size: inherit;
52
+ font-weight: inherit;
53
+ }
54
+
55
+ textarea {
56
+ resize: vertical;
57
+ }
58
+
59
+ img,
60
+ picture,
61
+ svg,
62
+ video {
63
+ display: block;
64
+ max-width: 100%;
65
+ height: auto;
66
+ }
67
+
68
+ input,
69
+ button,
70
+ textarea,
71
+ select {
72
+ font: inherit;
73
+ }
74
+
75
+ a {
76
+ text-decoration: none;
77
+ color: inherit;
78
+ &:hover {
79
+ text-decoration: underline;
80
+ }
81
+ }
82
+
83
+ ol,
84
+ ul,
85
+ menu {
86
+ list-style: none;
87
+ }
88
+ }
89
+
90
+ @layer components {
91
+ h1 {
92
+ font-size: var(--text-2xl);
93
+ }
94
+ h2 {
95
+ font-size: var(--text-xl);
96
+ }
97
+ h3 {
98
+ font-size: var(--text-lg);
99
+ }
100
+ ::placeholder {
101
+ color: var(--muted-foreground);
102
+ }
103
+
104
+ .font-xs {
105
+ font-size: var(--text-xs);
106
+ /* line-height: var(--leading-relaxed); */
107
+ }
108
+ .font-sm {
109
+ font-size: var(--text-sm);
110
+ /* line-height: var(--leading-normal); */
111
+ }
112
+ .font-base {
113
+ font-size: var(--text-base);
114
+ /* line-height: var(--leading-normal); */
115
+ }
116
+ .font-lg {
117
+ font-size: var(--text-lg);
118
+ /* line-height: var(--leading-normal); */
119
+ }
120
+ .font-xl {
121
+ font-size: var(--text-xl);
122
+ /* line-height: var(--leading-snug); */
123
+ }
124
+ .font-2xl {
125
+ font-size: var(--text-2xl);
126
+ /* line-height: var(--leading-tight); */
127
+ }
128
+ .font-3xl {
129
+ font-size: var(--text-3xl);
130
+ /* line-height: 1; */
131
+ }
132
+ .font-4xl {
133
+ font-size: var(--text-4xl);
134
+ /* line-height: 1; */
135
+ }
136
+ }