Files
zeekay 4620297b84 chore(layout): rename code/ → pkgs/ + apps/ + templates/ for cross-repo consistency
Match the layout convention used elsewhere in the Hanzo / Liquidity stack
(pkgs/* for libraries, apps/* for consumer apps, templates/* for starters).

Layout changes:
  pkgs/    ← code/{compiler,core,packages,ui,ui-admin}/
  apps/    ← code/{admin-base,admin-tasks,demos,gui.hanzo.ai,
              kitchen-sink*,sandbox,tests}/
  templates/  ← code/starters/* (flattened — no more starters/ subdir)

Updated:
- package.json workspaces glob.
- All scripts/* path references (postinstall, format, repair, release,
  sync-gui-versions, ci-serve, check-references, etc).
- ../../tasks/scripts/sync-admin-ui.sh — DEFAULT_SRC now points at
  ../gui/apps/admin-tasks (and scrubbed Tamagui mentions in comments).
- ~/work/hanzo/HANZO_BINARY.md doctrine doc.
- ~/work/hanzo/ui/pkg/ui/BASES.md framework-bases doc.
- gui CLAUDE.md, CONTRIBUTING.md.
- Memory entries.

Side-effect fix: ui-admin/src/primitives/index.ts now re-exports
PageShell from ../shell/PageShell. Several IAM identity pages
(GroupEdit, OrgEdit, GroupTree, UserEdit, RoleEdit) imported PageShell
from ./primitives — pre-existing barrel mismatch the test suite missed
because the smoke tests don't full-render. The rename surfaced it via
production build; back-compat re-export keeps both barrels working.

Verification:
- bun install: clean.
- ui-admin tests: 127/127 green.
- admin-tasks vite build: 721 KB JS / 213 KB gzip + 10 KB CSS, clean.
2026-04-27 14:33:21 -07:00

216 lines
6.8 KiB
TypeScript

import { describe, expect, test, assertType } from 'vitest'
import {
createV5Theme,
themes as defaultThemes,
defaultChildrenThemes,
} from '../src/v5-themes'
import { opacify, interpolateColor } from '../src/opacify'
describe('opacify', () => {
test('converts hex to hex with alpha', () => {
expect(opacify('#ff0000', 0.5)).toBe('#ff000080')
expect(opacify('#fff', 0.25)).toBe('#ffffff40') // shorthand
})
test('converts hsl to hsla', () => {
expect(opacify('hsl(0, 100%, 50%)', 0.5)).toBe('hsla(0, 100%, 50%, 0.5)')
})
test('returns unsupported formats unchanged', () => {
expect(opacify('rgb(255, 0, 0)', 0.5)).toBe('rgb(255, 0, 0)')
expect(opacify('invalid', 0.5)).toBe('invalid')
})
})
describe('interpolateColor', () => {
test('interpolates between colors', () => {
expect(interpolateColor('#000000', '#ffffff', 0)).toBe('rgb(0, 0, 0)')
expect(interpolateColor('#000000', '#ffffff', 1)).toBe('rgb(255, 255, 255)')
expect(interpolateColor('#000000', '#ffffff', 0.5)).toBe('rgb(128, 128, 128)')
})
test('handles different color formats', () => {
expect(interpolateColor('rgb(0, 0, 0)', 'rgb(255, 255, 255)', 0.5)).toBe(
'rgb(128, 128, 128)'
)
expect(interpolateColor('hsl(0, 100%, 50%)', 'hsl(0, 100%, 100%)', 0.5)).toMatch(
/^rgb/
)
})
test('returns first color on invalid input', () => {
expect(interpolateColor('#ff0000', 'invalid', 0.5)).toBe('#ff0000')
})
})
describe('v5 themes', () => {
test('generates expected theme structure', () => {
const keys = Object.keys(defaultThemes)
// Base + accent
expect(keys).toContain('light')
expect(keys).toContain('dark')
expect(keys).toContain('light_accent')
// Children themes
expect(keys).toContain('light_blue')
expect(keys).toContain('dark_red')
})
test('light and dark themes have different values', () => {
expect(defaultThemes.light.background).not.toBe(defaultThemes.dark.background)
expect(defaultThemes.light.color).not.toBe(defaultThemes.dark.color)
})
test('accent themes are inverted', () => {
expect(defaultThemes.light.background).not.toBe(defaultThemes.light_accent.background)
expect(defaultThemes.dark.background).not.toBe(defaultThemes.dark_accent.background)
})
test('getTheme computes opacity and interpolation colors', () => {
// Opacity variants
expect(defaultThemes.light.color01).toBeDefined()
expect(defaultThemes.light.background01).toBeDefined()
expect(defaultThemes.light.background08).toBeDefined()
expect(defaultThemes.light.outlineColor).toBeDefined()
// Works on all theme types
expect(defaultThemes.light_blue.color01).toBeDefined()
})
test('children colors spread to base theme extras', () => {
expect(defaultThemes.light.blue1).toBe(defaultChildrenThemes.blue.light.blue1)
expect(defaultThemes.light.blue12).toBe(defaultChildrenThemes.blue.light.blue12)
expect(defaultThemes.dark.blue1).toBe(defaultChildrenThemes.blue.dark.blue1)
})
test('includes shadow and whiteBlack colors', () => {
expect(defaultThemes.light.shadow1).toBeDefined()
expect(defaultThemes.light.shadow6).toBeDefined()
expect(defaultThemes.light.white).toBe('rgba(255,255,255,1)')
expect(defaultThemes.light.black).toBe('rgba(0,0,0,1)')
})
test('has color1-12 and accent1-12 tokens', () => {
expect(defaultThemes.light.color1).toBeDefined()
expect(defaultThemes.light.color12).toBeDefined()
expect(defaultThemes.light.accent1).toBeDefined()
expect(defaultThemes.light.accent12).toBeDefined()
})
})
describe('createV5Theme options', () => {
test('custom palettes affect black/white colors', () => {
const themes = createV5Theme({
darkPalette: [
'#111',
'#222',
'#333',
'#444',
'#555',
'#666',
'#777',
'#888',
'#999',
'#aaa',
'#bbb',
'#ccc',
],
})
expect(themes.light.black1).toBe('#111')
expect(themes.light.black12).toBe('#ccc')
})
test('componentThemes: false removes component themes', () => {
const themes = createV5Theme({ componentThemes: false })
const keys = Object.keys(themes)
expect(keys.filter((k) => k.includes('Button'))).toHaveLength(0)
})
test('empty childrenThemes produces minimal themes', () => {
const themes = createV5Theme({ childrenThemes: {} })
const keys = Object.keys(themes)
expect(keys).toContain('light')
expect(keys).toContain('light_black') // internal
expect(keys).not.toContain('light_blue')
})
test('custom childrenThemes adds color themes', () => {
const themes = createV5Theme({
childrenThemes: {
brand: {
light: { brand1: '#e0f7fa', brand12: '#00251a' },
dark: { brand1: '#00251a', brand12: '#e0f7fa' },
},
},
})
expect(themes.light.brand1).toBe('#e0f7fa')
expect(themes.dark.brand1).toBe('#00251a')
})
test('grandChildrenThemes override defaults', () => {
const themes = createV5Theme({
grandChildrenThemes: { custom: { template: 'surface1' } },
})
const keys = Object.keys(themes)
expect(keys).toContain('light_blue_custom')
expect(keys).not.toContain('light_blue_accent')
})
test('custom getTheme merges on top of built-in computed values', () => {
const themes = createV5Theme({
getTheme: ({ palette }) => ({
myOverlay: opacify(palette![7]!, 0.5),
outlineColor: '#123456',
}),
})
expect(themes.light.color01).toBeDefined()
expect(themes.light.myOverlay).toBeDefined()
expect(themes.light.outlineColor).toBe('#123456')
})
})
describe('defaultChildrenThemes', () => {
test('has expected structure', () => {
expect(Object.keys(defaultChildrenThemes.blue.light)).toHaveLength(12)
expect(defaultChildrenThemes.neutral.light).toEqual(
defaultChildrenThemes.neutral.dark
)
})
})
describe('type safety', () => {
test('theme values are string | number', () => {
assertType<string | number>(defaultThemes.light.background)
assertType<string | number>(defaultThemes.light.color01)
assertType<string | number>(defaultThemes.light.blue1)
})
test('custom themes infer types correctly', () => {
const themes = createV5Theme({
childrenThemes: {
brand: {
light: { brand1: '#fff', brand12: '#000' },
dark: { brand1: '#000', brand12: '#fff' },
},
},
})
// brand colors should be typed
assertType<string | number>(themes.light.brand1)
})
test('custom getTheme values infer types correctly', () => {
const themes = createV5Theme({
getTheme: ({ palette }) => ({
myOverlay: opacify(palette![7]!, 0.5),
}),
})
assertType<string | number>(themes.light.myOverlay)
assertType<string | number>(themes.dark.outlineColor)
// @ts-expect-error
themes.light.nonValidCustomKey
})
})