@firerian/fireui 1.0.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/fireui.cjs +2 -0
  4. package/dist/fireui.cjs.map +1 -0
  5. package/dist/fireui.css +1 -0
  6. package/dist/fireui.es.mjs +3867 -0
  7. package/dist/fireui.es.mjs.map +1 -0
  8. package/dist/fireui.umd.js +2 -0
  9. package/dist/fireui.umd.js.map +1 -0
  10. package/dist/types/index.d.ts +1590 -0
  11. package/package.json +132 -0
  12. package/src/components/button/button.test.ts +357 -0
  13. package/src/components/button/button.vue +366 -0
  14. package/src/components/button/index.ts +17 -0
  15. package/src/components/button/types.ts +76 -0
  16. package/src/components/form/form-item.vue +136 -0
  17. package/src/components/form/form.vue +76 -0
  18. package/src/components/form/index.ts +16 -0
  19. package/src/components/grid/col.vue +99 -0
  20. package/src/components/grid/index.ts +16 -0
  21. package/src/components/grid/row.vue +85 -0
  22. package/src/components/grid/types.ts +66 -0
  23. package/src/components/index.ts +36 -0
  24. package/src/components/input/index.ts +8 -0
  25. package/src/components/input/input.test.ts +129 -0
  26. package/src/components/input/input.vue +256 -0
  27. package/src/components/input/types.ts +100 -0
  28. package/src/components/layout/aside.vue +89 -0
  29. package/src/components/layout/container.vue +53 -0
  30. package/src/components/layout/footer.vue +57 -0
  31. package/src/components/layout/header.vue +56 -0
  32. package/src/components/layout/index.ts +28 -0
  33. package/src/components/layout/main.vue +36 -0
  34. package/src/components/layout/types.ts +74 -0
  35. package/src/components/table/index.ts +16 -0
  36. package/src/components/table/table-column.vue +69 -0
  37. package/src/components/table/table.vue +354 -0
  38. package/src/components/tips/index.ts +12 -0
  39. package/src/components/tips/tips.test.ts +96 -0
  40. package/src/components/tips/tips.vue +206 -0
  41. package/src/components/tips/types.ts +56 -0
  42. package/src/components/tooltip/index.ts +8 -0
  43. package/src/components/tooltip/tooltip.test.ts +187 -0
  44. package/src/components/tooltip/tooltip.vue +261 -0
  45. package/src/components/tooltip/types.ts +60 -0
  46. package/src/hooks/useForm.ts +233 -0
  47. package/src/hooks/useTable.ts +153 -0
  48. package/src/index.ts +48 -0
  49. package/src/styles/main.scss +6 -0
  50. package/src/styles/mixins.scss +48 -0
  51. package/src/styles/reset.scss +49 -0
  52. package/src/styles/variables.scss +137 -0
  53. package/src/types/component.ts +9 -0
  54. package/src/types/form.ts +149 -0
  55. package/src/types/global.d.ts +49 -0
  56. package/src/types/grid.ts +76 -0
  57. package/src/types/index.ts +23 -0
  58. package/src/types/table.ts +181 -0
  59. package/src/types/tooltip.ts +44 -0
  60. package/src/utils/auto-import.ts +41 -0
  61. package/src/utils/index.ts +2 -0
  62. package/src/utils/install.ts +20 -0
  63. package/src/utils/useNamespace.ts +29 -0
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ns.b,
5
+ {
6
+ [ns.m(`span-${span}`)]: span,
7
+ [ns.m(`offset-${offset}`)]: offset
8
+ },
9
+ ...responsiveClasses
10
+ ]"
11
+ :style="colStyle"
12
+ >
13
+ <slot></slot>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { computed } from 'vue'
19
+ import { useNamespace } from '../../utils'
20
+ import type { ColProps } from '../../types/grid'
21
+
22
+ const ns = useNamespace('col')
23
+
24
+ const props = withDefaults(defineProps<ColProps>(), {
25
+ span: 24,
26
+ offset: 0
27
+ })
28
+
29
+ const responsiveClasses = computed(() => {
30
+ const classes: string[] = []
31
+ const breakpoints = ['xs', 'sm', 'md', 'lg', 'xl'] as const
32
+
33
+ breakpoints.forEach((breakpoint) => {
34
+ const value = props[breakpoint]
35
+ if (value) {
36
+ if (typeof value === 'number') {
37
+ classes.push(ns.m(`${breakpoint}-span-${value}`))
38
+ } else {
39
+ if (value.span) {
40
+ classes.push(ns.m(`${breakpoint}-span-${value.span}`))
41
+ }
42
+ if (value.offset) {
43
+ classes.push(ns.m(`${breakpoint}-offset-${value.offset}`))
44
+ }
45
+ }
46
+ }
47
+ })
48
+
49
+ return classes
50
+ })
51
+
52
+ const colStyle = computed(() => {
53
+ return {
54
+ padding: `calc(var(--fire-row-gutter-vertical, 0) * 0.5) calc(var(--fire-row-gutter-horizontal, 0) * 0.5)`
55
+ }
56
+ })
57
+ </script>
58
+
59
+ <style scoped lang="scss">
60
+ @import '../../styles/variables.scss';
61
+
62
+ .#{$namespace}-col {
63
+ box-sizing: border-box;
64
+ flex-shrink: 0;
65
+ min-width: 0;
66
+ max-width: 100%;
67
+
68
+ @for $i from 1 through 24 {
69
+ &--span-#{$i} {
70
+ flex: 0 0 calc(#{$i} / 24 * 100%);
71
+ max-width: calc(#{$i} / 24 * 100%);
72
+ }
73
+
74
+ &--offset-#{$i} {
75
+ margin-left: calc(#{$i} / 24 * 100%);
76
+ }
77
+
78
+ // 响应式断点
79
+ @each $breakpoint, $value in (
80
+ xs: 0,
81
+ sm: 576px,
82
+ md: 768px,
83
+ lg: 992px,
84
+ xl: 1200px
85
+ ) {
86
+ @media (min-width: $value) {
87
+ &--#{$breakpoint}-span-#{$i} {
88
+ flex: 0 0 calc(#{$i} / 24 * 100%);
89
+ max-width: calc(#{$i} / 24 * 100%);
90
+ }
91
+
92
+ &--#{$breakpoint}-offset-#{$i} {
93
+ margin-left: calc(#{$i} / 24 * 100%);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ </style>
@@ -0,0 +1,16 @@
1
+ import Row from './row.vue'
2
+ import Col from './col.vue'
3
+ import { withInstall } from '../../utils'
4
+
5
+ const FireRow = withInstall(Row)
6
+ const FireCol = withInstall(Col)
7
+
8
+ export {
9
+ FireRow,
10
+ FireCol
11
+ }
12
+
13
+ export default {
14
+ FireRow,
15
+ FireCol
16
+ }
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ns.b,
5
+ {
6
+ [ns.m(`justify-${justify}`)]: justify,
7
+ [ns.m(`align-${align}`)]: align
8
+ }
9
+ ]"
10
+ :style="rowStyle"
11
+ >
12
+ <slot></slot>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { computed } from 'vue'
18
+ import { useNamespace } from '../../utils'
19
+ import type { RowProps } from '../../types/grid'
20
+
21
+ const ns = useNamespace('row')
22
+
23
+ const props = withDefaults(defineProps<RowProps>(), {
24
+ gutter: 0,
25
+ justify: 'start',
26
+ align: 'top'
27
+ })
28
+
29
+ const rowStyle = computed(() => {
30
+ const style: Record<string, string> = {}
31
+ if (props.gutter) {
32
+ const [horizontal, vertical] = Array.isArray(props.gutter) ? props.gutter : [props.gutter, props.gutter]
33
+ style['--fire-row-gutter-horizontal'] = `${horizontal}px`
34
+ style['--fire-row-gutter-vertical'] = `${vertical}px`
35
+ }
36
+ return style
37
+ })
38
+ </script>
39
+
40
+ <style scoped lang="scss">
41
+ @import '../../styles/variables.scss';
42
+
43
+ .#{$namespace}-row {
44
+ display: flex;
45
+ flex-wrap: wrap;
46
+ margin: 0 calc(var(--fire-row-gutter-horizontal, 0) * -0.5);
47
+ margin-top: calc(var(--fire-row-gutter-vertical, 0) * -0.5);
48
+
49
+ &--justify-start {
50
+ justify-content: flex-start;
51
+ }
52
+
53
+ &--justify-end {
54
+ justify-content: flex-end;
55
+ }
56
+
57
+ &--justify-center {
58
+ justify-content: center;
59
+ }
60
+
61
+ &--justify-space-between {
62
+ justify-content: space-between;
63
+ }
64
+
65
+ &--justify-space-around {
66
+ justify-content: space-around;
67
+ }
68
+
69
+ &--justify-space-evenly {
70
+ justify-content: space-evenly;
71
+ }
72
+
73
+ &--align-top {
74
+ align-items: flex-start;
75
+ }
76
+
77
+ &--align-middle {
78
+ align-items: center;
79
+ }
80
+
81
+ &--align-bottom {
82
+ align-items: flex-end;
83
+ }
84
+ }
85
+ </style>
@@ -0,0 +1,66 @@
1
+ export interface ResponsiveProps {
2
+ span?: number
3
+ offset?: number
4
+ }
5
+
6
+ export interface RowProps {
7
+ /**
8
+ * 栅格间距
9
+ */
10
+ gutter?: number | [number, number]
11
+ /**
12
+ * 水平对齐方式
13
+ */
14
+ justify?: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly'
15
+ /**
16
+ * 垂直对齐方式
17
+ */
18
+ align?: 'top' | 'middle' | 'bottom'
19
+ /**
20
+ * 自定义类名
21
+ */
22
+ class?: string
23
+ /**
24
+ * 自定义样式
25
+ */
26
+ style?: string | object
27
+ }
28
+
29
+ export interface ColProps {
30
+ /**
31
+ * 占列数
32
+ */
33
+ span?: number
34
+ /**
35
+ * 偏移量
36
+ */
37
+ offset?: number
38
+ /**
39
+ * 超小屏幕响应式配置
40
+ */
41
+ xs?: number | ResponsiveProps
42
+ /**
43
+ * 小屏幕响应式配置
44
+ */
45
+ sm?: number | ResponsiveProps
46
+ /**
47
+ * 中等屏幕响应式配置
48
+ */
49
+ md?: number | ResponsiveProps
50
+ /**
51
+ * 大屏幕响应式配置
52
+ */
53
+ lg?: number | ResponsiveProps
54
+ /**
55
+ * 超大屏幕响应式配置
56
+ */
57
+ xl?: number | ResponsiveProps
58
+ /**
59
+ * 自定义类名
60
+ */
61
+ class?: string
62
+ /**
63
+ * 自定义样式
64
+ */
65
+ style?: string | object
66
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file 组件统一导出
3
+ * @description 导出所有 UI 组件
4
+ */
5
+
6
+ // Button 组件
7
+ export { FButton } from './button'
8
+ export type { ButtonInstance, ButtonProps, ButtonEmits, ButtonSlots } from './button'
9
+
10
+ // Grid 组件
11
+ export { FireRow, FireCol } from './grid'
12
+ export type { RowProps, ColProps, ResponsiveProps } from './grid/types'
13
+
14
+ // Layout 组件
15
+ export { FireContainer, FireHeader, FireAside, FireMain, FireFooter } from './layout'
16
+ export type { ContainerProps, HeaderProps, AsideProps, MainProps, FooterProps } from './layout/types'
17
+
18
+ // Tooltip 组件
19
+ export { FireTooltip } from './tooltip'
20
+ export type { TooltipProps, TooltipEmits, TooltipSlots } from './tooltip/types'
21
+
22
+ // Tips 组件
23
+ export { FireTips } from './tips'
24
+ export type { TipsProps, TipsEmits, TipsSlots } from './tips/types'
25
+
26
+ // Input 组件
27
+ export { FireInput } from './input'
28
+ export type { InputProps, InputEmits, InputSlots } from './input/types'
29
+
30
+ // Form 组件
31
+ export { FireForm, FireFormItem } from './form'
32
+ export type { FormProps, FormEmits, FormInstance, FormItemProps, FormItemEmits, FormItemInstance } from '../types/form'
33
+
34
+ // Table 组件
35
+ export { FireTable, FireTableColumn } from './table'
36
+ export type { TableProps, TableEmits, TableInstance, TableColumnProps, TableColumnSlots } from '../types/table'
@@ -0,0 +1,8 @@
1
+ import Input from './input.vue'
2
+ import { withInstall } from '../../utils'
3
+ import type { App } from 'vue'
4
+
5
+ const FireInput = withInstall(Input as any) as typeof Input & { install: (app: App) => void }
6
+
7
+ export { FireInput }
8
+ export default FireInput
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { FireInput } from './index'
4
+
5
+ describe('FireInput', () => {
6
+ it('should render correctly', () => {
7
+ const wrapper = mount(FireInput, {
8
+ props: {
9
+ modelValue: 'test'
10
+ }
11
+ })
12
+
13
+ expect(wrapper.exists()).toBe(true)
14
+ expect(wrapper.find('input').element.value).toBe('test')
15
+ })
16
+
17
+ it('should update modelValue when input', async () => {
18
+ const wrapper = mount(FireInput, {
19
+ props: {
20
+ modelValue: ''
21
+ }
22
+ })
23
+
24
+ const input = wrapper.find('input')
25
+ await input.setValue('new value')
26
+ await input.trigger('input')
27
+
28
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value'])
29
+ expect(wrapper.emitted('input')?.[0]).toEqual(['new value'])
30
+ })
31
+
32
+ it('should emit focus and blur events', async () => {
33
+ const wrapper = mount(FireInput)
34
+
35
+ const input = wrapper.find('input')
36
+ await input.trigger('focus')
37
+ expect(wrapper.emitted('focus')).toBeTruthy()
38
+
39
+ await input.trigger('blur')
40
+ expect(wrapper.emitted('blur')).toBeTruthy()
41
+ })
42
+
43
+ it('should emit enter event', async () => {
44
+ const wrapper = mount(FireInput)
45
+
46
+ const input = wrapper.find('input')
47
+ await input.trigger('keydown.enter')
48
+ expect(wrapper.emitted('enter')).toBeTruthy()
49
+ })
50
+
51
+ it('should be disabled', () => {
52
+ const wrapper = mount(FireInput, {
53
+ props: {
54
+ disabled: true
55
+ }
56
+ })
57
+
58
+ const input = wrapper.find('input')
59
+ expect(input.element.disabled).toBe(true)
60
+ expect(wrapper.classes()).toContain('fire-input--disabled')
61
+ })
62
+
63
+ it('should be readonly', () => {
64
+ const wrapper = mount(FireInput, {
65
+ props: {
66
+ readonly: true
67
+ }
68
+ })
69
+
70
+ const input = wrapper.find('input')
71
+ expect(input.element.readOnly).toBe(true)
72
+ })
73
+
74
+ it('should show word limit', () => {
75
+ const wrapper = mount(FireInput, {
76
+ props: {
77
+ modelValue: 'test',
78
+ maxlength: 10,
79
+ showWordLimit: true
80
+ }
81
+ })
82
+
83
+ expect(wrapper.find('.fire-input__word-limit').text()).toBe('4/10')
84
+ })
85
+
86
+ it('should toggle password visibility', async () => {
87
+ const wrapper = mount(FireInput, {
88
+ props: {
89
+ password: true
90
+ }
91
+ })
92
+
93
+ const input = wrapper.find('input')
94
+ expect(input.element.type).toBe('password')
95
+
96
+ const toggleButton = wrapper.find('.fire-input__password-icon')
97
+ await toggleButton.trigger('click')
98
+ expect(input.element.type).toBe('text')
99
+
100
+ await toggleButton.trigger('click')
101
+ expect(input.element.type).toBe('password')
102
+ })
103
+
104
+ it('should render different sizes', () => {
105
+ const sizes = ['large', 'medium', 'small']
106
+
107
+ sizes.forEach((size) => {
108
+ const wrapper = mount(FireInput, {
109
+ props: {
110
+ size: size as 'large' | 'medium' | 'small'
111
+ }
112
+ })
113
+
114
+ expect(wrapper.classes()).toContain(`fire-input--${size}`)
115
+ })
116
+ })
117
+
118
+ it('should render prefix and suffix slots', () => {
119
+ const wrapper = mount(FireInput, {
120
+ slots: {
121
+ prefix: '<span class="prefix">P</span>',
122
+ suffix: '<span class="suffix">S</span>'
123
+ }
124
+ })
125
+
126
+ expect(wrapper.find('.prefix').exists()).toBe(true)
127
+ expect(wrapper.find('.suffix').exists()).toBe(true)
128
+ })
129
+ })
@@ -0,0 +1,256 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ ns.b,
5
+ ns.m(size),
6
+ {
7
+ [ns.m('disabled')]: disabled,
8
+ [ns.m('prefix')]: $slots.prefix || prefixIcon,
9
+ [ns.m('suffix')]: $slots.suffix || suffixIcon || showWordLimit
10
+ }
11
+ ]"
12
+ >
13
+ <div v-if="$slots.prefix || prefixIcon" class="fire-input__prefix">
14
+ <slot name="prefix">
15
+ <component :is="prefixIcon" v-if="prefixIcon" />
16
+ </slot>
17
+ </div>
18
+ <input
19
+ ref="inputRef"
20
+ :type="password ? (showPassword ? 'text' : 'password') : type"
21
+ :value="modelValue"
22
+ :placeholder="placeholder"
23
+ :disabled="disabled"
24
+ :readonly="readonly"
25
+ :maxlength="maxlength"
26
+ :minlength="minlength"
27
+ @input="handleInput"
28
+ @focus="handleFocus"
29
+ @blur="handleBlur"
30
+ @keydown.enter="handleEnter"
31
+ class="fire-input__inner"
32
+ />
33
+ <div v-if="$slots.suffix || suffixIcon || showWordLimit || (password && !disabled)" class="fire-input__suffix">
34
+ <span v-if="showWordLimit" class="fire-input__word-limit">{{ (modelValue || '').toString().length }}/{{ maxlength }}</span>
35
+ <div v-if="password && !disabled" class="fire-input__password-toggle">
36
+ <button
37
+ type="button"
38
+ @click="togglePassword"
39
+ class="fire-input__password-icon"
40
+ aria-label="Toggle password visibility"
41
+ >
42
+ <Eye v-if="!showPassword" :size="16" />
43
+ <EyeOff v-else :size="16" />
44
+ </button>
45
+ </div>
46
+ <slot name="suffix">
47
+ <component :is="suffixIcon" v-if="suffixIcon" />
48
+ </slot>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { ref } from 'vue'
55
+ import { Eye, EyeOff } from 'lucide-vue-next'
56
+ import { useNamespace } from '../../utils'
57
+
58
+ // 初始化命名空间
59
+ const ns = useNamespace('input')
60
+
61
+ // Props 定义 - 使用 export 导出
62
+ export interface InputProps {
63
+ type?: string
64
+ modelValue?: string | number
65
+ placeholder?: string
66
+ disabled?: boolean
67
+ readonly?: boolean
68
+ password?: boolean
69
+ maxlength?: number
70
+ minlength?: number
71
+ showWordLimit?: boolean
72
+ size?: 'large' | 'medium' | 'small'
73
+ prefixIcon?: string | object
74
+ suffixIcon?: string | object
75
+ class?: string
76
+ style?: string | object
77
+ }
78
+
79
+ withDefaults(defineProps<InputProps>(), {
80
+ type: 'text',
81
+ modelValue: '',
82
+ placeholder: '',
83
+ disabled: false,
84
+ readonly: false,
85
+ password: false,
86
+ maxlength: undefined,
87
+ minlength: undefined,
88
+ showWordLimit: false,
89
+ size: 'medium',
90
+ prefixIcon: undefined,
91
+ suffixIcon: undefined
92
+ })
93
+
94
+ const emit = defineEmits<{
95
+ (e: 'update:modelValue', value: string | number): void
96
+ (e: 'input', value: string | number): void
97
+ (e: 'focus', event: FocusEvent): void
98
+ (e: 'blur', event: FocusEvent): void
99
+ (e: 'enter', event: KeyboardEvent): void
100
+ (e: 'clear'): void
101
+ }>()
102
+
103
+ const inputRef = ref<HTMLInputElement>()
104
+ const showPassword = ref(false)
105
+
106
+ const handleInput = (event: Event) => {
107
+ const target = event.target as HTMLInputElement
108
+ const value = target.value
109
+ emit('update:modelValue', value)
110
+ emit('input', value)
111
+ }
112
+
113
+ const handleFocus = (event: FocusEvent) => {
114
+ emit('focus', event)
115
+ }
116
+
117
+ const handleBlur = (event: FocusEvent) => {
118
+ emit('blur', event)
119
+ }
120
+
121
+ const handleEnter = (event: KeyboardEvent) => {
122
+ emit('enter', event)
123
+ }
124
+
125
+ const togglePassword = () => {
126
+ showPassword.value = !showPassword.value
127
+ }
128
+ </script>
129
+
130
+ <style scoped lang="scss">
131
+ @import '../../styles/variables.scss';
132
+
133
+ .#{$namespace}-input {
134
+ position: relative;
135
+ display: inline-flex;
136
+ align-items: center;
137
+ width: 100%;
138
+ border: 1px solid $border-color;
139
+ border-radius: $border-radius-base;
140
+ transition: all 0.2s ease;
141
+ background-color: #fff;
142
+
143
+ &:hover {
144
+ border-color: $primary-color;
145
+ }
146
+
147
+ &:focus-within {
148
+ border-color: $primary-color;
149
+ box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
150
+ }
151
+
152
+ &--disabled {
153
+ background-color: $disabled-bg;
154
+ border-color: $border-color;
155
+ cursor: not-allowed;
156
+
157
+ .#{$namespace}-input__inner {
158
+ background-color: $disabled-bg;
159
+ cursor: not-allowed;
160
+ }
161
+ }
162
+
163
+ &--prefix {
164
+ .#{$namespace}-input__inner {
165
+ padding-left: 8px;
166
+ }
167
+ }
168
+
169
+ &--suffix {
170
+ .#{$namespace}-input__inner {
171
+ padding-right: 8px;
172
+ }
173
+ }
174
+
175
+ &--large {
176
+ height: 40px;
177
+
178
+ .#{$namespace}-input__inner {
179
+ height: 40px;
180
+ font-size: 14px;
181
+ padding: 0 12px;
182
+ }
183
+ }
184
+
185
+ &--medium {
186
+ height: 32px;
187
+
188
+ .#{$namespace}-input__inner {
189
+ height: 32px;
190
+ font-size: 14px;
191
+ padding: 0 12px;
192
+ }
193
+ }
194
+
195
+ &--small {
196
+ height: 24px;
197
+
198
+ .#{$namespace}-input__inner {
199
+ height: 24px;
200
+ font-size: 12px;
201
+ padding: 0 8px;
202
+ }
203
+ }
204
+
205
+ &__inner {
206
+ flex: 1;
207
+ border: none;
208
+ outline: none;
209
+ background: transparent;
210
+ font-size: 14px;
211
+ color: $text-color;
212
+ transition: all 0.2s ease;
213
+
214
+ &::placeholder {
215
+ color: $text-color-secondary;
216
+ }
217
+ }
218
+
219
+ &__prefix {
220
+ padding: 0 8px;
221
+ color: $text-color-secondary;
222
+ font-size: 14px;
223
+ }
224
+
225
+ &__suffix {
226
+ display: flex;
227
+ align-items: center;
228
+ padding: 0 8px;
229
+ color: $text-color-secondary;
230
+ font-size: 14px;
231
+ }
232
+
233
+ &__word-limit {
234
+ font-size: 12px;
235
+ color: $text-color-secondary;
236
+ margin-right: 8px;
237
+ }
238
+
239
+ &__password-toggle {
240
+ margin-right: 4px;
241
+ }
242
+
243
+ &__password-icon {
244
+ background: transparent;
245
+ border: none;
246
+ cursor: pointer;
247
+ padding: 0;
248
+ color: $text-color-secondary;
249
+ transition: color 0.2s ease;
250
+
251
+ &:hover {
252
+ color: $primary-color;
253
+ }
254
+ }
255
+ }
256
+ </style>