softleader-nuxt-core 1.2.0 → 2.2.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.
@@ -1,466 +1,466 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import IIcon from './IIcon.vue'
4
-
5
- /**
6
- * Component: IButton (按鈕元件)
7
- *
8
- * 介面層 (Interface Layer) 標準元件。
9
- * 負責將統一的 Props 轉換為底層 UI 框架 (Vuetify) 的屬性。
10
- * 內部保留了「與 UI 框架解耦」的能力,可隨時切換回原生或其他框架。
11
- *
12
- * 設計風格遵循 Corporate Trust (企業信賴) 風格指南:
13
- * - Primary: 漸層背景 (Indigo to Violet),圓角 (Rounded Full/Lg),懸浮時微幅上浮。
14
- * - Shadows: 使用帶有藍紫色的陰影,增加深度感。
15
- *
16
- * @example
17
- * <IButton variant="primary" size="large" loading prepend-icon="mdi-check">提交</IButton>
18
- */
19
-
20
- // ====================================================
21
- // 框架切換開關 (可改為 inject 或 config)
22
- // ====================================================
23
- const USE_FRAMEWORK = true
24
-
25
- // 1. 定義標準 Props
26
- interface Props {
27
- /**
28
- * 按鈕樣式變體
29
- * @default 'primary'
30
- */
31
- variant?:
32
- | 'primary'
33
- | 'secondary'
34
- | 'success'
35
- | 'danger'
36
- | 'warning'
37
- | 'info'
38
- | 'text'
39
- | 'outlined'
40
- | 'plain' // 新增 Vuetify 常用變體
41
- | 'tonal'
42
- | 'flat'
43
-
44
- /**
45
- * 尺寸
46
- * @default 'medium'
47
- */
48
- size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
49
-
50
- /**
51
- * 是否為區塊按鈕 (全寬)
52
- * @default false
53
- */
54
- block?: boolean
55
-
56
- /**
57
- * 是否處於載入狀態
58
- * @default false
59
- */
60
- loading?: boolean
61
-
62
- /**
63
- * 是否禁用
64
- * @default false
65
- */
66
- disabled?: boolean
67
-
68
- /**
69
- * 連結目標 URL (若存在則渲染為 <a>)
70
- */
71
- href?: string
72
-
73
- /**
74
- * 連結開啟目標 (_blank, _self, etc.)
75
- */
76
- target?: string
77
-
78
- /**
79
- * 自訂顏色 (Hex, RGB, 或 CSS 變數)
80
- * 若未指定,將根據 variant 自動決定
81
- */
82
- color?: string
83
-
84
- /**
85
- * 前置圖示名稱 (支援 mdi, fa, svg-softleader)
86
- */
87
- prependIcon?: string
88
-
89
- /**
90
- * 後置圖示名稱
91
- */
92
- appendIcon?: string
93
-
94
- /**
95
- * 圓角設定 (符合設計系統)
96
- * Primary 按鈕通常使用圓角較大的風格
97
- */
98
- rounded?: string | number | boolean
99
-
100
- /**
101
- * 陰影設定
102
- * @default undefined
103
- */
104
- elevation?: string | number
105
- }
106
-
107
- const props = withDefaults(defineProps<Props>(), {
108
- variant: 'primary',
109
- size: 'medium',
110
- block: false,
111
- loading: false,
112
- disabled: false,
113
- target: undefined,
114
- color: undefined,
115
- href: undefined,
116
- prependIcon: undefined,
117
- appendIcon: undefined,
118
- rounded: undefined,
119
- elevation: undefined
120
- })
121
-
122
- // 定義事件
123
- interface Emits {
124
- (e: 'click', event: MouseEvent): void
125
- }
126
- const emit = defineEmits<Emits>()
127
-
128
- // ====================================================
129
- // 2. 屬性對照表 (Adapter Pattern)
130
- // ====================================================
131
- const vuetifyBindings = computed(() => {
132
- const bindings: Record<string, any> = {}
133
-
134
- // [Size Mapping]
135
- // Vuetify 3 支援: x-small, small, default, large, x-large
136
- const sizeMap: Record<string, string> = {
137
- 'x-small': 'x-small',
138
- small: 'small',
139
- medium: 'default', // Mapping medium to default
140
- large: 'large',
141
- 'x-large': 'x-large'
142
- }
143
- bindings.size = sizeMap[props.size] || props.size
144
-
145
- // [Variant & Color Mapping]
146
- const colorMap: Record<string, string> = {
147
- danger: 'error',
148
- success: 'success',
149
- warning: 'warning',
150
- info: 'info',
151
- secondary: 'secondary',
152
- primary: 'primary'
153
- }
154
-
155
- // 處理特殊 Variant 映射
156
- if (['text', 'outlined', 'plain', 'tonal', 'flat'].includes(props.variant)) {
157
- bindings.variant = props.variant
158
- // 若無指定顏色,則使用 Primary 或預設
159
- bindings.color = props.color || (props.variant === 'outlined' ? 'primary' : undefined)
160
- } else {
161
- // 實心類按鈕 (Elevated)
162
- bindings.variant = 'elevated'
163
- // 映射語意顏色
164
- bindings.color = props.color || colorMap[props.variant] || props.variant
165
- }
166
-
167
- // 處理圓角
168
- // 只有在 props.rounded 真的是 undefined 時才使用預設值
169
- // 空字串 '' 代表使用者明確選擇 "Default",應該傳遞給 Vuetify
170
- if (props.rounded !== undefined) {
171
- // 如果是空字串,轉換為 undefined 讓 Vuetify 使用預設值
172
- bindings.rounded = props.rounded === '' ? undefined : props.rounded
173
- }
174
-
175
- // 處理 Elevation
176
- if (props.elevation !== undefined) {
177
- bindings.elevation = props.elevation
178
- }
179
-
180
- return bindings
181
- })
182
-
183
- // ====================================================
184
- // 3. 樣式類別計算 (Classes)
185
- // ====================================================
186
- const buttonClasses = computed(() => {
187
- const classes: any[] = []
188
-
189
- // Design System: Primary Button Gradients & Shadows
190
- if (props.variant === 'primary' && !props.disabled && !props.loading && !props.color) {
191
- classes.push(
192
- 'bg-gradient-to-r from-indigo-600 to-violet-600 text-white',
193
- 'hover:shadow-[0_10px_25px_-5px_rgba(79,70,229,0.4)]', // Enhanced Colored Shadow
194
- 'hover:-translate-y-0.5', // Lift effect
195
- 'transition-all duration-200 ease-out'
196
- )
197
- }
198
-
199
- // Custom Outline Style
200
- if (props.variant === 'outlined' && !props.color) {
201
- classes.push('border-slate-200 text-slate-700 hover:bg-slate-50 hover:border-slate-300')
202
- }
203
-
204
- return classes
205
- })
206
-
207
- // ====================================================
208
- // 4. 原生實作邏輯 (Native Fallback)
209
- // ====================================================
210
- const handleClick = (event: MouseEvent) => {
211
- if (!props.disabled && !props.loading) {
212
- emit('click', event)
213
- }
214
- }
215
-
216
- const isLink = computed(() => !!props.href)
217
- const componentTag = computed(() => (isLink.value ? 'a' : 'button'))
218
-
219
- const nativeStyle = computed(() => {
220
- if (!props.color) return {}
221
- const colorValue =
222
- props.color.startsWith('var(') || props.color.startsWith('#') || props.color.startsWith('rgb')
223
- ? props.color
224
- : `var(--color-${props.color}, ${props.color})`
225
-
226
- const isOutlinedOrText = ['text', 'outlined', 'plain'].includes(props.variant)
227
-
228
- return {
229
- color: isOutlinedOrText ? colorValue : '#ffffff',
230
- backgroundColor: isOutlinedOrText ? 'transparent' : colorValue,
231
- borderColor: props.variant === 'outlined' ? colorValue : 'transparent'
232
- }
233
- })
234
- </script>
235
-
236
- <template>
237
- <!--
238
- 實作 A: 底層框架 (Vuetify)
239
- 原則:屬性透傳 ($attrs) 讓 Vue 自動處理剩下的 80% 屬性
240
- -->
241
- <v-btn
242
- v-if="USE_FRAMEWORK"
243
- v-bind="{ ...vuetifyBindings, ...$attrs }"
244
- :block="block"
245
- :loading="loading"
246
- :disabled="disabled"
247
- :href="href"
248
- :target="target"
249
- :class="buttonClasses"
250
- @click="handleClick"
251
- >
252
- <!-- Prepend Slot -->
253
- <template
254
- v-if="prependIcon"
255
- #prepend
256
- >
257
- <slot name="prepend">
258
- <IIcon :icon="prependIcon" />
259
- </slot>
260
- </template>
261
- <template
262
- v-else-if="$slots.prepend"
263
- #prepend
264
- >
265
- <slot name="prepend" />
266
- </template>
267
-
268
- <!-- Default Slot -->
269
- <slot />
270
-
271
- <!-- Append Slot -->
272
- <template
273
- v-if="appendIcon"
274
- #append
275
- >
276
- <slot name="append">
277
- <IIcon :icon="appendIcon" />
278
- </slot>
279
- </template>
280
- <template
281
- v-else-if="$slots.append"
282
- #append
283
- >
284
- <slot name="append" />
285
- </template>
286
-
287
- <!-- Loader Slot (Optional customization) -->
288
- <template
289
- v-if="$slots.loader"
290
- #loader
291
- >
292
- <slot name="loader" />
293
- </template>
294
- </v-btn>
295
-
296
- <!--
297
- 實作 B: 原生 HTML/CSS
298
- 完全不依賴任何第三方 UI 庫,證明介面層解耦能力
299
- -->
300
- <component
301
- :is="componentTag"
302
- v-else
303
- :type="isLink ? undefined : 'button'"
304
- :href="isLink ? href : undefined"
305
- :target="isLink ? target : undefined"
306
- :disabled="isLink ? undefined : disabled || loading"
307
- :class="[
308
- 'i-button',
309
- `i-button--${variant}`,
310
- `i-button--${size}`,
311
- {
312
- 'i-button--block': block,
313
- 'i-button--loading': loading,
314
- 'i-button--disabled': disabled
315
- },
316
- ...buttonClasses // Apply same utility classes where possible
317
- ]"
318
- :style="nativeStyle"
319
- @click="handleClick"
320
- >
321
- <!-- Loading -->
322
- <span
323
- v-if="loading"
324
- class="i-button__loading"
325
- >
326
- <svg
327
- class="animate-spin h-5 w-5 text-current"
328
- xmlns="http://www.w3.org/2000/svg"
329
- fill="none"
330
- viewBox="0 0 24 24"
331
- >
332
- <circle
333
- class="opacity-25"
334
- cx="12"
335
- cy="12"
336
- r="10"
337
- stroke="currentColor"
338
- stroke-width="4"
339
- ></circle>
340
- <path
341
- class="opacity-75"
342
- fill="currentColor"
343
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
344
- ></path>
345
- </svg>
346
- </span>
347
-
348
- <!-- Prepend Icon -->
349
- <span
350
- v-if="!loading && (prependIcon || $slots.prepend)"
351
- class="i-button__prepend mr-2"
352
- >
353
- <slot name="prepend">
354
- <IIcon :icon="prependIcon!" />
355
- </slot>
356
- </span>
357
-
358
- <!-- Content -->
359
- <slot />
360
-
361
- <!-- Append Icon -->
362
- <span
363
- v-if="!loading && (appendIcon || $slots.append)"
364
- class="i-button__append ml-2"
365
- >
366
- <slot name="append">
367
- <IIcon :icon="appendIcon!" />
368
- </slot>
369
- </span>
370
- </component>
371
- </template>
372
-
373
- <style scoped>
374
- /*
375
- Native Implementation Styles
376
- Only loaded when USE_FRAMEWORK is false
377
- */
378
- .i-button {
379
- display: inline-flex;
380
- align-items: center;
381
- justify-content: center;
382
- padding: 0.5rem 1rem;
383
- border: 1px solid transparent; /* Ensure border exists for layout */
384
- border-radius: 0.5rem; /* rounded-lg */
385
- font-size: 1rem; /* text-base */
386
- font-weight: 500; /* font-medium */
387
- cursor: pointer;
388
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
389
- text-decoration: none;
390
- line-height: 1.5;
391
- outline: none;
392
- user-select: none;
393
- }
394
-
395
- .i-button:focus-visible {
396
- box-shadow:
397
- 0 0 0 2px white,
398
- 0 0 0 4px var(--color-primary, #4f46e5);
399
- }
400
-
401
- /* Sizes */
402
- .i-button--x-small {
403
- padding: 0.125rem 0.5rem;
404
- font-size: 0.75rem;
405
- }
406
- .i-button--small {
407
- padding: 0.25rem 0.75rem;
408
- font-size: 0.875rem;
409
- }
410
- .i-button--medium {
411
- padding: 0.5rem 1rem;
412
- font-size: 0.875rem;
413
- } /* Default is often text-sm in systems */
414
- .i-button--large {
415
- padding: 0.75rem 1.5rem;
416
- font-size: 1rem;
417
- }
418
- .i-button--x-large {
419
- padding: 1rem 2rem;
420
- font-size: 1.125rem;
421
- }
422
-
423
- /* Block */
424
- .i-button--block {
425
- display: flex;
426
- width: 100%;
427
- }
428
-
429
- /* Disabled */
430
- .i-button--disabled {
431
- opacity: 0.5;
432
- cursor: not-allowed;
433
- pointer-events: none;
434
- box-shadow: none !important;
435
- transform: none !important;
436
- }
437
-
438
- /* Native Variants (Fallbacks) */
439
- .i-button--primary {
440
- /* Handled by utility classes mostly, but fallback: */
441
- background-color: #4f46e5;
442
- color: white;
443
- }
444
- .i-button--secondary {
445
- background-color: #64748b;
446
- color: white;
447
- }
448
- .i-button--danger {
449
- background-color: #ef4444;
450
- color: white;
451
- }
452
- .i-button--text {
453
- background-color: transparent;
454
- color: inherit;
455
- }
456
- .i-button--outlined {
457
- background-color: transparent;
458
- border: 1px solid currentColor;
459
- }
460
-
461
- .i-button__loading {
462
- margin-right: 0.5rem;
463
- display: flex;
464
- align-items: center;
465
- }
466
- </style>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import IIcon from './IIcon.vue'
4
+
5
+ /**
6
+ * Component: IButton (按鈕元件)
7
+ *
8
+ * 介面層 (Interface Layer) 標準元件。
9
+ * 負責將統一的 Props 轉換為底層 UI 框架 (Vuetify) 的屬性。
10
+ * 內部保留了「與 UI 框架解耦」的能力,可隨時切換回原生或其他框架。
11
+ *
12
+ * 設計風格遵循 Corporate Trust (企業信賴) 風格指南:
13
+ * - Primary: 漸層背景 (Indigo to Violet),圓角 (Rounded Full/Lg),懸浮時微幅上浮。
14
+ * - Shadows: 使用帶有藍紫色的陰影,增加深度感。
15
+ *
16
+ * @example
17
+ * <IButton variant="primary" size="large" loading prepend-icon="mdi-check">提交</IButton>
18
+ */
19
+
20
+ // ====================================================
21
+ // 框架切換開關 (可改為 inject 或 config)
22
+ // ====================================================
23
+ const USE_FRAMEWORK = true
24
+
25
+ // 1. 定義標準 Props
26
+ interface Props {
27
+ /**
28
+ * 按鈕樣式變體
29
+ * @default 'primary'
30
+ */
31
+ variant?:
32
+ | 'primary'
33
+ | 'secondary'
34
+ | 'success'
35
+ | 'danger'
36
+ | 'warning'
37
+ | 'info'
38
+ | 'text'
39
+ | 'outlined'
40
+ | 'plain' // 新增 Vuetify 常用變體
41
+ | 'tonal'
42
+ | 'flat'
43
+
44
+ /**
45
+ * 尺寸
46
+ * @default 'medium'
47
+ */
48
+ size?: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
49
+
50
+ /**
51
+ * 是否為區塊按鈕 (全寬)
52
+ * @default false
53
+ */
54
+ block?: boolean
55
+
56
+ /**
57
+ * 是否處於載入狀態
58
+ * @default false
59
+ */
60
+ loading?: boolean
61
+
62
+ /**
63
+ * 是否禁用
64
+ * @default false
65
+ */
66
+ disabled?: boolean
67
+
68
+ /**
69
+ * 連結目標 URL (若存在則渲染為 <a>)
70
+ */
71
+ href?: string
72
+
73
+ /**
74
+ * 連結開啟目標 (_blank, _self, etc.)
75
+ */
76
+ target?: string
77
+
78
+ /**
79
+ * 自訂顏色 (Hex, RGB, 或 CSS 變數)
80
+ * 若未指定,將根據 variant 自動決定
81
+ */
82
+ color?: string
83
+
84
+ /**
85
+ * 前置圖示名稱 (支援 mdi, fa, svg-softleader)
86
+ */
87
+ prependIcon?: string
88
+
89
+ /**
90
+ * 後置圖示名稱
91
+ */
92
+ appendIcon?: string
93
+
94
+ /**
95
+ * 圓角設定 (符合設計系統)
96
+ * Primary 按鈕通常使用圓角較大的風格
97
+ */
98
+ rounded?: string | number | boolean
99
+
100
+ /**
101
+ * 陰影設定
102
+ * @default undefined
103
+ */
104
+ elevation?: string | number
105
+ }
106
+
107
+ const props = withDefaults(defineProps<Props>(), {
108
+ variant: 'primary',
109
+ size: 'medium',
110
+ block: false,
111
+ loading: false,
112
+ disabled: false,
113
+ target: undefined,
114
+ color: undefined,
115
+ href: undefined,
116
+ prependIcon: undefined,
117
+ appendIcon: undefined,
118
+ rounded: undefined,
119
+ elevation: undefined
120
+ })
121
+
122
+ // 定義事件
123
+ interface Emits {
124
+ (e: 'click', event: MouseEvent): void
125
+ }
126
+ const emit = defineEmits<Emits>()
127
+
128
+ // ====================================================
129
+ // 2. 屬性對照表 (Adapter Pattern)
130
+ // ====================================================
131
+ const vuetifyBindings = computed(() => {
132
+ const bindings: Record<string, any> = {}
133
+
134
+ // [Size Mapping]
135
+ // Vuetify 3 支援: x-small, small, default, large, x-large
136
+ const sizeMap: Record<string, string> = {
137
+ 'x-small': 'x-small',
138
+ small: 'small',
139
+ medium: 'default', // Mapping medium to default
140
+ large: 'large',
141
+ 'x-large': 'x-large'
142
+ }
143
+ bindings.size = sizeMap[props.size] || props.size
144
+
145
+ // [Variant & Color Mapping]
146
+ const colorMap: Record<string, string> = {
147
+ danger: 'error',
148
+ success: 'success',
149
+ warning: 'warning',
150
+ info: 'info',
151
+ secondary: 'secondary',
152
+ primary: 'primary'
153
+ }
154
+
155
+ // 處理特殊 Variant 映射
156
+ if (['text', 'outlined', 'plain', 'tonal', 'flat'].includes(props.variant)) {
157
+ bindings.variant = props.variant
158
+ // 若無指定顏色,則使用 Primary 或預設
159
+ bindings.color = props.color || (props.variant === 'outlined' ? 'primary' : undefined)
160
+ } else {
161
+ // 實心類按鈕 (Elevated)
162
+ bindings.variant = 'elevated'
163
+ // 映射語意顏色
164
+ bindings.color = props.color || colorMap[props.variant] || props.variant
165
+ }
166
+
167
+ // 處理圓角
168
+ // 只有在 props.rounded 真的是 undefined 時才使用預設值
169
+ // 空字串 '' 代表使用者明確選擇 "Default",應該傳遞給 Vuetify
170
+ if (props.rounded !== undefined) {
171
+ // 如果是空字串,轉換為 undefined 讓 Vuetify 使用預設值
172
+ bindings.rounded = props.rounded === '' ? undefined : props.rounded
173
+ }
174
+
175
+ // 處理 Elevation
176
+ if (props.elevation !== undefined) {
177
+ bindings.elevation = props.elevation
178
+ }
179
+
180
+ return bindings
181
+ })
182
+
183
+ // ====================================================
184
+ // 3. 樣式類別計算 (Classes)
185
+ // ====================================================
186
+ const buttonClasses = computed(() => {
187
+ const classes: any[] = []
188
+
189
+ // Design System: Primary Button Gradients & Shadows
190
+ if (props.variant === 'primary' && !props.disabled && !props.loading && !props.color) {
191
+ classes.push(
192
+ 'bg-[var(--color-primary,#4f46e5)] text-white',
193
+ 'hover:shadow-[0_10px_25px_-5px_var(--color-primary-alpha,rgba(79,70,229,0.4))]', // Enhanced Colored Shadow
194
+ 'hover:-translate-y-0.5', // Lift effect
195
+ 'transition-all duration-200 ease-out'
196
+ )
197
+ }
198
+
199
+ // Custom Outline Style
200
+ if (props.variant === 'outlined' && !props.color) {
201
+ classes.push('border-slate-200 text-slate-700 hover:bg-slate-50 hover:border-slate-300')
202
+ }
203
+
204
+ return classes
205
+ })
206
+
207
+ // ====================================================
208
+ // 4. 原生實作邏輯 (Native Fallback)
209
+ // ====================================================
210
+ const handleClick = (event: MouseEvent) => {
211
+ if (!props.disabled && !props.loading) {
212
+ emit('click', event)
213
+ }
214
+ }
215
+
216
+ const isLink = computed(() => !!props.href)
217
+ const componentTag = computed(() => (isLink.value ? 'a' : 'button'))
218
+
219
+ const nativeStyle = computed(() => {
220
+ if (!props.color) return {}
221
+ const colorValue =
222
+ props.color.startsWith('var(') || props.color.startsWith('#') || props.color.startsWith('rgb')
223
+ ? props.color
224
+ : `var(--color-${props.color}, ${props.color})`
225
+
226
+ const isOutlinedOrText = ['text', 'outlined', 'plain'].includes(props.variant)
227
+
228
+ return {
229
+ color: isOutlinedOrText ? colorValue : '#ffffff',
230
+ backgroundColor: isOutlinedOrText ? 'transparent' : colorValue,
231
+ borderColor: props.variant === 'outlined' ? colorValue : 'transparent'
232
+ }
233
+ })
234
+ </script>
235
+
236
+ <template>
237
+ <!--
238
+ 實作 A: 底層框架 (Vuetify)
239
+ 原則:屬性透傳 ($attrs) 讓 Vue 自動處理剩下的 80% 屬性
240
+ -->
241
+ <v-btn
242
+ v-if="USE_FRAMEWORK"
243
+ v-bind="{ ...vuetifyBindings, ...$attrs }"
244
+ :block="block"
245
+ :loading="loading"
246
+ :disabled="disabled"
247
+ :href="href"
248
+ :target="target"
249
+ :class="buttonClasses"
250
+ @click="handleClick"
251
+ >
252
+ <!-- Prepend Slot -->
253
+ <template
254
+ v-if="prependIcon"
255
+ #prepend
256
+ >
257
+ <slot name="prepend">
258
+ <IIcon :icon="prependIcon" />
259
+ </slot>
260
+ </template>
261
+ <template
262
+ v-else-if="$slots.prepend"
263
+ #prepend
264
+ >
265
+ <slot name="prepend" />
266
+ </template>
267
+
268
+ <!-- Default Slot -->
269
+ <slot />
270
+
271
+ <!-- Append Slot -->
272
+ <template
273
+ v-if="appendIcon"
274
+ #append
275
+ >
276
+ <slot name="append">
277
+ <IIcon :icon="appendIcon" />
278
+ </slot>
279
+ </template>
280
+ <template
281
+ v-else-if="$slots.append"
282
+ #append
283
+ >
284
+ <slot name="append" />
285
+ </template>
286
+
287
+ <!-- Loader Slot (Optional customization) -->
288
+ <template
289
+ v-if="$slots.loader"
290
+ #loader
291
+ >
292
+ <slot name="loader" />
293
+ </template>
294
+ </v-btn>
295
+
296
+ <!--
297
+ 實作 B: 原生 HTML/CSS
298
+ 完全不依賴任何第三方 UI 庫,證明介面層解耦能力
299
+ -->
300
+ <component
301
+ :is="componentTag"
302
+ v-else
303
+ :type="isLink ? undefined : 'button'"
304
+ :href="isLink ? href : undefined"
305
+ :target="isLink ? target : undefined"
306
+ :disabled="isLink ? undefined : disabled || loading"
307
+ :class="[
308
+ 'i-button',
309
+ `i-button--${variant}`,
310
+ `i-button--${size}`,
311
+ {
312
+ 'i-button--block': block,
313
+ 'i-button--loading': loading,
314
+ 'i-button--disabled': disabled
315
+ },
316
+ ...buttonClasses // Apply same utility classes where possible
317
+ ]"
318
+ :style="nativeStyle"
319
+ @click="handleClick"
320
+ >
321
+ <!-- Loading -->
322
+ <span
323
+ v-if="loading"
324
+ class="i-button__loading"
325
+ >
326
+ <svg
327
+ class="animate-spin h-5 w-5 text-current"
328
+ xmlns="http://www.w3.org/2000/svg"
329
+ fill="none"
330
+ viewBox="0 0 24 24"
331
+ >
332
+ <circle
333
+ class="opacity-25"
334
+ cx="12"
335
+ cy="12"
336
+ r="10"
337
+ stroke="currentColor"
338
+ stroke-width="4"
339
+ ></circle>
340
+ <path
341
+ class="opacity-75"
342
+ fill="currentColor"
343
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
344
+ ></path>
345
+ </svg>
346
+ </span>
347
+
348
+ <!-- Prepend Icon -->
349
+ <span
350
+ v-if="!loading && (prependIcon || $slots.prepend)"
351
+ class="i-button__prepend mr-2"
352
+ >
353
+ <slot name="prepend">
354
+ <IIcon :icon="prependIcon!" />
355
+ </slot>
356
+ </span>
357
+
358
+ <!-- Content -->
359
+ <slot />
360
+
361
+ <!-- Append Icon -->
362
+ <span
363
+ v-if="!loading && (appendIcon || $slots.append)"
364
+ class="i-button__append ml-2"
365
+ >
366
+ <slot name="append">
367
+ <IIcon :icon="appendIcon!" />
368
+ </slot>
369
+ </span>
370
+ </component>
371
+ </template>
372
+
373
+ <style scoped>
374
+ /*
375
+ Native Implementation Styles
376
+ Only loaded when USE_FRAMEWORK is false
377
+ */
378
+ .i-button {
379
+ display: inline-flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ padding: 0.5rem 1rem;
383
+ border: 1px solid transparent; /* Ensure border exists for layout */
384
+ border-radius: 0.5rem; /* rounded-lg */
385
+ font-size: 1rem; /* text-base */
386
+ font-weight: 500; /* font-medium */
387
+ cursor: pointer;
388
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
389
+ text-decoration: none;
390
+ line-height: 1.5;
391
+ outline: none;
392
+ user-select: none;
393
+ }
394
+
395
+ .i-button:focus-visible {
396
+ box-shadow:
397
+ 0 0 0 2px white,
398
+ 0 0 0 4px var(--color-primary, #4f46e5);
399
+ }
400
+
401
+ /* Sizes */
402
+ .i-button--x-small {
403
+ padding: 0.125rem 0.5rem;
404
+ font-size: 0.75rem;
405
+ }
406
+ .i-button--small {
407
+ padding: 0.25rem 0.75rem;
408
+ font-size: 0.875rem;
409
+ }
410
+ .i-button--medium {
411
+ padding: 0.5rem 1rem;
412
+ font-size: 0.875rem;
413
+ } /* Default is often text-sm in systems */
414
+ .i-button--large {
415
+ padding: 0.75rem 1.5rem;
416
+ font-size: 1rem;
417
+ }
418
+ .i-button--x-large {
419
+ padding: 1rem 2rem;
420
+ font-size: 1.125rem;
421
+ }
422
+
423
+ /* Block */
424
+ .i-button--block {
425
+ display: flex;
426
+ width: 100%;
427
+ }
428
+
429
+ /* Disabled */
430
+ .i-button--disabled {
431
+ opacity: 0.5;
432
+ cursor: not-allowed;
433
+ pointer-events: none;
434
+ box-shadow: none !important;
435
+ transform: none !important;
436
+ }
437
+
438
+ /* Native Variants (Fallbacks) */
439
+ .i-button--primary {
440
+ /* Handled by utility classes mostly, but fallback: */
441
+ background-color: #4f46e5;
442
+ color: white;
443
+ }
444
+ .i-button--secondary {
445
+ background-color: #64748b;
446
+ color: white;
447
+ }
448
+ .i-button--danger {
449
+ background-color: #ef4444;
450
+ color: white;
451
+ }
452
+ .i-button--text {
453
+ background-color: transparent;
454
+ color: inherit;
455
+ }
456
+ .i-button--outlined {
457
+ background-color: transparent;
458
+ border: 1px solid currentColor;
459
+ }
460
+
461
+ .i-button__loading {
462
+ margin-right: 0.5rem;
463
+ display: flex;
464
+ align-items: center;
465
+ }
466
+ </style>