@fxshelf/button-kit 0.1.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.
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@fxshelf/button-kit",
3
+ "version": "0.1.0",
4
+ "description": "Animated button shelf for React and Vue — switch styles by variant string. Zero animation-lib deps. Copy-as-source friendly.",
5
+ "license": "MIT",
6
+ "author": "Lu Zhaotian",
7
+ "type": "module",
8
+ "sideEffects": [
9
+ "**/*.css"
10
+ ],
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "unpkg": "./dist/button-kit.iife.js",
15
+ "jsdelivr": "./dist/button-kit.iife.js",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ },
22
+ "./vue": {
23
+ "types": "./src/vue/index.ts",
24
+ "import": "./src/vue/index.ts",
25
+ "default": "./src/vue/index.ts"
26
+ },
27
+ "./styles.css": "./dist/index.css"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src/core",
32
+ "src/react",
33
+ "src/vue",
34
+ "src/vue-shim.d.ts",
35
+ "README.md",
36
+ "README.zh-CN.md",
37
+ "LICENSE",
38
+ "docs/使用说明.md",
39
+ "docs/动效说明-专业.md",
40
+ "docs/动效说明-大白话.md",
41
+ "docs/CDN示例.html"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup && node scripts/postbuild.mjs",
48
+ "lint": "oxlint src",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "pack:check": "npm run build && npm pack --dry-run",
51
+ "prepublishOnly": "npm run typecheck && npm run build"
52
+ },
53
+ "peerDependencies": {
54
+ "react": ">=18",
55
+ "react-dom": ">=18",
56
+ "vue": ">=3.3"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "vue": {
60
+ "optional": true
61
+ },
62
+ "react": {
63
+ "optional": true
64
+ },
65
+ "react-dom": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "keywords": [
70
+ "fxshelf",
71
+ "react",
72
+ "vue",
73
+ "button",
74
+ "button-kit",
75
+ "animation",
76
+ "effect",
77
+ "ui"
78
+ ],
79
+ "repository": {
80
+ "type": "git",
81
+ "url": "git+https://github.com/Luzhaotian/fxshelf.git",
82
+ "directory": "packages/button-kit"
83
+ },
84
+ "bugs": {
85
+ "url": "https://github.com/Luzhaotian/fxshelf/issues"
86
+ },
87
+ "homepage": "https://github.com/Luzhaotian/fxshelf/tree/main/packages/button-kit#readme"
88
+ }
@@ -0,0 +1,330 @@
1
+ .fx-btn {
2
+ --fx-btn-fg: #0f0f12;
3
+ --fx-btn-bg: #f4f4f5;
4
+ --fx-btn-accent: #3b82f6;
5
+ --fx-btn-radius: 10px;
6
+
7
+ position: relative;
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: 0.5rem;
12
+ padding: 0.7rem 1.25rem;
13
+ border: 1px solid transparent;
14
+ border-radius: var(--fx-btn-radius);
15
+ background: var(--fx-btn-bg);
16
+ color: var(--fx-btn-fg);
17
+ font: inherit;
18
+ font-size: 0.9375rem;
19
+ font-weight: 600;
20
+ line-height: 1.2;
21
+ letter-spacing: 0.01em;
22
+ text-decoration: none;
23
+ cursor: pointer;
24
+ user-select: none;
25
+ -webkit-tap-highlight-color: transparent;
26
+ isolation: isolate;
27
+ overflow: hidden;
28
+ transition:
29
+ transform 160ms ease,
30
+ box-shadow 200ms ease,
31
+ background-color 200ms ease,
32
+ border-color 200ms ease,
33
+ color 200ms ease;
34
+ }
35
+
36
+ .fx-btn:focus-visible {
37
+ outline: 2px solid var(--fx-btn-accent);
38
+ outline-offset: 3px;
39
+ }
40
+
41
+ .fx-btn:disabled {
42
+ opacity: 0.5;
43
+ cursor: not-allowed;
44
+ pointer-events: none;
45
+ }
46
+
47
+ .fx-btn:active:not(:disabled) {
48
+ transform: translateY(1px) scale(0.98);
49
+ }
50
+
51
+ /* —— solid —— */
52
+ .fx-btn--solid {
53
+ --fx-btn-fg: #fafafa;
54
+ --fx-btn-bg: #18181b;
55
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.08) inset;
56
+ }
57
+
58
+ .fx-btn--solid:hover:not(:disabled) {
59
+ background: #27272a;
60
+ }
61
+
62
+ /* —— outline —— */
63
+ .fx-btn--outline {
64
+ --fx-btn-fg: #18181b;
65
+ --fx-btn-bg: transparent;
66
+ border-color: #d4d4d8;
67
+ background: transparent;
68
+ }
69
+
70
+ .fx-btn--outline:hover:not(:disabled) {
71
+ border-color: #18181b;
72
+ background: #18181b;
73
+ color: #fafafa;
74
+ }
75
+
76
+ /* —— soft —— */
77
+ .fx-btn--soft {
78
+ --fx-btn-fg: #1d4ed8;
79
+ --fx-btn-bg: #dbeafe;
80
+ }
81
+
82
+ .fx-btn--soft:hover:not(:disabled) {
83
+ background: #bfdbfe;
84
+ transform: translateY(-1px);
85
+ }
86
+
87
+ .fx-btn--soft:active:not(:disabled) {
88
+ transform: translateY(0) scale(0.98);
89
+ }
90
+
91
+ /* —— glow —— */
92
+ .fx-btn--glow {
93
+ --fx-btn-fg: #f8fafc;
94
+ --fx-btn-bg: #2563eb;
95
+ box-shadow:
96
+ 0 0 0 0 rgba(37, 99, 235, 0.45),
97
+ 0 8px 24px rgba(37, 99, 235, 0.28);
98
+ animation: fx-btn-glow-pulse 2.4s ease-in-out infinite;
99
+ }
100
+
101
+ .fx-btn--glow:hover:not(:disabled) {
102
+ background: #1d4ed8;
103
+ box-shadow:
104
+ 0 0 0 6px rgba(37, 99, 235, 0.18),
105
+ 0 10px 28px rgba(37, 99, 235, 0.4);
106
+ animation: none;
107
+ }
108
+
109
+ @keyframes fx-btn-glow-pulse {
110
+ 0%,
111
+ 100% {
112
+ box-shadow:
113
+ 0 0 0 0 rgba(37, 99, 235, 0.4),
114
+ 0 8px 24px rgba(37, 99, 235, 0.28);
115
+ }
116
+ 50% {
117
+ box-shadow:
118
+ 0 0 0 8px rgba(37, 99, 235, 0),
119
+ 0 10px 28px rgba(37, 99, 235, 0.35);
120
+ }
121
+ }
122
+
123
+ /* —— neon —— */
124
+ .fx-btn--neon {
125
+ --fx-btn-fg: #67e8f9;
126
+ --fx-btn-bg: #0b1220;
127
+ border-color: #22d3ee;
128
+ box-shadow:
129
+ 0 0 0 1px rgba(34, 211, 238, 0.25),
130
+ 0 0 18px rgba(34, 211, 238, 0.35),
131
+ inset 0 0 12px rgba(34, 211, 238, 0.08);
132
+ text-shadow: 0 0 10px rgba(103, 232, 249, 0.55);
133
+ }
134
+
135
+ .fx-btn--neon:hover:not(:disabled) {
136
+ color: #ecfeff;
137
+ border-color: #67e8f9;
138
+ box-shadow:
139
+ 0 0 0 1px rgba(103, 232, 249, 0.45),
140
+ 0 0 28px rgba(34, 211, 238, 0.55),
141
+ inset 0 0 16px rgba(34, 211, 238, 0.14);
142
+ }
143
+
144
+ /* —— shimmer —— */
145
+ .fx-btn--shimmer {
146
+ --fx-btn-fg: #fafafa;
147
+ --fx-btn-bg: #111827;
148
+ }
149
+
150
+ .fx-btn--shimmer::before {
151
+ content: '';
152
+ position: absolute;
153
+ inset: 0;
154
+ z-index: -1;
155
+ background: linear-gradient(
156
+ 110deg,
157
+ transparent 30%,
158
+ rgba(255, 255, 255, 0.28) 48%,
159
+ transparent 66%
160
+ );
161
+ transform: translateX(-120%);
162
+ transition: transform 0.55s ease;
163
+ }
164
+
165
+ .fx-btn--shimmer:hover:not(:disabled)::before {
166
+ transform: translateX(120%);
167
+ }
168
+
169
+ .fx-btn--shimmer:hover:not(:disabled) {
170
+ background: #1f2937;
171
+ }
172
+
173
+ /* —— chroma(炫彩流光 + hover 高斯光晕) —— */
174
+ .fx-btn--chroma {
175
+ --fx-btn-fg: #fff;
176
+ --fx-btn-radius: 999px;
177
+ z-index: 0;
178
+ overflow: visible;
179
+ isolation: auto;
180
+ border: none;
181
+ background: linear-gradient(
182
+ to right,
183
+ #03a9f4,
184
+ #f441a5,
185
+ #ffeb3b,
186
+ #03a9f4
187
+ );
188
+ background-size: 400%;
189
+ letter-spacing: 0.08em;
190
+ animation: fx-btn-chroma-shift 8s ease infinite;
191
+ }
192
+
193
+ .fx-btn--chroma::before {
194
+ content: '';
195
+ position: absolute;
196
+ inset: -4px;
197
+ z-index: -1;
198
+ border-radius: inherit;
199
+ background: linear-gradient(
200
+ to right,
201
+ #03a9f4,
202
+ #f441a5,
203
+ #ffeb3b,
204
+ #03a9f4
205
+ );
206
+ background-size: 400%;
207
+ filter: blur(0);
208
+ opacity: 0;
209
+ pointer-events: none;
210
+ transition:
211
+ opacity 0.4s ease,
212
+ filter 0.4s ease;
213
+ animation: fx-btn-chroma-shift 8s ease infinite;
214
+ }
215
+
216
+ .fx-btn--chroma:hover:not(:disabled)::before {
217
+ opacity: 1;
218
+ filter: blur(8px);
219
+ }
220
+
221
+ .fx-btn--chroma:active:not(:disabled) {
222
+ transform: translateY(1px) scale(0.98);
223
+ }
224
+
225
+ @keyframes fx-btn-chroma-shift {
226
+ 0% {
227
+ background-position: 0%;
228
+ }
229
+
230
+ 100% {
231
+ background-position: 400%;
232
+ }
233
+ }
234
+
235
+ /* —— liquid(底部液体升起) —— */
236
+ .fx-btn--liquid {
237
+ --fx-btn-fg: #0891b2;
238
+ --fx-btn-bg: transparent;
239
+ z-index: 0;
240
+ border-color: #0891b2;
241
+ background: transparent;
242
+ overflow: hidden;
243
+ }
244
+
245
+ .fx-btn--liquid::before {
246
+ content: '';
247
+ position: absolute;
248
+ left: -30%;
249
+ bottom: 0;
250
+ z-index: -1;
251
+ width: 160%;
252
+ height: 220%;
253
+ border-radius: 42% 48% 0 0 / 28% 32% 0 0;
254
+ background: linear-gradient(180deg, #22d3ee, #0891b2);
255
+ transform: translateY(82%);
256
+ transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
257
+ }
258
+
259
+ .fx-btn--liquid:hover:not(:disabled) {
260
+ color: #fff;
261
+ }
262
+
263
+ .fx-btn--liquid:hover:not(:disabled)::before {
264
+ transform: translateY(12%);
265
+ animation: fx-btn-liquid-wave 2.4s ease-in-out infinite 0.55s;
266
+ }
267
+
268
+ @keyframes fx-btn-liquid-wave {
269
+ 0%,
270
+ 100% {
271
+ border-radius: 42% 48% 0 0 / 28% 32% 0 0;
272
+ }
273
+
274
+ 50% {
275
+ border-radius: 48% 42% 0 0 / 34% 26% 0 0;
276
+ }
277
+ }
278
+
279
+ /* —— wipe(左向右擦除填充) —— */
280
+ .fx-btn--wipe {
281
+ --fx-btn-fg: #f4f4f5;
282
+ --fx-btn-bg: transparent;
283
+ z-index: 0;
284
+ border-color: rgba(244, 244, 245, 0.7);
285
+ background: transparent;
286
+ overflow: hidden;
287
+ }
288
+
289
+ .fx-btn--wipe::before {
290
+ content: '';
291
+ position: absolute;
292
+ inset: 0;
293
+ z-index: -1;
294
+ background: #f4f4f5;
295
+ transform: translateX(-105%) skewX(-12deg);
296
+ transform-origin: left center;
297
+ transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1);
298
+ }
299
+
300
+ .fx-btn--wipe:hover:not(:disabled) {
301
+ color: #18181b;
302
+ border-color: #f4f4f5;
303
+ }
304
+
305
+ .fx-btn--wipe:hover:not(:disabled)::before {
306
+ transform: translateX(0) skewX(0deg);
307
+ }
308
+
309
+ /* —— glass(毛玻璃) —— */
310
+ .fx-btn--glass {
311
+ --fx-btn-fg: #f8fafc;
312
+ --fx-btn-bg: rgba(255, 255, 255, 0.1);
313
+ border-color: rgba(255, 255, 255, 0.28);
314
+ background: rgba(255, 255, 255, 0.1);
315
+ box-shadow:
316
+ inset 0 1px 0 rgba(255, 255, 255, 0.22),
317
+ 0 8px 24px rgba(0, 0, 0, 0.18);
318
+ backdrop-filter: blur(14px);
319
+ -webkit-backdrop-filter: blur(14px);
320
+ }
321
+
322
+ .fx-btn--glass:hover:not(:disabled) {
323
+ background: rgba(255, 255, 255, 0.18);
324
+ border-color: rgba(255, 255, 255, 0.4);
325
+ transform: translateY(-1px);
326
+ }
327
+
328
+ .fx-btn--glass:active:not(:disabled) {
329
+ transform: translateY(0) scale(0.98);
330
+ }
@@ -0,0 +1,31 @@
1
+ /** Built-in button looks. Pass any of these as `variant`. */
2
+ export const BUTTON_VARIANTS = [
3
+ 'solid',
4
+ 'outline',
5
+ 'soft',
6
+ 'glow',
7
+ 'neon',
8
+ 'shimmer',
9
+ 'chroma',
10
+ 'liquid',
11
+ 'wipe',
12
+ 'glass',
13
+ ] as const
14
+
15
+ export type ButtonVariant = (typeof BUTTON_VARIANTS)[number]
16
+
17
+ export const DEFAULT_VARIANT: ButtonVariant = 'solid'
18
+
19
+ export function resolveVariant(variant?: string): ButtonVariant {
20
+ if (
21
+ variant != null &&
22
+ (BUTTON_VARIANTS as readonly string[]).includes(variant)
23
+ ) {
24
+ return variant as ButtonVariant
25
+ }
26
+ return DEFAULT_VARIANT
27
+ }
28
+
29
+ export function variantClassName(variant?: string): string {
30
+ return `fx-btn fx-btn--${resolveVariant(variant)}`
31
+ }
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type ButtonHTMLAttributes,
5
+ type CSSProperties,
6
+ type ReactNode,
7
+ } from 'react'
8
+ import {
9
+ BUTTON_VARIANTS,
10
+ type ButtonVariant,
11
+ variantClassName,
12
+ } from '../core/variants'
13
+ import '../core/styles.css'
14
+
15
+ export type ButtonProps = Omit<
16
+ ButtonHTMLAttributes<HTMLButtonElement>,
17
+ 'className' | 'style' | 'children'
18
+ > & {
19
+ /** Switch look by name: solid | outline | soft | glow | neon | shimmer | chroma | liquid | wipe | glass. */
20
+ variant?: ButtonVariant | (string & {})
21
+ children?: ReactNode
22
+ className?: string
23
+ style?: CSSProperties
24
+ }
25
+
26
+ export function Button({
27
+ variant = 'solid',
28
+ children,
29
+ className,
30
+ style,
31
+ type = 'button',
32
+ ...rest
33
+ }: ButtonProps) {
34
+ const classes = [variantClassName(variant), className]
35
+ .filter(Boolean)
36
+ .join(' ')
37
+
38
+ return (
39
+ <button type={type} className={classes} style={style} {...rest}>
40
+ {children}
41
+ </button>
42
+ )
43
+ }
44
+
45
+ export { BUTTON_VARIANTS, type ButtonVariant }
46
+ export default Button
@@ -0,0 +1,6 @@
1
+ export {
2
+ Button,
3
+ BUTTON_VARIANTS,
4
+ type ButtonProps,
5
+ type ButtonVariant,
6
+ } from './Button'
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { computed, type CSSProperties } from 'vue'
3
+ import { type ButtonVariant, variantClassName } from '../core/variants'
4
+ import '../core/styles.css'
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ variant?: ButtonVariant | string
9
+ className?: string
10
+ style?: CSSProperties
11
+ type?: 'button' | 'submit' | 'reset'
12
+ disabled?: boolean
13
+ }>(),
14
+ {
15
+ variant: 'solid',
16
+ type: 'button',
17
+ },
18
+ )
19
+
20
+ defineEmits<{
21
+ click: [event: MouseEvent]
22
+ }>()
23
+
24
+ const rootClass = computed(() =>
25
+ [variantClassName(props.variant), props.className].filter(Boolean).join(' '),
26
+ )
27
+ </script>
28
+
29
+ <template>
30
+ <button
31
+ :type="type"
32
+ :class="rootClass"
33
+ :style="style"
34
+ :disabled="disabled"
35
+ @click="$emit('click', $event)"
36
+ >
37
+ <slot />
38
+ </button>
39
+ </template>
@@ -0,0 +1,5 @@
1
+ export { default as Button } from './Button.vue'
2
+ export {
3
+ BUTTON_VARIANTS,
4
+ type ButtonVariant,
5
+ } from '../core/variants'
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<object, object, unknown>
4
+ export default component
5
+ }