Files
tehran_taj/apps/user-app-v2/themeConfig/appearance.ts
s.esmaeiliandCursor afb068c643 feat: paginate customer orders and polish the dashboard hero
Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 13:11:42 +03:30

156 lines
6.0 KiB
TypeScript

// Runtime theming (light / dark) for the LESS design tokens.
//
// The app's colours are LESS variables (@canvas, @ink, @wine, …) injected by
// vite.config.ts through `modifyVars`. Before dark mode they compiled straight
// to hex, so nothing could change at runtime. Now the THEME tokens below are
// injected as `var(--c-<name>)` instead, and their values live in a generated
// stylesheet (`virtual:palette.css`, imported first in main.ts):
//
// :root { --c-canvas: #FFFFFF; --c-canvas-rgb: 255 255 255; … }
// html.theme-dark { --c-canvas: #16171A; --c-canvas-rgb: 22 23 26; … }
//
// Light values come from colors.json (unchanged, still the source of truth and
// still what Tailwind reads), dark values from colors.dark.json. A key missing
// from colors.dark.json simply keeps its light value in dark mode.
//
// Not converted, on purpose: `white`, `black`, `secondary`, `danger` and the
// antd keys in themeConfig.ts. ant-design-vue's own LESS compiles with the
// same modifyVars and runs colour functions (fade/tint/colorPalette) on those,
// which a `var()` would break.
//
// LESS colour functions can't operate on a `var()` either, so app code uses
// `alpha(@token, 40%)` instead of `fade(@token, 40%)` — registered below as a
// LESS plugin; for a theme token it emits `rgb(var(--c-token-rgb) / 40%)`, for
// a real colour it behaves exactly like `fade`.
//
// This file is kept identical in user-app and user-app-v2; only the JSONs
// differ per app.
import type { Plugin } from 'vite'
import light from './colors.json'
import dark from './colors.dark.json'
/** Tokens that change with the theme. Everything else in colors.json stays a
* plain compile-time hex value. */
export const THEME_TOKENS = [
'canvas',
'canvasSoft',
'ink',
'inkSoft',
'inkMuted',
'inkFaint',
'hairline',
'hairlineSoft',
'wine',
'wineDeep',
'wineSoft',
'wine200',
'wineWash',
'primary',
'primaryLighten',
'green',
'red',
'info',
'light',
'grey',
'lightGray',
'lightGray1',
'lightGray2',
] as const
/**
* Semantic tokens added for dark mode, for the places where one light-mode
* token did two jobs. Light values reproduce the old look exactly.
* - onInk: text/icons ON an @ink fill (filled buttons, chosen chips). White in
* light; in dark @ink turns light, so this turns dark.
* - heroSurface: decorative dark cards (rate strip, balance card…) that stay
* dark in both themes, so their white text needs no change.
* - hairlineStrong: a step darker than @hairline (was `darken(@hairline, 7%)`).
* - photoMat: behind product photos. Transparent in light (no change); in
* dark a light mat, so photos shot on white don't sit as white boxes on a
* dark card.
* - page: the screen's own ground, behind every card and panel. Same white
* as @canvas in light; in dark it is the deepest layer, so cards (@canvas)
* and panels (@hairlineSoft) stand off it a step at a time.
* - photoStage: the detail page's gallery backdrop — @hairlineSoft in light,
* the same light mat in dark.
*/
const SEMANTIC_LIGHT: Record<string, string> = {
page: (light as Record<string, string>).canvas,
onInk: '#FFFFFF',
heroSurface: '#7C0035',
hairlineStrong: '#DADCE0',
photoMat: 'transparent',
photoStage: (light as Record<string, string>).hairlineSoft,
}
type Palette = Record<string, string>
function rgbChannels(hex: string): string | null {
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim())
if (!m) return null
let h = m[1]
if (h.length === 3) h = h.split('').map((c) => c + c).join('')
const n = parseInt(h, 16)
return `${(n >> 16) & 255} ${(n >> 8) & 255} ${n & 255}`
}
function block(selector: string, values: Palette): string {
const lines: string[] = []
for (const [name, value] of Object.entries(values)) {
lines.push(` --c-${name}: ${value};`)
const rgb = rgbChannels(value)
if (rgb) lines.push(` --c-${name}-rgb: ${rgb};`)
}
return `${selector} {\n${lines.join('\n')}\n}\n`
}
const lightPalette: Palette = {
...Object.fromEntries(THEME_TOKENS.map((t) => [t, (light as Record<string, string>)[t]])),
...SEMANTIC_LIGHT,
}
const darkPalette: Palette = Object.fromEntries(
Object.entries(dark as Record<string, string>).filter(([k]) => !k.startsWith('//')),
)
export const paletteCss =
block(':root', lightPalette) +
// `color-scheme` lets native bits (inputs, scrollbars, autofill) follow.
block('html.theme-dark', darkPalette).replace('{\n', '{\n color-scheme: dark;\n')
/** LESS variables for every theme/semantic token: `@canvas` → `var(--c-canvas)`. */
export const themeLessVarOverrides: Record<string, string> = Object.fromEntries(
Object.keys(lightPalette).map((t) => [t, `var(--c-${t})`]),
)
/** Raw light hex values, for places that need a real colour at build time
* (PWA manifest). */
export const lightHex = light as Record<string, string>
/** Serves `virtual:palette.css`. */
export function palettePlugin(): Plugin {
const id = 'virtual:palette.css'
const resolved = '\0' + id
return {
name: 'gold-bank-palette',
resolveId: (source) => (source === id ? resolved : undefined),
load: (source) => (source === resolved ? paletteCss : undefined),
}
}
/** LESS plugin adding `alpha(color, amount)` — see the file header. */
export const lessAlphaPlugin = {
install(less: any, _pm: unknown, functions: any) {
functions.add('alpha', function (this: unknown, color: any, amount: any) {
if (color && color.type === 'Color') {
const fade = less.functions.functionRegistry.get('fade')
return fade.call(this, color, amount)
}
const css: string = typeof color?.toCSS === 'function' ? color.toCSS({}) : String(color?.value ?? '')
const m = /var\(\s*--c-([A-Za-z0-9]+)\s*\)/.exec(css)
if (!m) throw new Error(`alpha(): expected a colour or a theme token, got "${css}"`)
const pct = amount.unit && amount.unit.toString() === '%' ? amount.value : amount.value * 100
return new less.tree.Anonymous(`rgb(var(--c-${m[1]}-rgb) / ${pct}%)`)
})
},
}