@lumen-design/input 0.0.2

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.
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import Icon from '@lumen-design/icon'
3
+ import { Eye, EyeOff, XCircle } from 'lucide'
4
+ import type { InputProps } from './types'
5
+
6
+ let {
7
+ value = $bindable(''),
8
+ autofilled = $bindable(false),
9
+ type = 'text',
10
+ size = 'medium',
11
+ placeholder = '',
12
+ disabled = false,
13
+ clearable = false,
14
+ readonly = false,
15
+ showPassword = false,
16
+ maxlength,
17
+ name,
18
+ id,
19
+ autocomplete,
20
+ prefix,
21
+ suffix,
22
+ oninput,
23
+ onchange,
24
+ onfocus,
25
+ onblur,
26
+ onkeydown,
27
+ onkeyup,
28
+ onclear,
29
+ class: cls = '',
30
+ ...attrs
31
+ }: InputProps = $props()
32
+
33
+ let inputRef: HTMLInputElement | null = $state(null)
34
+ let focused = $state(false)
35
+ let showClear = $derived(clearable && value && !disabled && !readonly && focused)
36
+
37
+ let passwordVisible = $state(false)
38
+ const showPwdToggle = $derived(showPassword && type === 'password' && !disabled)
39
+ const resolvedType = $derived(showPwdToggle ? (passwordVisible ? 'text' : 'password') : type)
40
+
41
+ const suffixCount = $derived((suffix ? 1 : 0) + (showClear ? 1 : 0) + (showPwdToggle ? 1 : 0))
42
+
43
+ const classes = $derived(
44
+ `lm-input lm-input--${size}${disabled ? ' is-disabled' : ''}${focused ? ' is-focused' : ''}${prefix ? ' has-prefix' : ''}${suffixCount >= 1 ? ' has-suffix' : ''}${suffixCount >= 2 ? ' has-suffix-2' : ''}${suffixCount >= 3 ? ' has-suffix-3' : ''}${cls ? ` ${cls}` : ''}`
45
+ )
46
+
47
+ const handleInput = (e: Event & { currentTarget: HTMLInputElement }): void => {
48
+ value = e.currentTarget.value
49
+ oninput?.(e)
50
+ }
51
+
52
+ const handleChange = (e: Event & { currentTarget: HTMLInputElement }): void => {
53
+ value = e.currentTarget.value; onchange?.(e)
54
+ }
55
+
56
+ const handleFocus = (e: FocusEvent & { currentTarget: HTMLInputElement }): void => {
57
+ updateAutofillFromNode(e.currentTarget)
58
+ focused = true
59
+ onfocus?.(e)
60
+ }
61
+
62
+ const handleAnimationStart = (e: AnimationEvent & { currentTarget: HTMLInputElement }): void => {
63
+ if (e.animationName === 'lm-input-autofill') {
64
+ updateAutofillFromNode(e.currentTarget)
65
+ }
66
+ }
67
+
68
+ const handleBlur = (e: FocusEvent & { currentTarget: HTMLInputElement }): void => {
69
+ focused = false
70
+ onblur?.(e)
71
+ }
72
+
73
+ const handleClear = (): void => {
74
+ value = ''
75
+ onclear?.()
76
+ inputRef?.focus()
77
+ }
78
+
79
+ const handleTogglePassword = (): void => {
80
+ passwordVisible = !passwordVisible
81
+ inputRef?.focus()
82
+ }
83
+
84
+ const updateAutofillFromNode = (node: HTMLInputElement): void => {
85
+ try {
86
+ autofilled = node.matches(':-webkit-autofill')
87
+ if (autofilled) {
88
+ const domValue = node.value ?? ''
89
+ if (domValue && domValue !== value) value = domValue
90
+ }
91
+ } catch {
92
+ autofilled = false
93
+ }
94
+ }
95
+
96
+ const autofillProbe = (node: HTMLInputElement) => {
97
+ updateAutofillFromNode(node)
98
+ const raf = requestAnimationFrame(() => updateAutofillFromNode(node))
99
+ const timer = setTimeout(() => updateAutofillFromNode(node), 200)
100
+
101
+ return {
102
+ destroy() {
103
+ cancelAnimationFrame(raf)
104
+ clearTimeout(timer)
105
+ },
106
+ }
107
+ }
108
+ </script>
109
+
110
+ <div class={classes}>
111
+ {#if prefix}
112
+ <span class="lm-input__prefix">
113
+ {@render prefix()}
114
+ </span>
115
+ {/if}
116
+
117
+ <input
118
+ bind:this={inputRef}
119
+ use:autofillProbe
120
+ {...attrs}
121
+ id={id}
122
+ name={name}
123
+ autocomplete={autocomplete as any}
124
+ type={resolvedType}
125
+ class="lm-input__inner"
126
+ {placeholder}
127
+ {disabled}
128
+ {readonly}
129
+ {maxlength}
130
+ bind:value={value}
131
+ oninput={handleInput}
132
+ onchange={handleChange}
133
+ onfocus={handleFocus}
134
+ onanimationstart={handleAnimationStart}
135
+ onblur={handleBlur}
136
+ {onkeydown}
137
+ {onkeyup}
138
+ />
139
+
140
+ {#if suffix || showClear || showPwdToggle}
141
+ <span class="lm-input__suffix">
142
+ {#if showPwdToggle}
143
+ <button
144
+ type="button"
145
+ class="lm-input__password"
146
+ onmousedown={(e: MouseEvent) => {
147
+ e.preventDefault()
148
+ handleTogglePassword()
149
+ }}
150
+ tabindex="-1"
151
+ aria-label={passwordVisible ? '隐藏密码' : '显示密码'}
152
+ >
153
+ <Icon icon={passwordVisible ? EyeOff : Eye} size={16} />
154
+ </button>
155
+ {/if}
156
+
157
+ {#if showClear}
158
+ <button
159
+ type="button"
160
+ class="lm-input__clear"
161
+ onmousedown={(e: MouseEvent) => {
162
+ e.preventDefault()
163
+ handleClear()
164
+ }}
165
+ tabindex="-1"
166
+ aria-label="Clear input"
167
+ >
168
+ <Icon icon={XCircle} size={16} />
169
+ </button>
170
+ {/if}
171
+
172
+ {#if suffix}
173
+ {@render suffix()}
174
+ {/if}
175
+ </span>
176
+ {/if}
177
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { InputProps } from './types';
2
+ declare const Input: import("svelte").Component<InputProps, {}, "value" | "autofilled">;
3
+ type Input = ReturnType<typeof Input>;
4
+ export default Input;
@@ -0,0 +1,4 @@
1
+ import Input from './Input.svelte';
2
+ export { Input };
3
+ export type { InputProps, InputType, InputSize } from './types';
4
+ export default Input;
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Input from './Input.svelte';
2
+ export { Input };
3
+ export default Input;
@@ -0,0 +1,72 @@
1
+ import type { Snippet } from 'svelte';
2
+ /** Input 类型 */
3
+ export type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url';
4
+ /** Input 尺寸 */
5
+ export type InputSize = 'small' | 'medium' | 'large';
6
+ /** Input 属性 */
7
+ export interface InputProps {
8
+ /** 绑定值 */
9
+ value?: string;
10
+ /** 输入框类型 */
11
+ type?: InputType;
12
+ /** 尺寸 */
13
+ size?: InputSize;
14
+ /** 占位符 */
15
+ placeholder?: string;
16
+ /** 原生 name(对浏览器自动填充很关键) */
17
+ name?: string;
18
+ /** 原生 id */
19
+ id?: string;
20
+ /** 原生 autocomplete */
21
+ autocomplete?: string;
22
+ /** 原生 autocapitalize */
23
+ autocapitalize?: 'off' | 'on' | 'characters' | 'none' | 'sentences' | 'words' | null;
24
+ /** 原生 spellcheck */
25
+ spellcheck?: boolean;
26
+ /** 无可视 label 时的可访问性标签(透传到 input) */
27
+ 'aria-label'?: string;
28
+ /** 是否禁用 */
29
+ disabled?: boolean;
30
+ /** 是否可清空 */
31
+ clearable?: boolean;
32
+ /** 是否只读 */
33
+ readonly?: boolean;
34
+ /** 密码可见切换(仅 type=password 时生效) */
35
+ showPassword?: boolean;
36
+ /** 最大长度 */
37
+ maxlength?: number;
38
+ /** 前缀内容 */
39
+ prefix?: Snippet;
40
+ /** 后缀内容 */
41
+ suffix?: Snippet;
42
+ /** 输入事件 */
43
+ oninput?: (e: Event & {
44
+ currentTarget: HTMLInputElement;
45
+ }) => void;
46
+ /** 变化事件 */
47
+ onchange?: (e: Event & {
48
+ currentTarget: HTMLInputElement;
49
+ }) => void;
50
+ /** 聚焦事件 */
51
+ onfocus?: (e: FocusEvent & {
52
+ currentTarget: HTMLInputElement;
53
+ }) => void;
54
+ /** 失焦事件 */
55
+ onblur?: (e: FocusEvent & {
56
+ currentTarget: HTMLInputElement;
57
+ }) => void;
58
+ /** 键盘按下事件 */
59
+ onkeydown?: (e: KeyboardEvent & {
60
+ currentTarget: HTMLInputElement;
61
+ }) => void;
62
+ /** 键盘抬起事件 */
63
+ onkeyup?: (e: KeyboardEvent & {
64
+ currentTarget: HTMLInputElement;
65
+ }) => void;
66
+ /** 清空事件 */
67
+ onclear?: () => void;
68
+ /** 自定义类名 */
69
+ class?: string;
70
+ /** 是否被浏览器自动填充(只读,由组件内部检测 :-webkit-autofill 设置) */
71
+ autofilled?: boolean;
72
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@lumen-design/input",
3
+ "version": "0.0.2",
4
+ "description": "Input component for Lumen UI",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./dist/index.js",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "svelte-package -i src -o dist --types",
21
+ "postbuild": "cp -f src/*.d.ts dist/ 2>/dev/null || true",
22
+ "build:watch": "svelte-package -i src -o dist --types -w"
23
+ },
24
+ "devDependencies": {
25
+ "@sveltejs/package": "^2.5.7",
26
+ "svelte": "5.48.2"
27
+ },
28
+ "peerDependencies": {
29
+ "svelte": "^5.0.0"
30
+ },
31
+ "dependencies": {
32
+ "lucide": "^0.563.0"
33
+ }
34
+ }