star-cloud/.agents/skills/ui-minimal-luxury/SKILL.md
sky121113 ea438d55d1 [FEAT] 實作系統管理員切換帳號 (Impersonate) 功能
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` 避免層級堆疊問題。
2026-05-17 11:07:41 +08:00

21 KiB
Raw Blame History

name description
極簡奢華風 UI 實作規範 (Minimal Luxury UI) 定義 Star Cloud 管理後台的「極簡奢華風」設計規範,包含 CSS Tokens、常用組件樣式、動畫效果與互動模式確保全站 15+ 模組的視覺一致性。

極簡奢華風 UI 實作規範 (Minimal Luxury UI)

Important

開始寫任何頁面前,先對齊本規範。所有規範以此文件為最終依據。 黃金範本頁面Type A → admin/machines/index.blade.phpType B → admin/products/index.blade.php


1. 全局速查卡 (Quick Reference)

1.1 字型規則

使用場景 Tailwind Class
頁面主標題 (H1)、大數字 font-displayOutfit
所有內文、按鈕、標籤 font-sansPlus 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 4Mobile 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">
        {{-- 圖示 56x56hover 變 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
切換點 統一 xl1280px禁止用 lg2xl
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-overlay class 會導致 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>

跨元件開啟 Modaldropdown 內的按鈕):

  • 按鈕在 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>