Добавил файл .env.demo для локального тестирования брендинга, добавил использование --host в vite, улучшил адаптивность интерфейса, обновил README с инструкциями по кастомизации и изменил структуру компонентов для улучшения стилей

This commit is contained in:
Ilia Miheev
2026-08-03 22:18:46 +03:00
parent d475ed775b
commit 215cc195de
12 changed files with 218 additions and 120 deletions
+21
View File
@@ -0,0 +1,21 @@
# Local test branding — not committed (.gitignore).
# Delete or rename this file to fall back to built-in defaults.
# Branding
IRANDOM_NAME=LuckyDraw
IRANDOM_TITLE=LuckyDraw - test theme
IRANDOM_BRAND_URL=https://example.com
IRANDOM_LOGO_URL=/logo.png
IRANDOM_DEFAULT_LOCALE=en
# Cool slate + teal (contrast check vs default gold/brown)
IRANDOM_COLOR_PRIMARY=#2dd4bf
IRANDOM_COLOR_ACCENT=#5eead4
IRANDOM_COLOR_BG=#0f172a
IRANDOM_COLOR_SURFACE=#1e293b
IRANDOM_COLOR_SURFACE_RAISED=#334155
IRANDOM_COLOR_TEXT=#f1f5f9
IRANDOM_COLOR_TEXT_MUTED=#94a3b8
IRANDOM_COLOR_DANGER=#f87171
IRANDOM_COLOR_LINK=#38bdf8
IRANDOM_COLOR_BORDER=#475569
+2
View File
@@ -2,6 +2,8 @@
/node_modules /node_modules
/dist /dist
# Generated at serve/build / Docker entrypoint
/public/config.js
# local env files # local env files
.env .env
+14 -12
View File
@@ -3,33 +3,35 @@
Сервис генерации случайных значений: числа из диапазона, выбор из списка, орёл/решка. Сервис генерации случайных значений: числа из диапазона, выбор из списка, орёл/решка.
Vue 3 + Vite + Element Plus. Тема и брендинг настраиваются через env / Docker. Vue 3 + Vite + Element Plus. Тема и брендинг настраиваются через env / Docker.
**Версия:** `1.0.0` **Версия:** `1.0.1`
## Setup ## Установка
``` ```
npm install npm install
``` ```
### Dev server ### Dev-сервер
``` ```
npm run serve npm run serve
``` ```
### Production build ### Production-сборка
``` ```
npm run build npm run build
npm run preview npm run preview
``` ```
### Lint ### Линт
``` ```
npm run lint npm run lint
``` ```
## Customization ## Кастомизация
Copy `.env.example` to `.env` and set `IRANDOM_*` variables Скопируйте `.env.example` в `.env` и задайте переменные `IRANDOM_*`
(name, title, logo URL, colors, default locale). (имя, title, URL логотипа, цвета, язык по умолчанию).
`npm run serve` / `npm run build` regenerates `public/config.js`. `npm run serve` / `npm run build` пересобирают `public/config.js`.
Чтобы локально проверить другую палитру брендинга, скопируйте `.env.demo``.env` и перезапустите `serve`.
## Docker ## Docker
``` ```
@@ -37,6 +39,6 @@ docker compose up --build
``` ```
→ http://localhost:8080 → http://localhost:8080
## Offline (PWA) ## Офлайн (PWA)
After one online visit to a production build (`build` + `preview` or Docker), После одного визита онлайн к production-сборке (`build` + `preview` или Docker)
the app shell works offline. Runtime branding updates title, theme-color, icons and manifest. оболочка приложения работает офлайн. Runtime-брендинг обновляет title, theme-color, иконки и manifest.
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
irandom: irandom:
build: . build: .
image: irandom:1.0.0 image: irandom:1.0.1
ports: ports:
- "8080:80" - "8080:80"
environment: environment:
+2 -2
View File
@@ -8,12 +8,12 @@
<meta name="description" content="Random value generator" /> <meta name="description" content="Random value generator" />
<link rel="icon" href="/logo.png" /> <link rel="icon" href="/logo.png" />
<link rel="apple-touch-icon" href="/logo.png" /> <link rel="apple-touch-icon" href="/logo.png" />
<title>irandom</title> <title></title>
<script src="/config.js"></script> <script src="/config.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>Для работы irandom нужен JavaScript.</strong> <strong>Для работы сервиса нужен JavaScript.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
+4 -4
View File
@@ -1,16 +1,16 @@
{ {
"name": "irandom", "name": "irandom",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"generate-config": "node scripts/generate-config.js", "generate-config": "node scripts/generate-config.js",
"predev": "node scripts/generate-config.js", "predev": "node scripts/generate-config.js",
"dev": "vite", "dev": "vite --host",
"preserve": "node scripts/generate-config.js", "preserve": "node scripts/generate-config.js",
"serve": "vite", "serve": "vite --host",
"prebuild": "node scripts/generate-config.js", "prebuild": "node scripts/generate-config.js",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview --host",
"lint": "eslint --ext .js,.vue src" "lint": "eslint --ext .js,.vue src"
}, },
"dependencies": { "dependencies": {
-20
View File
@@ -1,20 +0,0 @@
/* Generated by scripts/generate-config.js — do not edit by hand */
window.__APP_CONFIG__ = {
"name": "irandom",
"title": "irandom",
"brandUrl": "https://git.imiheev.online/admin/irandom",
"logoUrl": "/logo.png",
"defaultLocale": "ru",
"colors": {
"primary": "#c9a66b",
"accent": "#e6c566",
"bg": "#1a1815",
"surface": "#2e2820",
"surfaceRaised": "#3a3228",
"text": "#ffffff",
"textMuted": "#c8c0b4",
"danger": "#ee5c5c",
"link": "#4a6a8a",
"border": "#4a4034"
}
};
+10 -4
View File
@@ -64,17 +64,22 @@ export default {
} }
.aside { .aside {
position: relative;
z-index: 5;
background: var(--ir-surface); background: var(--ir-surface);
overflow: hidden; overflow: visible !important;
padding: 0; padding: 0;
transition: width 0.2s ease; transition: width 0.2s ease;
.mode-sidebar { .sidebar-shell {
height: 100%; height: 100%;
min-height: 0;
} }
} }
.main-column { .main-column {
position: relative;
z-index: 1;
min-width: 0; min-width: 0;
} }
@@ -101,7 +106,7 @@ export default {
border: 1px solid var(--ir-border) !important; border: 1px solid var(--ir-border) !important;
} }
@media (max-width: 424px) { @media (max-width: 650px), ((orientation: landscape) and (max-height: 500px)) {
.app-shell { .app-shell {
flex-direction: column !important; flex-direction: column !important;
padding-bottom: 72px; padding-bottom: 72px;
@@ -117,7 +122,8 @@ export default {
} }
.main { .main {
padding: 1rem 1rem 2rem; /* Extra scroll room so Result can be brought to mid-screen above the toolbar */
padding-bottom: 72px;
} }
.mobile-toolbar { .mobile-toolbar {
+5 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="generator-actions"> <el-button-group class="generator-actions">
<el-button type="primary" native-type="submit" size="large"> <el-button type="primary" native-type="submit" size="large">
<el-icon class="btn-icon"><RefreshRight /></el-icon> <el-icon class="btn-icon"><RefreshRight /></el-icon>
{{ t('common.generate') }} {{ t('common.generate') }}
@@ -8,7 +8,7 @@
<el-icon class="btn-icon"><DocumentCopy /></el-icon> <el-icon class="btn-icon"><DocumentCopy /></el-icon>
{{ t('common.copy') }} {{ t('common.copy') }}
</el-button> </el-button>
</div> </el-button-group>
</template> </template>
<script> <script>
@@ -37,9 +37,9 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.generator-actions { .generator-actions {
display: flex; display: inline-flex;
flex-wrap: wrap; width: 100%;
gap: 0.75rem; max-width: 480px;
} }
.btn-icon { .btn-icon {
+110 -34
View File
@@ -1,29 +1,17 @@
<template> <template>
<nav class="mode-sidebar" :class="{ collapsed: isCollapsed && !isMobile }" :aria-label="t('nav.modes')"> <div class="sidebar-shell" :class="{ collapsed: isCollapsed && !isMobile }">
<nav class="mode-sidebar" :aria-label="t('nav.modes')">
<div v-if="showBrand" class="sidebar-top">
<a <a
v-if="showBrand"
class="brand" class="brand"
:href="config.brandUrl" :href="config.brandUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<img class="logo" :src="config.logoUrl" :alt="config.name" /> <img class="logo" :src="config.logoUrl" :alt="config.name" />
<span v-show="!isCollapsed || isMobile" class="brand-name">{{ config.name }}</span> <span class="brand-name">{{ config.name }}</span>
</a> </a>
</div>
<button
v-if="showBrand && !isMobile"
type="button"
class="collapse-btn"
:title="isCollapsed ? t('nav.expand') : t('nav.collapse')"
:aria-label="isCollapsed ? t('nav.expand') : t('nav.collapse')"
@click="toggleCollapse"
>
<el-icon :size="18">
<Fold v-if="!isCollapsed" />
<Expand v-else />
</el-icon>
</button>
<el-menu <el-menu
class="side-menu" class="side-menu"
@@ -67,6 +55,21 @@
</el-sub-menu> </el-sub-menu>
</el-menu> </el-menu>
</nav> </nav>
<button
v-if="showBrand && !isMobile"
type="button"
class="collapse-btn"
:title="isCollapsed ? t('nav.expand') : t('nav.collapse')"
:aria-label="isCollapsed ? t('nav.expand') : t('nav.collapse')"
@click="toggleCollapse"
>
<el-icon :size="16">
<Fold v-if="!isCollapsed" />
<Expand v-else />
</el-icon>
</button>
</div>
</template> </template>
<script> <script>
@@ -76,6 +79,8 @@ import { getAppConfig } from '@/config/appConfig'
import i18n, { setAppLocale } from '@/i18n' import i18n, { setAppLocale } from '@/i18n'
const COLLAPSE_KEY = 'irandom:sidebar-collapsed' const COLLAPSE_KEY = 'irandom:sidebar-collapsed'
/** Portrait phones + landscape short screens (phone sideways). */
const MOBILE_MQ = '(max-width: 650px), ((orientation: landscape) and (max-height: 500px))'
export default { export default {
name: 'ModeSidebar', name: 'ModeSidebar',
@@ -121,10 +126,12 @@ export default {
mounted() { mounted() {
this.updateMobile() this.updateMobile()
window.addEventListener('resize', this.updateMobile) window.addEventListener('resize', this.updateMobile)
window.addEventListener('orientationchange', this.updateMobile)
this.$emit('collapse-change', this.isCollapsed && !this.isMobile) this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener('resize', this.updateMobile) window.removeEventListener('resize', this.updateMobile)
window.removeEventListener('orientationchange', this.updateMobile)
}, },
methods: { methods: {
readCollapsed() { readCollapsed() {
@@ -147,7 +154,7 @@ export default {
this.$emit('collapse-change', this.isCollapsed && !this.isMobile) this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
}, },
updateMobile() { updateMobile() {
this.isMobile = window.matchMedia('(max-width: 424px)').matches this.isMobile = window.matchMedia(MOBILE_MQ).matches
this.$emit('collapse-change', this.isCollapsed && !this.isMobile) this.$emit('collapse-change', this.isCollapsed && !this.isMobile)
}, },
onMenuSelect(index) { onMenuSelect(index) {
@@ -168,23 +175,42 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.sidebar-shell {
position: relative;
height: 100%;
min-height: 0;
overflow: visible;
}
.mode-sidebar { .mode-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: 0.35rem;
height: 100%; height: 100%;
min-height: 0;
padding: 1rem 0.5rem; padding: 1rem 0.5rem;
background: var(--ir-surface); background: var(--ir-surface);
border-right: 1px solid var(--ir-border); border-right: 1px solid var(--ir-border);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.sidebar-top {
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0 0.15rem 0.65rem;
margin-bottom: 0.15rem;
border-bottom: 1px solid var(--ir-border);
} }
.brand { .brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.65rem; gap: 0.65rem;
padding: 0.35rem 0.5rem 0.85rem; min-width: 0;
margin-bottom: 0.15rem; flex: 1;
border-bottom: 1px solid var(--ir-border);
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
} }
@@ -204,6 +230,8 @@ export default {
color: var(--ir-primary); color: var(--ir-primary);
transition: color 0.15s ease; transition: color 0.15s ease;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.brand:hover .brand-name { .brand:hover .brand-name {
@@ -211,19 +239,32 @@ export default {
} }
.collapse-btn { .collapse-btn {
align-self: flex-end; position: absolute;
top: 1.15rem;
left: 100%;
right: auto;
z-index: 20;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 46px;
padding: 0;
transform: none;
appearance: none; appearance: none;
border: 1px solid transparent; border: 1px solid var(--ir-border);
border-radius: 8px; border-left: none;
background: transparent; border-radius: 0 18px 18px 0;
background: var(--ir-surface);
color: var(--ir-text-muted); color: var(--ir-text-muted);
padding: 0.45rem;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease, color 0.15s ease; box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.3);
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
&:hover { &:hover {
background: var(--ir-surface-raised); background: var(--ir-surface-raised);
color: var(--ir-text); color: var(--ir-primary);
border-color: var(--ir-primary);
} }
} }
@@ -231,7 +272,7 @@ export default {
border-right: none !important; border-right: none !important;
width: 100%; width: 100%;
background: transparent !important; background: transparent !important;
flex: 1; flex: 1 0 auto;
} }
.side-menu:not(.el-menu--collapse) { .side-menu:not(.el-menu--collapse) {
@@ -263,22 +304,52 @@ export default {
} }
.collapsed { .collapsed {
.brand { .mode-sidebar {
padding-left: 0.4rem;
padding-right: 0.4rem;
}
.sidebar-top {
justify-content: center; justify-content: center;
padding-left: 0.25rem; padding-left: 0;
padding-right: 0.25rem; padding-right: 0;
}
.brand {
display: flex;
justify-content: center;
align-items: center;
flex: none;
width: 100%;
overflow: visible;
}
.logo {
display: block;
width: 36px;
height: 36px;
}
.brand-name {
display: none;
} }
.collapse-btn { .collapse-btn {
align-self: center; top: 1.15rem;
height: 36px;
} }
} }
@media (max-width: 424px) { @media (max-width: 650px), ((orientation: landscape) and (max-height: 500px)) {
.sidebar-shell {
height: auto;
}
.mode-sidebar { .mode-sidebar {
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
height: auto; height: auto;
overflow: visible;
padding: 0.2rem 0.15rem calc(0.2rem + env(safe-area-inset-bottom, 0px)); padding: 0.2rem 0.15rem calc(0.2rem + env(safe-area-inset-bottom, 0px));
border-right: none; border-right: none;
border-top: 1px solid var(--ir-border); border-top: 1px solid var(--ir-border);
@@ -286,11 +357,16 @@ export default {
gap: 0; gap: 0;
} }
.sidebar-top {
display: none;
}
.side-menu { .side-menu {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
justify-content: space-around; justify-content: space-around;
flex: 1;
} }
:deep(.el-menu-item), :deep(.el-menu-item),
+10
View File
@@ -54,3 +54,13 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
::selection {
background: color-mix(in srgb, var(--ir-primary) 55%, transparent);
color: var(--ir-text);
}
::-moz-selection {
background: color-mix(in srgb, var(--ir-primary) 55%, transparent);
color: var(--ir-text);
}
+1
View File
@@ -72,6 +72,7 @@ export default defineConfig({
} }
}, },
server: { server: {
host: true,
port: 8080 port: 8080
} }
}) })