1. 新增 `ImpersonateController` 處理帳號切換與退出邏輯,並提供公司與使用者的動態下拉選單 API (包含 username 支援)。 2. 於 `admin.blade.php` 的 Header 個人選單中加入「切換帳號」入口,並支援退出切換狀態的顯示。 3. 實作 `modal.blade.php` Alpine.js 彈窗,使用專案規範的 `x-searchable-select` 動態重建模式 (Dynamic Rebuild Pattern) 與 `focus-within:z-[60]` 防護。 4. 註冊相關 Web Routes。 5. 更新 `ui-minimal-luxury/SKILL.md`,新增 Alpine.js Modal 實作規範,並禁止使用 `hs-overlay` 避免層級堆疊問題。
21 KiB
| name | description |
|---|---|
| 極簡奢華風 UI 實作規範 (Minimal Luxury UI) | 定義 Star Cloud 管理後台的「極簡奢華風」設計規範,包含 CSS Tokens、常用組件樣式、動畫效果與互動模式,確保全站 15+ 模組的視覺一致性。 |
極簡奢華風 UI 實作規範 (Minimal Luxury UI)
Important
開始寫任何頁面前,先對齊本規範。所有規範以此文件為最終依據。 黃金範本頁面:Type A →
admin/machines/index.blade.php|Type B →admin/products/index.blade.php
1. 全局速查卡 (Quick Reference)
1.1 字型規則
| 使用場景 | Tailwind Class |
|---|---|
| 頁面主標題 (H1)、大數字 | font-display(Outfit) |
| 所有內文、按鈕、標籤 | font-sans(Plus Jakarta Sans,預設不需加) |
| SN 碼、版本號、Barcode、時間戳 | font-mono(僅限純英數代碼) |
1.2 文字大小階層
| 元素 | 大小 | 字重 | 顏色 | 其他 |
|---|---|---|---|---|
| 頁面 H1 | text-2xl sm:text-3xl |
font-black |
slate-800 dark:white |
tracking-tight font-display |
| 頁面副標題 | text-sm |
font-bold |
slate-500 dark:slate-400 |
uppercase tracking-widest mt-1 |
| 表格標頭 (th) | text-xs |
font-bold |
slate-500 dark:slate-400 |
uppercase tracking-[0.15em] |
| 表格主要資料 | text-base |
font-extrabold |
slate-800 dark:slate-100 |
tracking-tight |
| 表格次要/描述 | text-xs |
font-bold |
slate-500 dark:slate-400 |
tracking-wide |
| 技術碼(SN/ID/條碼) | text-xs |
font-black |
slate-400 dark:slate-500 |
font-mono uppercase tracking-widest |
| 時間戳 | text-xs |
font-black |
slate-400 |
font-mono tracking-widest |
| 狀態 Badge | text-[10px] |
font-black |
依狀態色系 | uppercase tracking-widest |
| Modal 標題 | text-xl sm:text-2xl |
font-black |
slate-800 dark:white |
font-display tracking-tight |
| Mobile Card 標籤 | text-[10px] |
font-black |
slate-400 dark:slate-500 |
uppercase tracking-widest mb-1 |
| Mobile Card 主標題 | text-base |
font-extrabold |
slate-800 dark:slate-100 |
tracking-tight |
| Mobile Card 副標(技術碼) | text-xs |
font-bold font-mono |
slate-400 dark:slate-500 |
uppercase tracking-widest |
❌ 嚴禁使用
italic|❌ Mobile Card 標題禁用text-lg
1.3 顏色速查
| 用途 | Class |
|---|---|
| 品牌強調/按鈕 | cyan-500 / bg-cyan-500 |
| 成功/上線 | emerald-500 / bg-emerald-500/10 |
| 警告/等待 | amber-500 / bg-amber-500/10 |
| 錯誤/危險 | rose-500 / bg-rose-500/10 |
| 進階/系統 | indigo-500 / bg-indigo-500/10 |
| 停用/次要 | slate-400 / bg-slate-500/10 |
Badge 背景:
bg-{color}/10 border border-{color}/20 text-{color}-500
1.4 互動效果
| 元素 | Hover | Transition |
|---|---|---|
| Primary 按鈕 | hover:bg-cyan-600 + active:scale-95 |
transition-all duration-300 |
| 表格列 (tr) | hover:bg-slate-50/50 dark:hover:bg-white/[0.02] |
transition-colors duration-200 |
| 表格操作按鈕 | hover:bg-cyan-500/5 hover:text-cyan-500 |
transition-all duration-200 |
| 危險操作按鈕 | hover:bg-rose-500/5 hover:text-rose-500 |
transition-all duration-200 |
| Mobile Card 操作 | hover:bg-cyan-500 hover:text-white |
transition-all duration-300 |
1.5 圓角規則
| 元素 | Class |
|---|---|
| 主要內容卡片(桌面) | rounded-3xl |
| Mobile Card | rounded-[2rem](比桌面更大) |
| 按鈕、輸入框 | rounded-xl |
| 狀態 Badge | rounded-full |
| 縮圖卡片、圖示框 | rounded-2xl |
1.6 進場動畫
| 場景 | 做法 |
|---|---|
| 頁面卡片、主內容區 | animate-luxury-in |
| 多卡片依序 | style="animation-delay: 100ms" 遞增 |
| Loading Overlay | <x-luxury-spinner>(禁止手寫) |
| Slide-over 面板 | translate-x-full → translate-x-0 (x-transition) |
2. 強制使用的組件
Important
以下 HTML 結構嚴禁手寫,一律使用組件:
| 禁止手寫 | 改用組件 |
|---|---|
<h1> 標題 + 副標題 + Action 按鈕 |
<x-page-header> |
| Tab 導覽列 | <x-tab-nav> + <x-tab-nav-item> |
| Loading Overlay | <x-luxury-spinner> |
| 搜尋欄 + 重置按鈕(Type A 純列表) | <x-search-bar> |
| 狀態標籤 | <x-status-badge> |
| 統計卡片 | <x-stat-card> |
| 空狀態提示 | <x-empty-state> |
| 刪除確認 | <x-delete-confirm-modal> |
x-luxury-spinner 傳參規則
Caution
- ✅ 正確:
show="isLoadingTable"- ✅ 正確:
show="tabLoading === 'products'"- ❌ 錯誤:
:show="\"tabLoading === 'products'\""→ Blade ParseError!- ❌ 錯誤:
:show="'isLoadingTable'"→ 語意不清
x-status-badge 用法
{{-- 靜態 --}}
<x-status-badge :status="$machine->is_online ? 'online' : 'offline'" />
{{-- Mobile Card 用小尺寸 --}}
<x-status-badge :status="$item->status" size="sm" />
內建狀態:active, inactive, online, offline, enabled, disabled, pending, error
x-empty-state 用法
{{-- 表格 @empty 中 --}}
<x-empty-state mode="table" :colspan="6" :message="__('No data found')" />
{{-- Card Grid @empty 中 --}}
<x-empty-state :message="__('No records found')" />
x-stat-card 用法
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<x-stat-card :label="__('Total')" :value="$stats['total']" />
<x-stat-card :label="__('Online')" :value="$stats['online']" color="cyan" delay="100ms" />
<x-stat-card :label="__('Error')" :value="$stats['error']" color="rose" delay="200ms" />
<x-stat-card :label="__('Idle')" :value="$stats['idle']" color="amber" delay="300ms" />
</div>
3. 頁面佈局範本 (Page Templates)
Type A — 純列表(黃金範本:machines/index.blade.php)
@section('content')
<div class="space-y-4 pb-20" x-data="machineApp()">
<x-page-header
:title="__('Module Name')"
:subtitle="__('Description')"
>
<button @click="openCreateModal()" class="btn-luxury-primary flex items-center gap-2">
<svg class="w-4 h-4">...</svg>
<span class="text-sm sm:text-base">{{ __('Add') }}</span>
</button>
</x-page-header>
<div class="luxury-card rounded-3xl p-8 animate-luxury-in relative overflow-hidden">
<x-luxury-spinner show="isLoadingTable" />
<div id="ajax-content-container" :class="{ 'opacity-30 pointer-events-none': isLoadingTable }">
<x-search-bar
:action="route('admin.xxx.index')"
:value="request('search')"
:placeholder="__('Search...')"
/>
{{-- 桌面表格 --}}
<div class="hidden xl:block overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">
{{ __('Column') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($items as $item)
<tr class="group hover:bg-slate-50/50 dark:hover:bg-white/[0.02] transition-colors duration-200">
<td class="px-6 py-6">...</td>
</tr>
@empty
<x-empty-state mode="table" :colspan="5" :message="__('No records found')" />
@endforelse
</tbody>
</table>
</div>
{{-- Mobile/Tablet Card Grid --}}
<div class="xl:hidden grid grid-cols-1 md:grid-cols-2 gap-6">
@foreach($items as $item)
{{-- 見 Section 4:Mobile Card 黃金範本 --}}
@endforeach
</div>
{{-- 分頁 --}}
<div class="mt-8 border-t border-slate-100/50 dark:border-slate-800/50 pt-6">
{{ $items->appends(request()->query())->links('vendor.pagination.luxury') }}
</div>
</div>
</div>
</div>
@endsection
Type B — Tab + 列表(黃金範本:products/index.blade.php)
@section('content')
<div class="space-y-2 pb-20" x-data="{ activeTab: 'main', tabLoading: null }">
<x-page-header :title="__('Module Name')" :subtitle="__('Description')">
<template x-if="activeTab === 'main'">
<a href="..." class="btn-luxury-primary flex items-center gap-2">...</a>
</template>
</x-page-header>
<x-tab-nav>
<x-tab-nav-item value="main" :label="__('Main List')" />
<x-tab-nav-item value="secondary" :label="__('Secondary')" />
</x-tab-nav>
<div x-show="activeTab === 'main'" class="luxury-card rounded-3xl p-6 animate-luxury-in relative min-h-[300px]" x-cloak>
<x-luxury-spinner show="tabLoading === 'main'" z-index="z-20" />
<div id="tab-main-container">
@include('admin.xxx.partials.tab-main')
</div>
</div>
<div x-show="activeTab === 'secondary'" class="luxury-card rounded-3xl p-6 animate-luxury-in relative min-h-[300px]" x-cloak>
<x-luxury-spinner show="tabLoading === 'secondary'" z-index="z-20" />
<div id="tab-secondary-container">
@include('admin.xxx.partials.tab-secondary')
</div>
</div>
</div>
@endsection
⚠️ Tab Partial 搜尋欄:Tab 頁面的
@submit.prevent需呼叫handleFilterSubmit('tab_name'),而非fetchPage()。因此 Tab Partial 不使用<x-search-bar>,須自行手寫<form @submit.prevent="handleFilterSubmit('...')">結構。
4. Mobile Card 黃金範本
Important
所有 Mobile Card 必須嚴格遵守以下三區結構。禁止自行發明新結構。
卡片容器
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800
bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
區域一:Card Header
<div class="flex items-start justify-between gap-4 mb-6">
<div class="flex items-center gap-4 min-w-0">
{{-- 圖示 56x56,hover 變 cyan --}}
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800
flex items-center justify-center text-slate-400
border border-slate-200 dark:border-slate-700
group-hover:bg-cyan-500 group-hover:text-white
transition-all duration-300 overflow-hidden shadow-sm shrink-0">
{{-- img or SVG icon --}}
</div>
<div class="min-w-0">
{{-- 主標題:text-base font-extrabold,絕對不用 text-lg --}}
<h3 class="text-base font-extrabold text-slate-800 dark:text-slate-100
truncate hover:text-cyan-600 dark:hover:text-cyan-400
transition-colors tracking-tight cursor-pointer">
主要名稱
</h3>
{{-- 副標(SN/Barcode)--}}
<p class="text-xs font-mono font-bold text-slate-400 dark:text-slate-500
uppercase tracking-widest truncate">
副標
</p>
</div>
</div>
{{-- 右側:強制用 x-status-badge,禁止用小圓點 --}}
<x-status-badge :status="$item->status" size="sm" />
</div>
區域二:Info Grid
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
<div>
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Label') }}
</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">值</p>
</div>
{{-- 跨欄(如時間戳)--}}
<div class="col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1">
{{ __('Last Updated') }}
</p>
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 font-mono">
{{ $item->updated_at->format('Y-m-d H:i:s') }}
</p>
</div>
</div>
區域三:Action Buttons
<div class="flex items-center gap-3">
{{-- 一般動作 --}}
<button type="button" @click="..."
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl
bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400
font-bold text-xs hover:bg-cyan-500 hover:text-white
transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5">...</svg>
{{ __('Edit') }}
</button>
{{-- 危險動作 --}}
<button type="button" @click="..."
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl
bg-slate-50 dark:bg-slate-800 text-slate-500 dark:text-slate-400
font-bold text-xs hover:bg-rose-500 hover:text-white
transition-all duration-300 border border-slate-200/50 dark:border-slate-700/50">
<svg class="w-3.5 h-3.5">...</svg>
{{ __('Delete') }}
</button>
</div>
5. RWD 斷點規則
| 元素 | 規則 |
|---|---|
| 桌面表格顯示 | hidden xl:block |
| Mobile Card 顯示 | xl:hidden |
| Card Grid 欄數 | grid-cols-1 md:grid-cols-2 gap-6 |
| 切換點 | 統一 xl(1280px)禁止用 lg 或 2xl |
| Page Header 排列 | 全斷點橫向排列 (flex-row) |
| H1 字大小 | text-2xl sm:text-3xl |
| Tab 列寬 | w-full sm:w-fit(組件已處理) |
| Tab 按鈕 | flex-1 sm:flex-none(組件已處理) |
RWD 禁止事項
Caution
- ❌ Mobile Card 標題用
text-lg(必須text-base font-extrabold)- ❌ Mobile Card 右上角用小圓點代替
x-status-badge- ❌ 操作按鈕不加
flex-1- ❌ Mobile Card 圓角用
rounded-3xl(應為rounded-[2rem])- ❌ Info Grid 欄位標籤用
text-xs(應為text-[10px])- ❌ 桌面/手機切換用
lg斷點
6. 表單元件
{{-- 輸入框 --}}
<input type="text" class="luxury-input py-2.5 pl-11 pr-4 block w-full sm:w-72 text-sm">
{{-- 可搜尋下拉(選項 > 10 筆時)--}}
<x-searchable-select
name="company_id"
:options="$companies"
:selected="request('company_id')"
:placeholder="__('All Companies')"
/>
{{-- 下拉選單遮擋處理範例 --}}
<div class="relative focus-within:z-20">
<label>...</label>
<x-searchable-select ... />
</div>
7. 提示與告警
| 類型 | 組件 |
|---|---|
| 成功/錯誤 Toast | <x-toast /> |
| 刪除確認 Modal | <x-delete-confirm-modal /> |
| 頁面內警告(琥珀色) | <div class="p-5 bg-amber-500/10 border border-amber-500/20 text-amber-600 rounded-2xl flex items-start gap-4 font-bold"> |
7.1 Modal 實作規範(強制)
Caution
嚴禁使用 Preline 的
hs-overlay系統來實作 Modal。 本專案全部 Modal 統一使用 Alpine.js (x-show+x-transition) 實作。 使用hs-overlayclass 會導致 Modal 出現但完全透明(opacity: 0),因為 Preline overlay 的動畫 class 在此專案中未被正確初始化。
正確:Alpine.js Modal 標準結構(參考 modal-replenishment-create.blade.php)
{{-- 1. 觸發按鈕:用 $dispatch 發送 window 事件 --}}
<button type="button" @click="$dispatch('open-xxx-modal')">開啟</button>
{{-- 2. Modal 主體:放在 layout 底部,x-data 宣告 Alpine state --}}
<div x-data="{ showModal: false }" @open-xxx-modal.window="showModal = true" x-cloak>
<div x-show="showModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 ...">
{{-- Backdrop --}}
<div x-show="showModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm"
@click="showModal = false"></div>
{{-- Modal 內容 --}}
<div x-show="showModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:scale-95"
class="relative inline-flex flex-col align-bottom bg-white dark:bg-slate-900
rounded-[2.5rem] text-left shadow-2xl transform
sm:my-8 sm:align-middle sm:max-w-lg w-full
border border-slate-100 dark:border-slate-800 z-10"
@click.stop>
{{-- Header --}}
<div class="px-10 py-8 pb-4 flex items-center justify-between">
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight">
標題
</h3>
<button @click="showModal = false"
class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 transition-all border border-slate-100 dark:border-slate-700">
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{{-- Body / Footer --}}
<div class="px-10 py-6">...</div>
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex justify-end gap-4">
<button @click="showModal = false" class="btn-luxury-ghost px-8">取消</button>
<button type="submit" class="btn-luxury-primary px-12">確認</button>
</div>
</div>
</div>
</div>
</div>
跨元件開啟 Modal(dropdown 內的按鈕):
- 按鈕在 Alpine.js dropdown 內時,需先關閉 dropdown 再發送事件:
<button @click="open = false; $dispatch('open-xxx-modal')">切換帳號</button> - Modal 透過
@open-xxx-modal.window接收(window層級才能跨 Alpine scope 傳遞)
8. AJAX fetchPage 標準實作
async fetchPage(url) {
if (!url || this.isLoadingTable) return;
this.isLoadingTable = true;
try {
const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const newContent = doc.querySelector('#ajax-content-container');
if (newContent) {
document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML;
window.history.pushState({}, '', url);
if (window.HSStaticMethods?.autoInit) window.HSStaticMethods.autoInit();
}
} catch (e) {
console.error('AJAX Load Failed:', e);
} finally {
this.isLoadingTable = false;
}
}
分頁:
{{ $items->appends(request()->query())->links('vendor.pagination.luxury') }}
9. 區塊圖示色彩(詳情/編輯頁)
| 區塊 | 色系 | Class |
|---|---|---|
| 基本資訊 | 翠綠 Emerald | bg-emerald-500/10 text-emerald-500 |
| 硬體/插槽 | 琥珀 Amber | bg-amber-500/10 text-amber-500 |
| 系統/進階 | 靛藍 Indigo | bg-indigo-500/10 text-indigo-500 |
| 危險/刪除 | 玫瑰 Rose | bg-rose-500/10 text-rose-500 |
10. Z-Index 堆疊與遮擋處理 (Z-Index Stacking)
Important
當頁面或 Modal 中存在多個垂直排列的 x-searchable-select 時,為防止選單開展時被下一個欄位遮擋,嚴禁使用固定的 z-index。
10.1 動態層級提升模式
必須在欄位容器上使用 focus-within:z-[index],確保正在操作的欄位永遠處於堆疊最前方。
| 使用場景 | 推薦 Class |
|---|---|
| 一般表單頁面 | relative focus-within:z-[20] |
| 模態框 (Modal) 內 | relative focus-within:z-[60] |
10.2 代碼範例
<div class="space-y-4">
<!-- 型號選單 -->
<div class="relative focus-within:z-[60]">
<label>型號</label>
<x-searchable-select ... />
</div>
<!-- 公司選單 -->
<div class="relative focus-within:z-[60]">
<label>公司</label>
<x-searchable-select ... />
</div>
</div>