ligth theme design fix
This commit is contained in:
@@ -1,19 +1,31 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
/**
|
||||
* The two official Loyaly.ai assets — and the only place in the product where
|
||||
* The official Loyaly.ai assets — and the only place in the product where
|
||||
* brand colour is allowed to appear. Everything around them stays monochrome.
|
||||
*
|
||||
* Both render at their own intrinsic ratio: a caller sizes ONE axis and the
|
||||
* other is derived, so neither mark can be letterboxed or stretched. There is
|
||||
* Every mark renders at its own intrinsic ratio: a caller sizes ONE axis and
|
||||
* the other is derived, so nothing can be letterboxed or stretched. There is
|
||||
* deliberately no `fill` / `objectFit` escape hatch — that is how logos end up
|
||||
* distorted. Recolouring is likewise not a prop: the PNGs carry the mark.
|
||||
*
|
||||
* ── Two lockups, one component ────────────────────────────────────────────
|
||||
* The wordmark is baked into the PNG, so a white one is invisible on a white
|
||||
* sidebar — which is exactly what the light theme was shipping. Both files are
|
||||
* rendered and the cascade picks one (see `.brand-lockup-*` in globals.css);
|
||||
* the hidden variant is `display: none`, so it leaves the accessibility tree
|
||||
* and the alt text is announced exactly once.
|
||||
*
|
||||
* Resolving this in React instead was rejected: the mode may be the literal
|
||||
* 'system', which only CSS can resolve, and a client-side resolve would flash
|
||||
* the wrong mark on first paint.
|
||||
*/
|
||||
|
||||
// Intrinsic pixel dimensions of the shipped files, alpha-trimmed so the
|
||||
// wordmark optically centres (the supplied original carried 44px of dead
|
||||
// transparent space on its trailing edge).
|
||||
const LOGO = {src: '/white-logo.png', width: 1489, height: 248};
|
||||
const LOGO_DARK = {src: '/white-logo.png', width: 1489, height: 248};
|
||||
const LOGO_LIGHT = {src: '/brand/loyaly-logo.png', width: 596, height: 119};
|
||||
const MARK = {src: '/brand/loyaly-mark.png', width: 285, height: 256};
|
||||
|
||||
interface BrandProps {
|
||||
@@ -23,23 +35,46 @@ interface BrandProps {
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
/** Full horizontal Loyaly.ai lockup. Sized by height; width follows. */
|
||||
/**
|
||||
* Full horizontal Loyaly.ai lockup. Sized by height; width follows.
|
||||
*
|
||||
* Both variants are laid out at the SAME derived width, so swapping themes
|
||||
* cannot shift the sidebar, the top bar or the AI panel header by a pixel.
|
||||
*/
|
||||
export function BrandLogo({
|
||||
height = 28,
|
||||
alt = 'Loyaly.ai',
|
||||
priority = false,
|
||||
}: BrandProps & {height?: number}) {
|
||||
// Both files carry the same lockup at the same aspect ratio; deriving the
|
||||
// width from each one's own intrinsics keeps that true even if a future
|
||||
// asset is re-exported at a different scale.
|
||||
const darkWidth = Math.round((height * LOGO_DARK.width) / LOGO_DARK.height);
|
||||
const lightWidth = Math.round((height * LOGO_LIGHT.width) / LOGO_LIGHT.height);
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={LOGO.src}
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={Math.round((height * LOGO.width) / LOGO.height)}
|
||||
priority={priority}
|
||||
// Block display keeps the anchor that usually wraps this from painting a
|
||||
// hover underline in the leftover line box.
|
||||
className="block"
|
||||
/>
|
||||
<>
|
||||
{/* Dark wordmark on the light theme. The `block` display that used to
|
||||
live here — keeping the wrapping anchor from painting a hover
|
||||
underline in the leftover line box — is now set by the same rule
|
||||
that decides visibility. */}
|
||||
<Image
|
||||
src={LOGO_LIGHT.src}
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={lightWidth}
|
||||
priority={priority}
|
||||
className="brand-lockup-light"
|
||||
/>
|
||||
<Image
|
||||
src={LOGO_DARK.src}
|
||||
alt={alt}
|
||||
height={height}
|
||||
width={darkWidth}
|
||||
priority={priority}
|
||||
className="brand-lockup-dark"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ const TONE = {
|
||||
/**
|
||||
* Areas fade to transparent rather than sitting as flat gray blocks — on a
|
||||
* near-black surface a solid fill swallows the gridlines and the series below.
|
||||
*
|
||||
* The ramp is 0.20 → 0.02, down from 0.38 → 0.03. That original pair was tuned
|
||||
* against the monochrome ramp, where the top stop is a mid-gray and 38% of it
|
||||
* is a whisper. Footfall now draws in brand purple, and 38% of a saturated hue
|
||||
* on a white card is a colour block — it read as the loudest element on the
|
||||
* page while the line that carries the actual shape read as its edge. Lower
|
||||
* stops put the line back in charge and leave the gridlines visible through
|
||||
* the fill, which is the whole reason the fill is a gradient.
|
||||
*/
|
||||
export function AreaChartView<T extends object>({
|
||||
data,
|
||||
@@ -46,8 +54,8 @@ export function AreaChartView<T extends object>({
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor={c} stopOpacity={0.38} />
|
||||
<stop offset="100%" stopColor={c} stopOpacity={0.03} />
|
||||
<stop offset="0%" stopColor={c} stopOpacity={0.2} />
|
||||
<stop offset="100%" stopColor={c} stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -37,6 +37,20 @@ export const CHART = {
|
||||
positive: 'var(--color-success)',
|
||||
negative: 'var(--color-error)',
|
||||
attention: 'var(--color-warning)',
|
||||
/**
|
||||
* The two brand accents, for the small number of marks that are allowed to
|
||||
* carry the brand rather than merely a value: the two headline dashboard
|
||||
* charts, the reward series, the journey bars. Opt in per series with
|
||||
* `series={[{key, label, color: CHART.brand.cool}]}` — never as a default,
|
||||
* or the ramp stops being monochrome and the sparklines in every KPI card
|
||||
* turn purple with it. Defined in globals.css, not the Astryx theme.
|
||||
*/
|
||||
brand: {
|
||||
warm: 'var(--color-brand-warm)',
|
||||
/** Softened warm, for large filled areas — bars. See the token's note. */
|
||||
warmBar: 'var(--color-brand-warm-bar)',
|
||||
cool: 'var(--color-brand-cool)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** The non-chromatic channel. */
|
||||
|
||||
37
src/shared/utils/accent.ts
Normal file
37
src/shared/utils/accent.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* The two-accent brand system, expressed as class names.
|
||||
*
|
||||
* There are exactly two non-semantic hues in this product — warm (Loyaly
|
||||
* yellow) and cool (purple) — and their values live in globals.css as
|
||||
* `--color-brand-*`. This file is the only place that turns one into markup,
|
||||
* so "which utility do I use for a warm icon?" has a single answer and a
|
||||
* feature file never writes `text-brand-warm-ink` by hand.
|
||||
*
|
||||
* Assignment is a property of the DATA, not of the position in a grid. Each
|
||||
* activity carries its own accent from the API, so the same activity is the
|
||||
* same colour on the dashboard, on the activity page and inside a campaign
|
||||
* row — and a grid that gains a row does not repaint itself.
|
||||
*
|
||||
* `ink` is the readable slot (icons, text); `soft` is a tint for an icon chip
|
||||
* only. Nothing here paints a card background: the cards stay white.
|
||||
*/
|
||||
|
||||
export type BrandAccent = 'warm' | 'cool';
|
||||
|
||||
export const ACCENT: Record<
|
||||
BrandAccent,
|
||||
{ink: string; soft: string; solid: string; chip: string}
|
||||
> = {
|
||||
warm: {
|
||||
ink: 'text-brand-warm-ink',
|
||||
soft: 'bg-brand-warm-soft',
|
||||
solid: 'bg-brand-warm',
|
||||
chip: 'bg-brand-warm-soft text-brand-warm-ink',
|
||||
},
|
||||
cool: {
|
||||
ink: 'text-brand-cool-ink',
|
||||
soft: 'bg-brand-cool-soft',
|
||||
solid: 'bg-brand-cool',
|
||||
chip: 'bg-brand-cool-soft text-brand-cool-ink',
|
||||
},
|
||||
};
|
||||
@@ -11,11 +11,14 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ArrowRight,
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
BookOpen,
|
||||
Building2,
|
||||
CalendarClock,
|
||||
CalendarDays,
|
||||
Camera,
|
||||
ChartLine,
|
||||
ChevronsUpDown,
|
||||
CircleAlert,
|
||||
@@ -25,6 +28,8 @@ import {
|
||||
Coins,
|
||||
Columns3,
|
||||
CreditCard,
|
||||
Disc3,
|
||||
DoorOpen,
|
||||
Download,
|
||||
Expand,
|
||||
Footprints,
|
||||
@@ -35,9 +40,11 @@ import {
|
||||
Info,
|
||||
Keyboard,
|
||||
LayoutDashboard,
|
||||
Lightbulb,
|
||||
Lock,
|
||||
LogOut,
|
||||
Maximize2,
|
||||
Megaphone,
|
||||
Menu,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
@@ -50,21 +57,26 @@ import {
|
||||
Plus,
|
||||
Percent,
|
||||
Plug,
|
||||
Route,
|
||||
Send,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
Shrink,
|
||||
ShoppingBag,
|
||||
ShoppingCart,
|
||||
Sliders,
|
||||
Square,
|
||||
SquarePen,
|
||||
Sparkles,
|
||||
Store,
|
||||
Target,
|
||||
Ticket,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Trophy,
|
||||
User,
|
||||
UserCheck,
|
||||
UserPlus,
|
||||
Users,
|
||||
UserX,
|
||||
} from 'lucide-react';
|
||||
@@ -100,6 +112,30 @@ export const ICONS = {
|
||||
conversion: Percent,
|
||||
lyt: Coins,
|
||||
|
||||
// Customer activities.
|
||||
//
|
||||
// One glyph per activity in the loyalty ecosystem. Named with an `activity`
|
||||
// prefix rather than folded into the metric block above because several
|
||||
// collide with an existing concept and must not be merged with it: a `visit`
|
||||
// is a customer walking in, which is not the `visitors` KPI; `activityShop`
|
||||
// is the shop ACTIVITY, while `purchases` is the transaction metric.
|
||||
activityWalk: Footprints,
|
||||
activityVisit: DoorOpen,
|
||||
activitySelfie: Camera,
|
||||
activitySpin: Disc3,
|
||||
activityScratch: Ticket,
|
||||
activityBrand: Megaphone,
|
||||
activityChallenge: Target,
|
||||
activityFriend: UserPlus,
|
||||
activityShop: ShoppingBag,
|
||||
activityEvent: CalendarDays,
|
||||
|
||||
// Store intelligence
|
||||
journey: Route,
|
||||
campaign: Megaphone,
|
||||
insight: Lightbulb,
|
||||
arrowRight: ArrowRight,
|
||||
|
||||
// Dashboard controls
|
||||
compare: Columns3,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user