Initial commit: Hedena Gold monorepo.

First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
s.esmaeili
2026-10-15 17:19:10 +03:30
commit ea86636027
1121 changed files with 401065 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
// 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; 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',
'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: '#14161A',
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: 'hedena-gold-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}%)`)
})
},
}
@@ -0,0 +1,31 @@
{
"//": "Dark-mode values for the theme tokens in appearance.ts; missing keys keep their light value. Built as elevation layers so surfaces separate without shadows: page (deepest) < canvas (cards, sheets, bars) < hairlineSoft (panels, chips) < hairline (borders). v1: @primary (brand green) is both a fill under white text and a text accent, so it lifts to a mid green that works as both.",
"page": "#15171A",
"canvasSoft": "#15171A",
"canvas": "#1F2125",
"hairlineSoft": "#2A2D32",
"hairline": "#3A3D44",
"hairlineStrong": "#4B4F57",
"ink": "#F2F3F5",
"inkSoft": "#E1E3E7",
"inkMuted": "#B2B7C0",
"inkFaint": "#8F95A0",
"wine": "#C9566F",
"wineDeep": "#A8435C",
"wineSoft": "#DC8197",
"wineWash": "#3A222B",
"primary": "#33836A",
"primaryLighten": "#3F9478",
"green": "#45C677",
"red": "#F26161",
"info": "#7C9DE6",
"light": "#1F2125",
"grey": "#9298A2",
"lightGray": "#757A84",
"lightGray1": "#4A4E56",
"lightGray2": "#3A3D44",
"onInk": "#15171A",
"heroSurface": "#2C2F36",
"photoMat": "#FFFFFF",
"photoStage": "#F2F3F5"
}
+38
View File
@@ -0,0 +1,38 @@
{
"white": "#fff",
"black": "#000",
"primary": "#23493c",
"primaryLighten": "#2E5E4E",
"secondary": "#8D6A46",
"light": "#f9f9f9",
"grey": "#898989",
"danger": "#E31A1A",
"lightGray": "#A5A5A5",
"lightGray1": "#CACED8",
"lightGray2": "#E8EAEE",
"notify1": "#F4C57A",
"notify2": "#916F48",
"transparent": "transparent",
"current": "currentColor",
"info": "#335CB1",
"gray-50": "#f9fafb",
"gray-100": "#f3f4f6",
"gray-200": "#e5e7eb",
"gray-300": "#d1d5db",
"gray-400": "#9ca3af",
"gray-500": "#6b7280",
"green": "#16a34a",
"red": "#dc2626",
"ink": "#14161A",
"inkSoft": "#22262E",
"inkMuted": "#5B616E",
"inkFaint": "#8A909C",
"wine": "#7A1B33",
"wineDeep": "#5E1427",
"wineSoft": "#A33C55",
"wineWash": "#FBF2F4",
"canvas": "#FFFFFF",
"canvasSoft": "#FAFAFA",
"hairline": "#ECEDEF",
"hairlineSoft": "#F4F5F6"
}
+53
View File
@@ -0,0 +1,53 @@
// Ported from legacy henza_pwa/themeConfig/themeConfig.ts — styling values only.
// (legacy also defined baseURL/coreURL for its own Laravel API here; those are
// out of scope for a presentation-only port and superseded by this app's own
// src/api/client.ts, which already talks to the new NestJS API.)
import data from './colors.json'
import { themeLessVarOverrides } from './appearance'
const colors = data
const curve = {
none: 0,
xs: 2,
sm: 4,
font: 14,
px: 'px',
}
const vars = {
prefix: 'hedena',
// headers
'font-size': curve.font + curve.px,
'h1-font-size': curve.font * 2 + curve.px,
'h2-font-size': curve.font * 1.74 + curve.px,
'h3-font-size': curve.font * 1.51 + curve.px,
'h4-font-size': curve.font * 1.32 + curve.px,
'h5-font-size': curve.font * 1.2 + curve.px,
'h6-font-size': curve.font * 1.1 + curve.px,
// headers
//ant
'primary-color': colors.secondary,
'link-color': colors.primary,
'heading-color': colors.primary,
'error-color': colors.danger,
'border-radius-base': 5 + curve.px,
'input-padding-horizontal-base': 10 + curve.px,
'input-padding-vertical-base': 10 + curve.px,
'height-lg': '42px',
'height-base': '32px',
'checkbox-size': '20px',
'checkbox-border-radius': '3px',
//ant
'transition-duration': '.5s',
'transition-delay': '.5s',
}
// Theme tokens (see appearance.ts) are overridden last, so @canvas, @ink, …
// compile to var(--c-…) and switch with the theme at runtime. The antd keys
// above were already resolved to hex from `colors`, so antd still gets real
// colours for its colour functions; only its surface backgrounds follow the
// theme.
export const $themeLessVars = Object.assign(vars, colors, themeLessVarOverrides, {
'component-background': 'var(--c-canvas)',
'body-background': 'var(--c-page)',
'popover-background': 'var(--c-canvas)',
})