@mundogamernetwork/shared-ui 1.0.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.
Files changed (87) hide show
  1. package/README.md +283 -0
  2. package/components/PressKit/AssetGallery.vue +349 -0
  3. package/components/PressKit/Awards.vue +100 -0
  4. package/components/PressKit/Credits.vue +78 -0
  5. package/components/PressKit/FactSheet.vue +204 -0
  6. package/components/PressKit/Hero.vue +143 -0
  7. package/components/PressKit/Quotes.vue +80 -0
  8. package/components/PressKit/VideoPlayer.vue +134 -0
  9. package/components/checkout/MgCartItemList.vue +214 -0
  10. package/components/checkout/MgCartSummary.vue +204 -0
  11. package/components/checkout/MgCheckoutSidebar.vue +230 -0
  12. package/components/checkout/MgGuestEmailForm.vue +97 -0
  13. package/components/checkout/MgPaymentMethodSelector.vue +162 -0
  14. package/components/checkout/MgPixQRCode.vue +222 -0
  15. package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
  16. package/components/indie-wall/MuralCanvas.vue +481 -0
  17. package/components/indie-wall/StepBlock.vue +314 -0
  18. package/components/indie-wall/StepCustomize.vue +530 -0
  19. package/components/indie-wall/StepGoal.vue +169 -0
  20. package/components/indie-wall/StepPackage.vue +145 -0
  21. package/components/indie-wall/StepPay.vue +209 -0
  22. package/components/indie-wall/SupportStepper.vue +372 -0
  23. package/components/invoices/MgInvoiceDownload.vue +50 -0
  24. package/components/pricing/MgBillingToggle.vue +74 -0
  25. package/components/pricing/MgPricingCard.vue +245 -0
  26. package/components/ui/Header/MgMessageCard.vue +147 -0
  27. package/components/ui/Header/MgMessageModal.vue +414 -0
  28. package/components/ui/Header/MgNotificationCard.vue +200 -0
  29. package/components/ui/Header/MgNotificationsModal.vue +125 -0
  30. package/components/ui/MgAnnouncementBanner.vue +147 -0
  31. package/components/ui/MgBanners.vue +23 -0
  32. package/components/ui/MgHeaderComponent.vue +283 -0
  33. package/components/ui/MgHeaderUIConfig.vue +225 -0
  34. package/components/ui/MgHeaderUIUser.vue +301 -0
  35. package/components/ui/MgLoginModal.vue +156 -0
  36. package/components/ui/MgPromotionBanner.vue +185 -0
  37. package/composables/useLogout.ts +42 -0
  38. package/composables/useMgCheckout.ts +287 -0
  39. package/composables/useMgUserNotifications.ts +122 -0
  40. package/composables/usePaymentMethods.ts +75 -0
  41. package/composables/useSubscription.ts +163 -0
  42. package/middleware/auth.global.ts +40 -0
  43. package/nuxt.config.ts +31 -0
  44. package/package.json +40 -0
  45. package/pages/[slug]/index.vue +112 -0
  46. package/pages/about.vue +133 -0
  47. package/pages/blog.vue +430 -0
  48. package/pages/careers.vue +329 -0
  49. package/pages/contact.vue +339 -0
  50. package/pages/faq.vue +317 -0
  51. package/pages/health-check.vue +20 -0
  52. package/pages/icons.vue +58 -0
  53. package/pages/magazine/[slug].vue +209 -0
  54. package/pages/magazine/index.vue +267 -0
  55. package/pages/media-kit/[slug].vue +625 -0
  56. package/pages/mural/[slug].vue +1058 -0
  57. package/pages/partners.vue +290 -0
  58. package/pages/press.vue +237 -0
  59. package/pages/presskit/[slug].vue +191 -0
  60. package/pages/roadmap.vue +355 -0
  61. package/pages/status.vue +199 -0
  62. package/pages/team.vue +266 -0
  63. package/pages/wall/[slug].vue +11 -0
  64. package/plugins/auth.client.ts +17 -0
  65. package/plugins/echo.client.ts +132 -0
  66. package/services/authService.ts +95 -0
  67. package/services/chatService.ts +53 -0
  68. package/services/contactService.ts +35 -0
  69. package/services/documentService.ts +16 -0
  70. package/services/httpService.ts +95 -0
  71. package/services/indieWallService.ts +174 -0
  72. package/services/institutionalService.ts +248 -0
  73. package/services/mediaKitService.ts +51 -0
  74. package/services/notificationsService.ts +20 -0
  75. package/services/pressKitService.ts +55 -0
  76. package/stores/announcement.ts +129 -0
  77. package/stores/auth.ts +86 -0
  78. package/stores/chat.ts +150 -0
  79. package/stores/contact.ts +28 -0
  80. package/stores/document.ts +27 -0
  81. package/stores/index.ts +34 -0
  82. package/stores/institutional.ts +231 -0
  83. package/stores/login.ts +27 -0
  84. package/stores/notifications.ts +133 -0
  85. package/stores/promotion.ts +154 -0
  86. package/types/index.ts +135 -0
  87. package/utils/serialize.ts +29 -0
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # @mundogamernetwork/shared-ui
2
+
3
+ Nuxt 3 Layer compartilhado entre todos os frontends da Mundo Gamer Network.
4
+
5
+ ## O que inclui
6
+
7
+ ### Componentes
8
+ - **MgHeaderComponent** - Header unificado com notificações, chat, user menu, settings
9
+ - **MgHeaderUIUser** - Dropdown do usuário (login/register, avatar, logout)
10
+ - **MgHeaderUIConfig** - Dropdown de configurações (dark mode, idioma)
11
+ - **MgLoginModal** - Modal de login/registro
12
+ - **MgNotificationsModal** - Lista de notificações com infinite scroll
13
+ - **MgNotificationCard** - Card individual de notificação
14
+ - **MgMessageModal** - Interface completa de chat
15
+ - **MgMessageCard** - Card de conversa na lista de chats
16
+
17
+ ### Páginas institucionais (More)
18
+ `about`, `faq`, `careers`, `team`, `press`, `partners`, `roadmap`, `status`, `blog`, `contact`, `magazine`, `health-check`, `icons`
19
+
20
+ Todas filtram por `systemId` (platform_id) e compartilham a mesma estrutura visual.
21
+
22
+ ### Páginas de erro
23
+ - **error.vue** - Página de erro global (404, 500, etc.)
24
+
25
+ ### Páginas dinâmicas
26
+ - **[slug]/index.vue** - Páginas de documentos dinâmicos (termos, políticas, etc.)
27
+
28
+ ### Services
29
+ - **httpService** - Axios com interceptors (error handling, lang, timezone, bearer token)
30
+ - **authService** - Autenticação (authenticate, getUser, logout)
31
+ - **notificationsService** - CRUD de notificações
32
+ - **chatService** - CRUD de chat (endpoints configuráveis)
33
+ - **institutionalService** - APIs institucionais (FAQ, careers, team, press, etc.)
34
+ - **contactService** - Formulário de contato
35
+ - **documentService** - Documentos dinâmicos (termos, políticas)
36
+
37
+ ### Stores (Pinia)
38
+ - **useAuthStore** - Estado do usuário com persistência em localStorage
39
+ - **useLoginStore** - Estado do modal de login
40
+ - **useIndexStore** - Estado de sidebar e modais
41
+ - **useNotificationsStore** - Notificações com filtro por systemId
42
+ - **useChatStore** - Chat com WebSocket e polling fallback
43
+ - **useInstitutionalStore** - Cache de dados institucionais
44
+ - **useContactStore** - Estado do formulário de contato
45
+ - **useDocumentStore** - Documentos dinâmicos
46
+
47
+ ### Composables
48
+ - **useLogout()** - Logout unificado (API + cookies + localStorage + store reset + redirect)
49
+ - **useUserNotifications()** - Notificações reativas com filtro por systemId
50
+
51
+ ### Plugins
52
+ - **echo.client** - Laravel Echo / Pusher com auto-reconnect
53
+ - **auth.client** - Auto-fetch do usuário ao montar o app
54
+
55
+ ### Middleware
56
+ - **auth.global** - Proteção de rotas configurável via `protectedRoutes`
57
+
58
+ ---
59
+
60
+ ## Instalação
61
+
62
+ ### 1. Adicionar dependência
63
+
64
+ ```bash
65
+ npm install git+ssh://git@github.com/Mundo-Gamer-Network/shared-ui.git#v1.0.0
66
+ ```
67
+
68
+ Ou no `package.json`:
69
+
70
+ ```json
71
+ {
72
+ "dependencies": {
73
+ "@mundogamernetwork/shared-ui": "git+ssh://git@github.com/Mundo-Gamer-Network/shared-ui.git#v1.0.0"
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### 2. Configurar como Nuxt Layer
79
+
80
+ No `nuxt.config.ts` do app:
81
+
82
+ ```ts
83
+ export default defineNuxtConfig({
84
+ extends: ['@mundogamernetwork/shared-ui'],
85
+
86
+ runtimeConfig: {
87
+ public: {
88
+ mgSharedUi: {
89
+ platform: 'MGC', // Identificador do app (MGC, MGTV, MGN, etc.)
90
+ systemId: '1', // VITE_SYSTEM_ID - filtra notificações, FAQ, careers, etc.
91
+ apiBaseURL: 'https://api.mundogamer.network',
92
+ accountsBaseUrl: 'https://accounts.mundogamer.network',
93
+ networkBaseUrl: 'https://network.mundogamer.network',
94
+ features: {
95
+ chat: true, // Habilita chat no header
96
+ notifications: true, // Habilita notificações no header
97
+ wallet: false, // Habilita sistema de moedas (futuro)
98
+ darkMode: true, // Habilita toggle de dark mode
99
+ search: false, // Habilita barra de busca no header
100
+ statusOnline: true // Habilita toggle de status online
101
+ },
102
+ languages: ['pt-BR', 'en', 'es', 'de', 'ro'],
103
+ protectedRoutes: [ // Rotas que exigem autenticação
104
+ '/profile',
105
+ '/settings',
106
+ '/dashboard'
107
+ ]
108
+ }
109
+ }
110
+ }
111
+ })
112
+ ```
113
+
114
+ ### 3. Peer dependencies
115
+
116
+ O app precisa ter instalado:
117
+
118
+ ```json
119
+ {
120
+ "@pinia/nuxt": ">=0.5.0",
121
+ "@pinia-plugin-persistedstate/nuxt": ">=1.0.0",
122
+ "axios": ">=1.0.0",
123
+ "laravel-echo": ">=1.15.0",
124
+ "pinia": ">=2.1.0",
125
+ "pusher-js": ">=8.0.0"
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Configuração por App
132
+
133
+ ### Feature flags
134
+
135
+ Cada app habilita apenas o que usa:
136
+
137
+ | App | chat | notifications | wallet | darkMode | search | statusOnline |
138
+ |-----|------|--------------|--------|----------|--------|-------------|
139
+ | community-frontend | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
140
+ | tv-frontend | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
141
+ | jobs-frontend | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
142
+ | academy-frontend | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ |
143
+ | token-frontend | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
144
+ | club-frontend | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
145
+ | agency-frontend | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
146
+ | network-site | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
147
+ | network-accounts | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
148
+
149
+ ### Chat endpoints customizados (jobs-frontend)
150
+
151
+ O jobs-frontend usa endpoints diferentes para o chat. Configure assim:
152
+
153
+ ```ts
154
+ mgSharedUi: {
155
+ chatEndpoints: {
156
+ list: '/public/vacancy-application/chats',
157
+ send: '/public/vacancy-application/chats/messages/',
158
+ show: '/public/vacancy-application/chats/{chatId}',
159
+ unread: '/public/vacancy-application/chats/messages/unread/count',
160
+ delete: '/public/vacancy-application/chats/{chatId}',
161
+ deleteMessage: '/public/vacancy-application/chats/messages/{messageId}'
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### Rotas protegidas
167
+
168
+ Defina as rotas que exigem login. O middleware redireciona para accounts com `redirect_to`:
169
+
170
+ ```ts
171
+ mgSharedUi: {
172
+ protectedRoutes: ['/profile', '/settings', '/dashboard']
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Sobrescrita (Override)
179
+
180
+ Por ser um Nuxt Layer, qualquer arquivo pode ser sobrescrito no app consumidor. Basta criar o arquivo com o mesmo caminho:
181
+
182
+ ```
183
+ # Sobrescrever a página about
184
+ app/pages/about.vue → substitui shared-ui/pages/about.vue
185
+
186
+ # Sobrescrever um componente
187
+ app/components/ui/MgHeaderComponent.vue → substitui o header padrão
188
+
189
+ # Adicionar um store extra
190
+ app/stores/custom.ts → adiciona sem conflito
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Atualização
196
+
197
+ Para atualizar para uma nova versão:
198
+
199
+ ```bash
200
+ # Se usa tag específica, mude a tag no package.json e reinstale:
201
+ npm install git+ssh://git@github.com/Mundo-Gamer-Network/shared-ui.git#v1.1.0
202
+
203
+ # Se aponta para main (sem tag):
204
+ npm update @mundogamernetwork/shared-ui
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Variáveis de ambiente necessárias no app
210
+
211
+ ```env
212
+ VITE_API_BASE_URL=https://api.mundogamer.network
213
+ VITE_BASE_ACCOUNTS_URL=https://accounts.mundogamer.network
214
+ VITE_BASE_URL_NETWORK=https://network.mundogamer.network
215
+ VITE_SYSTEM_ID=1
216
+ VITE_PUSHER_APP_KEY=your_pusher_key
217
+ VITE_PUSHER_APP_CLUSTER=mt1
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Estrutura
223
+
224
+ ```
225
+ shared-ui/
226
+ ├── components/ui/
227
+ │ ├── Header/
228
+ │ │ ├── MgMessageCard.vue
229
+ │ │ ├── MgMessageModal.vue
230
+ │ │ ├── MgNotificationCard.vue
231
+ │ │ └── MgNotificationsModal.vue
232
+ │ ├── MgHeaderComponent.vue
233
+ │ ├── MgHeaderUIConfig.vue
234
+ │ ├── MgHeaderUIUser.vue
235
+ │ └── MgLoginModal.vue
236
+ ├── composables/
237
+ │ ├── useLogout.ts
238
+ │ └── useUserNotifications.ts
239
+ ├── middleware/
240
+ │ └── auth.global.ts
241
+ ├── pages/
242
+ │ ├── [slug]/index.vue
243
+ │ ├── magazine/
244
+ │ │ ├── index.vue
245
+ │ │ └── [slug].vue
246
+ │ ├── about.vue
247
+ │ ├── blog.vue
248
+ │ ├── careers.vue
249
+ │ ├── contact.vue
250
+ │ ├── faq.vue
251
+ │ ├── health-check.vue
252
+ │ ├── icons.vue
253
+ │ ├── partners.vue
254
+ │ ├── press.vue
255
+ │ ├── roadmap.vue
256
+ │ ├── status.vue
257
+ │ └── team.vue
258
+ ├── plugins/
259
+ │ ├── auth.client.ts
260
+ │ └── echo.client.ts
261
+ ├── services/
262
+ │ ├── authService.ts
263
+ │ ├── chatService.ts
264
+ │ ├── contactService.ts
265
+ │ ├── documentService.ts
266
+ │ ├── httpService.ts
267
+ │ ├── institutionalService.ts
268
+ │ └── notificationsService.ts
269
+ ├── stores/
270
+ │ ├── auth.ts
271
+ │ ├── chat.ts
272
+ │ ├── contact.ts
273
+ │ ├── document.ts
274
+ │ ├── index.ts
275
+ │ ├── institutional.ts
276
+ │ ├── login.ts
277
+ │ └── notifications.ts
278
+ ├── types/index.ts
279
+ ├── utils/serialize.ts
280
+ ├── error.vue
281
+ ├── nuxt.config.ts
282
+ └── package.json
283
+ ```
@@ -0,0 +1,349 @@
1
+ <script setup lang="ts">
2
+ interface Asset {
3
+ id: number;
4
+ type: string;
5
+ title?: string;
6
+ url: string;
7
+ thumbnail_url?: string;
8
+ file_size?: number;
9
+ mime_type?: string;
10
+ is_featured?: boolean;
11
+ sort_order?: number;
12
+ }
13
+
14
+ interface Props {
15
+ assets: Asset[];
16
+ allowDownload?: boolean;
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), { allowDownload: true });
20
+
21
+ const { t } = useI18n();
22
+
23
+ const selectedType = ref('all');
24
+ const lightboxIndex = ref<number | null>(null);
25
+
26
+ const assetTypes = computed(() => {
27
+ const types = new Set(props.assets.map(a => a.type));
28
+ return ['all', ...Array.from(types)];
29
+ });
30
+
31
+ const filteredAssets = computed(() => {
32
+ if (selectedType.value === 'all') return props.assets;
33
+ return props.assets.filter(a => a.type === selectedType.value);
34
+ });
35
+
36
+ const imageAssets = computed(() => filteredAssets.value.filter(a =>
37
+ ['screenshot', 'artwork', 'logo', 'gif'].includes(a.type)
38
+ ));
39
+
40
+ function openLightbox(idx: number) {
41
+ lightboxIndex.value = idx;
42
+ }
43
+
44
+ function closeLightbox() {
45
+ lightboxIndex.value = null;
46
+ }
47
+
48
+ function prevImage() {
49
+ if (lightboxIndex.value !== null && lightboxIndex.value > 0) {
50
+ lightboxIndex.value--;
51
+ }
52
+ }
53
+
54
+ function nextImage() {
55
+ if (lightboxIndex.value !== null && lightboxIndex.value < imageAssets.value.length - 1) {
56
+ lightboxIndex.value++;
57
+ }
58
+ }
59
+
60
+ function formatFileSize(bytes?: number) {
61
+ if (!bytes) return '';
62
+ if (bytes < 1024) return `${bytes} B`;
63
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
64
+ return `${(bytes / 1048576).toFixed(1)} MB`;
65
+ }
66
+
67
+ function downloadAsset(asset: Asset) {
68
+ window.open(asset.url, '_blank');
69
+ }
70
+
71
+ function downloadAll() {
72
+ props.assets.forEach(asset => {
73
+ window.open(asset.url, '_blank');
74
+ });
75
+ }
76
+ </script>
77
+
78
+ <template>
79
+ <div class="asset-gallery">
80
+ <div class="gallery-header">
81
+ <h3 class="section-title">{{ $t('press_kit.assets') }}</h3>
82
+ <button v-if="allowDownload && assets.length" class="btn-download-all" @click="downloadAll">
83
+ <MGIcon icon="download" /> {{ $t('press_kit.download_all') }}
84
+ </button>
85
+ </div>
86
+
87
+ <div v-if="assetTypes.length > 2" class="type-filters">
88
+ <button
89
+ v-for="type in assetTypes"
90
+ :key="type"
91
+ :class="['filter-btn', { active: selectedType === type }]"
92
+ @click="selectedType = type"
93
+ >
94
+ {{ type === 'all' ? $t('press_kit.all') : type }}
95
+ </button>
96
+ </div>
97
+
98
+ <div class="gallery-grid">
99
+ <div
100
+ v-for="(asset, idx) in filteredAssets"
101
+ :key="asset.id"
102
+ class="asset-card"
103
+ @click="['screenshot', 'artwork', 'logo', 'gif'].includes(asset.type) ? openLightbox(imageAssets.indexOf(asset)) : downloadAsset(asset)"
104
+ >
105
+ <div class="asset-thumb">
106
+ <img
107
+ v-if="['screenshot', 'artwork', 'logo', 'gif'].includes(asset.type)"
108
+ :src="asset.thumbnail_url || asset.url"
109
+ :alt="asset.title || asset.type"
110
+ loading="lazy"
111
+ />
112
+ <div v-else class="asset-icon">
113
+ <MGIcon :icon="asset.type === 'document' ? 'file' : 'film'" />
114
+ </div>
115
+ <span class="asset-type-badge">{{ asset.type }}</span>
116
+ </div>
117
+ <div class="asset-info">
118
+ <span class="asset-title">{{ asset.title || asset.type }}</span>
119
+ <span v-if="asset.file_size" class="asset-size">{{ formatFileSize(asset.file_size) }}</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div v-if="!assets.length" class="empty-state">
125
+ {{ $t('press_kit.no_assets') }}
126
+ </div>
127
+
128
+ <!-- Lightbox -->
129
+ <div v-if="lightboxIndex !== null" class="lightbox" @click.self="closeLightbox">
130
+ <button class="lb-close" @click="closeLightbox">&times;</button>
131
+ <button v-if="lightboxIndex > 0" class="lb-nav lb-prev" @click="prevImage">&lsaquo;</button>
132
+ <img :src="imageAssets[lightboxIndex]?.url" :alt="imageAssets[lightboxIndex]?.title" />
133
+ <button v-if="lightboxIndex < imageAssets.length - 1" class="lb-nav lb-next" @click="nextImage">&rsaquo;</button>
134
+ <div class="lb-info">
135
+ <span>{{ imageAssets[lightboxIndex]?.title || imageAssets[lightboxIndex]?.type }}</span>
136
+ <button v-if="allowDownload" class="lb-download" @click="downloadAsset(imageAssets[lightboxIndex])">
137
+ <MGIcon icon="download" /> {{ $t('press_kit.download') }}
138
+ </button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </template>
143
+
144
+ <style lang="scss" scoped>
145
+ .asset-gallery {
146
+ .gallery-header {
147
+ display: flex;
148
+ justify-content: space-between;
149
+ align-items: center;
150
+ margin-bottom: 16px;
151
+ }
152
+
153
+ .section-title {
154
+ font-size: 1.2rem;
155
+ font-weight: 700;
156
+ color: #fff;
157
+ }
158
+
159
+ .btn-download-all {
160
+ background: var(--color-primary, #FDB215);
161
+ color: #13161C;
162
+ border: none;
163
+ padding: 8px 16px;
164
+ font-weight: 600;
165
+ font-size: 0.85rem;
166
+ cursor: pointer;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ transition: opacity 0.2s;
171
+
172
+ &:hover { opacity: 0.85; }
173
+ }
174
+
175
+ .type-filters {
176
+ display: flex;
177
+ gap: 8px;
178
+ margin-bottom: 16px;
179
+ flex-wrap: wrap;
180
+ }
181
+
182
+ .filter-btn {
183
+ background: #2a2d35;
184
+ color: #aaa;
185
+ border: 1px solid #3a3d45;
186
+ padding: 6px 14px;
187
+ font-size: 0.85rem;
188
+ cursor: pointer;
189
+ text-transform: capitalize;
190
+ transition: all 0.2s;
191
+
192
+ &.active {
193
+ background: var(--color-primary, #FDB215);
194
+ color: #13161C;
195
+ border-color: var(--color-primary, #FDB215);
196
+ }
197
+
198
+ &:hover:not(.active) {
199
+ background: #3a3d45;
200
+ color: #fff;
201
+ }
202
+ }
203
+
204
+ .gallery-grid {
205
+ display: grid;
206
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
207
+ gap: 16px;
208
+ }
209
+
210
+ .asset-card {
211
+ background: #191B20;
212
+ border: 1px solid #1e2028;
213
+ overflow: hidden;
214
+ cursor: pointer;
215
+ transition: border-color 0.2s;
216
+
217
+ &:hover {
218
+ border-color: #3a3d45;
219
+ }
220
+ }
221
+
222
+ .asset-thumb {
223
+ position: relative;
224
+ aspect-ratio: 16 / 10;
225
+ background: #13161C;
226
+
227
+ img {
228
+ width: 100%;
229
+ height: 100%;
230
+ object-fit: cover;
231
+ }
232
+ }
233
+
234
+ .asset-icon {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: center;
238
+ height: 100%;
239
+ font-size: 2rem;
240
+ color: #555;
241
+ }
242
+
243
+ .asset-type-badge {
244
+ position: absolute;
245
+ top: 8px;
246
+ left: 8px;
247
+ background: rgba(0, 0, 0, 0.7);
248
+ color: #fff;
249
+ padding: 2px 8px;
250
+ font-size: 0.7rem;
251
+ text-transform: uppercase;
252
+ }
253
+
254
+ .asset-info {
255
+ padding: 10px 12px;
256
+ display: flex;
257
+ justify-content: space-between;
258
+ align-items: center;
259
+ }
260
+
261
+ .asset-title {
262
+ font-size: 0.85rem;
263
+ color: #fff;
264
+ text-transform: capitalize;
265
+ }
266
+
267
+ .asset-size {
268
+ font-size: 0.75rem;
269
+ color: #888;
270
+ }
271
+
272
+ .empty-state {
273
+ text-align: center;
274
+ padding: 40px;
275
+ color: #666;
276
+ }
277
+
278
+ // Lightbox
279
+ .lightbox {
280
+ position: fixed;
281
+ inset: 0;
282
+ background: rgba(0, 0, 0, 0.95);
283
+ z-index: 9999;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+
288
+ img {
289
+ max-width: 90vw;
290
+ max-height: 80vh;
291
+ object-fit: contain;
292
+ }
293
+ }
294
+
295
+ .lb-close {
296
+ position: absolute;
297
+ top: 20px;
298
+ right: 20px;
299
+ background: none;
300
+ border: none;
301
+ color: #fff;
302
+ font-size: 2.5rem;
303
+ cursor: pointer;
304
+ z-index: 1;
305
+ }
306
+
307
+ .lb-nav {
308
+ position: absolute;
309
+ top: 50%;
310
+ transform: translateY(-50%);
311
+ background: rgba(255, 255, 255, 0.1);
312
+ border: none;
313
+ color: #fff;
314
+ font-size: 3rem;
315
+ padding: 20px 12px;
316
+ cursor: pointer;
317
+ transition: background 0.2s;
318
+
319
+ &:hover { background: rgba(255, 255, 255, 0.2); }
320
+ }
321
+
322
+ .lb-prev { left: 20px; }
323
+ .lb-next { right: 20px; }
324
+
325
+ .lb-info {
326
+ position: absolute;
327
+ bottom: 20px;
328
+ left: 50%;
329
+ transform: translateX(-50%);
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 16px;
333
+ color: #fff;
334
+ }
335
+
336
+ .lb-download {
337
+ background: var(--color-primary, #FDB215);
338
+ color: #13161C;
339
+ border: none;
340
+ padding: 6px 14px;
341
+ font-weight: 600;
342
+ font-size: 0.85rem;
343
+ cursor: pointer;
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 6px;
347
+ }
348
+ }
349
+ </style>