@ui0/theme 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -2
- package/dist/backdrop.module.css +1 -0
- package/dist/backdrop.scss +7 -0
- package/dist/color.inject.module.css +7 -0
- package/dist/color.module.css +3 -0
- package/dist/color.scss +84 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +53 -0
- package/dist/components/style.css +1 -0
- package/dist/components/theme-provider.keys.d.ts +4 -0
- package/dist/components/theme-provider.keys.d.ts.map +1 -0
- package/dist/components/theme-provider.module.css +1 -0
- package/dist/components/theme-provider.types.d.ts +18 -0
- package/dist/components/theme-provider.types.d.ts.map +1 -0
- package/dist/components/theme-provider.vue.d.ts +19 -0
- package/dist/config.default.d.mts +4 -0
- package/dist/config.default.d.mts.map +1 -0
- package/dist/config.default.mjs +73 -0
- package/dist/config.default.mjs.map +1 -0
- package/dist/disabled.inject.module.css +5 -0
- package/dist/disabled.module.css +1 -0
- package/dist/disabled.scss +3 -0
- package/dist/display.module.css +2 -0
- package/dist/display.scss +63 -0
- package/dist/duration.scss +17 -0
- package/dist/elevation.module.css +33 -0
- package/dist/elevation.scss +46 -0
- package/dist/generate-BPYE5ALG.mjs +1037 -0
- package/dist/generate-BPYE5ALG.mjs.map +1 -0
- package/dist/generator/cli.d.mts +1 -0
- package/dist/generator/cli.mjs +103 -0
- package/dist/generator/cli.mjs.map +1 -0
- package/dist/generator/vite.d.mts +18 -0
- package/dist/generator/vite.d.mts.map +1 -0
- package/dist/generator/vite.mjs +117 -0
- package/dist/generator/vite.mjs.map +1 -0
- package/dist/index.d.mts +339 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +909 -0
- package/dist/index.mjs.map +1 -0
- package/dist/is.scss +185 -0
- package/dist/load-config-64q6nzFy.mjs +48 -0
- package/dist/load-config-64q6nzFy.mjs.map +1 -0
- package/dist/ripple.scss +57 -0
- package/dist/round.inject.module.css +92 -0
- package/dist/round.module.css +14 -0
- package/dist/round.scss +90 -0
- package/dist/size.inject.module.css +50 -0
- package/dist/size.module.css +13 -0
- package/dist/size.scss +28 -0
- package/dist/theme.scss +16 -0
- package/dist/typography.inject.module.css +81 -0
- package/dist/typography.module.css +32 -0
- package/dist/typography.scss +21 -0
- package/dist/util.scss +78 -0
- package/dist/vars.scss +0 -0
- package/package.json +96 -4
- package/src/contracts/backdrop/backdrop.contract.ts +44 -0
- package/src/contracts/backdrop/backdrop.generator.ts +82 -0
- package/src/contracts/backdrop/backdrop.keys.ts +7 -0
- package/src/contracts/backdrop/backdrop.module.css +3 -0
- package/src/contracts/backdrop/backdrop.scss +7 -0
- package/src/contracts/backdrop/backdrop.types.ts +45 -0
- package/src/contracts/backdrop/index.ts +21 -0
- package/src/contracts/color/color.contract.spec.ts +75 -0
- package/src/contracts/color/color.contract.ts +97 -0
- package/src/contracts/color/color.generator.spec.ts +73 -0
- package/src/contracts/color/color.generator.ts +122 -0
- package/src/contracts/color/color.gradient.spec.ts +32 -0
- package/src/contracts/color/color.gradient.ts +52 -0
- package/src/contracts/color/color.inject.module.css +5 -0
- package/src/contracts/color/color.keys.ts +7 -0
- package/src/contracts/color/color.module.css +5 -0
- package/src/contracts/color/color.scss +84 -0
- package/src/contracts/color/color.serialize.spec.ts +57 -0
- package/src/contracts/color/color.serialize.ts +35 -0
- package/src/contracts/color/color.token.spec.ts +57 -0
- package/src/contracts/color/color.token.ts +89 -0
- package/src/contracts/color/color.types.ts +82 -0
- package/src/contracts/color/index.md +87 -0
- package/src/contracts/color/index.ts +25 -0
- package/src/contracts/contract.ts +115 -0
- package/src/contracts/disabled/disabled.contract.ts +32 -0
- package/src/contracts/disabled/disabled.generator.ts +32 -0
- package/src/contracts/disabled/disabled.inject.module.css +5 -0
- package/src/contracts/disabled/disabled.keys.ts +6 -0
- package/src/contracts/disabled/disabled.module.css +3 -0
- package/src/contracts/disabled/disabled.scss +3 -0
- package/src/contracts/disabled/disabled.types.ts +19 -0
- package/src/contracts/disabled/index.ts +21 -0
- package/src/contracts/display/display.contract.ts +41 -0
- package/src/contracts/display/display.generator.ts +57 -0
- package/src/contracts/display/display.instance.ts +173 -0
- package/src/contracts/display/display.keys.ts +9 -0
- package/src/contracts/display/display.module.css +4 -0
- package/src/contracts/display/display.scss +63 -0
- package/src/contracts/display/display.types.ts +75 -0
- package/src/contracts/display/index.ts +27 -0
- package/src/contracts/elevation/elevation.contract.ts +37 -0
- package/src/contracts/elevation/elevation.generator.ts +71 -0
- package/src/contracts/elevation/elevation.keys.ts +6 -0
- package/src/contracts/elevation/elevation.module.css +35 -0
- package/src/contracts/elevation/elevation.scss +46 -0
- package/src/contracts/elevation/elevation.types.ts +21 -0
- package/src/contracts/elevation/index.ts +20 -0
- package/src/contracts/generator.ts +38 -0
- package/src/contracts/index.md +48 -0
- package/src/contracts/index.ts +37 -0
- package/src/contracts/round/index.ts +21 -0
- package/src/contracts/round/round.contract.ts +75 -0
- package/src/contracts/round/round.generator.ts +33 -0
- package/src/contracts/round/round.inject.module.scss +36 -0
- package/src/contracts/round/round.keys.ts +7 -0
- package/src/contracts/round/round.module.css +16 -0
- package/src/contracts/round/round.scss +90 -0
- package/src/contracts/round/round.types.ts +44 -0
- package/src/contracts/size/index.ts +22 -0
- package/src/contracts/size/size.contract.ts +98 -0
- package/src/contracts/size/size.generator.spec.ts +101 -0
- package/src/contracts/size/size.generator.ts +51 -0
- package/src/contracts/size/size.inject.module.scss +12 -0
- package/src/contracts/size/size.keys.ts +8 -0
- package/src/contracts/size/size.module.css +15 -0
- package/src/contracts/size/size.scss +28 -0
- package/src/contracts/size/size.types.ts +63 -0
- package/src/contracts/size/units.scss +17 -0
- package/src/contracts/style/index.ts +11 -0
- package/src/contracts/style/style.generator.ts +36 -0
- package/src/contracts/style/style.key.ts +4 -0
- package/src/contracts/style/style.types.ts +16 -0
- package/src/contracts/typography/index.ts +26 -0
- package/src/contracts/typography/typography.contract.ts +52 -0
- package/src/contracts/typography/typography.generator.spec.ts +48 -0
- package/src/contracts/typography/typography.generator.ts +44 -0
- package/src/contracts/typography/typography.inject.module.css +54 -0
- package/src/contracts/typography/typography.keys.ts +12 -0
- package/src/contracts/typography/typography.module.css +36 -0
- package/src/contracts/typography/typography.scss +21 -0
- package/src/contracts/typography/typography.types.ts +90 -0
- package/src/generator/theme.default.css +657 -0
- package/src/styles/duration.scss +17 -0
- package/src/styles/index.md +18 -0
- package/src/styles/is.scss +185 -0
- package/src/styles/ripple.scss +57 -0
- package/src/styles/theme.scss +16 -0
- package/src/styles/util.scss +78 -0
- package/src/styles/vars.scss +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MaybeAngle, MaybePercents, MaybeUnits, Numberish } from '@ui0/types'
|
|
2
|
+
|
|
3
|
+
/** the backdrop-filter functions the backdrop prop selects from */
|
|
4
|
+
export type BackdropFunction =
|
|
5
|
+
| 'blur'
|
|
6
|
+
| 'brightness'
|
|
7
|
+
| 'contrast'
|
|
8
|
+
| 'drop-shadow'
|
|
9
|
+
| 'grayscale'
|
|
10
|
+
| 'hue-rotate'
|
|
11
|
+
| 'invert'
|
|
12
|
+
| 'opacity'
|
|
13
|
+
| 'saturate'
|
|
14
|
+
| 'sepia'
|
|
15
|
+
|
|
16
|
+
export interface BackdropProps {
|
|
17
|
+
backdrop?: BackdropFunction | `${BackdropFunction}@${Numberish}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** per-function default values the generator turns into setter classes */
|
|
21
|
+
export interface BackdropThemeAppearanceConfig {
|
|
22
|
+
blur?: MaybeUnits
|
|
23
|
+
brightness?: MaybePercents
|
|
24
|
+
contrast?: MaybePercents
|
|
25
|
+
'drop-shadow'?: string
|
|
26
|
+
grayscale?: MaybePercents
|
|
27
|
+
'hue-rotate'?: MaybeAngle
|
|
28
|
+
invert?: MaybePercents
|
|
29
|
+
opacity?: MaybePercents
|
|
30
|
+
saturate?: MaybePercents
|
|
31
|
+
sepia?: MaybePercents
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// registration: the contract announces itself to the package registries
|
|
35
|
+
// (interface merging - no central import of the contracts anywhere)
|
|
36
|
+
declare module '@ui0/types/theme' {
|
|
37
|
+
interface Ui0ThemePresetAppearanceConfig {
|
|
38
|
+
backdrop?: BackdropThemeAppearanceConfig
|
|
39
|
+
}
|
|
40
|
+
interface Ui0ThemeContracts extends BackdropProps {}
|
|
41
|
+
interface Ui0ThemeContractSettings {
|
|
42
|
+
/** `false` disables the contract entirely; otherwise per-function toggles */
|
|
43
|
+
backdrop?: false | Partial<Record<BackdropFunction, boolean>>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ThemeContract } from '@ui0/types'
|
|
2
|
+
|
|
3
|
+
import { ThemeContractFlags } from '../../constants'
|
|
4
|
+
import { backdropDeal } from './backdrop.contract'
|
|
5
|
+
import { BackdropContractKey, BackdropRefKey } from './backdrop.keys'
|
|
6
|
+
import type { BackdropProps } from './backdrop.types'
|
|
7
|
+
|
|
8
|
+
export { default as $style } from './backdrop.module.css'
|
|
9
|
+
export type * from './backdrop.types'
|
|
10
|
+
export * from './backdrop.keys'
|
|
11
|
+
|
|
12
|
+
export const BackdropPropsKeys: ReadonlyArray<keyof BackdropProps> = ['backdrop']
|
|
13
|
+
|
|
14
|
+
export const backdrop: ThemeContract = {
|
|
15
|
+
name: 'backdrop',
|
|
16
|
+
key: BackdropContractKey,
|
|
17
|
+
ref: BackdropRefKey,
|
|
18
|
+
deal: backdropDeal,
|
|
19
|
+
flag: ThemeContractFlags.dealer + ThemeContractFlags.generator,
|
|
20
|
+
props: BackdropPropsKeys
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { hashClassName } from '../../generator/hash'
|
|
4
|
+
import { ThemeContractInstance } from '../contract'
|
|
5
|
+
import { color } from './index'
|
|
6
|
+
|
|
7
|
+
function setup(props: Record<string, unknown>) {
|
|
8
|
+
const provider = new ThemeContractInstance('test', props, {})
|
|
9
|
+
provider.add(color)
|
|
10
|
+
return provider
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('colorDeal - v2', () => {
|
|
14
|
+
it('delivers opacity as inline source vars, swatches as classes', () => {
|
|
15
|
+
const provider = setup({ color: 'error@40', fill: 'black@50' })
|
|
16
|
+
const classes = provider.classes()
|
|
17
|
+
expect(classes).toContain(hashClassName('c-error'))
|
|
18
|
+
expect(classes).toContain(hashClassName('f-black'))
|
|
19
|
+
expect(classes).not.toContain(hashClassName('co-40'))
|
|
20
|
+
const styles = provider.styles()
|
|
21
|
+
expect(styles['--0-c-src']).toBe('0.4')
|
|
22
|
+
expect(styles['--0-f-src']).toBe('0.5')
|
|
23
|
+
expect(classes).toContain(hashClassName('colored'))
|
|
24
|
+
expect(classes).toContain(hashClassName('filled'))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('maps a positional pair onto scheme-gated classes + scheme source vars', () => {
|
|
28
|
+
const provider = setup({ color: 'white/success@50' })
|
|
29
|
+
const classes = provider.classes()
|
|
30
|
+
expect(classes).toContain(hashClassName('light-c-white'))
|
|
31
|
+
expect(classes).toContain(hashClassName('dark-c-success'))
|
|
32
|
+
expect(provider.styles()['--0-c-src-d']).toBe('0.5')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('maps pseudo overrides from the same prop', () => {
|
|
36
|
+
const provider = setup({ color: 'success hover:warning@90' })
|
|
37
|
+
const classes = provider.classes()
|
|
38
|
+
expect(classes).toContain(hashClassName('c-success'))
|
|
39
|
+
expect(classes).toContain(hashClassName('hover-c-warning'))
|
|
40
|
+
expect(provider.styles()['--0-c-src-hover']).toBe('0.9')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('maps a scheme+pseudo combo onto the combined source var', () => {
|
|
44
|
+
const provider = setup({ color: 'dark:focus:warning@60' })
|
|
45
|
+
expect(provider.classes()).toContain(hashClassName('dark-focus-c-warning'))
|
|
46
|
+
expect(provider.styles()['--0-c-src-d-focus']).toBe('0.6')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('resolves the gradient prop onto the --0-g-cur style', () => {
|
|
50
|
+
const provider = setup({ gradient: 'to-right error->success@80' })
|
|
51
|
+
const stylesFirst = provider.styles()
|
|
52
|
+
const classes = provider.classes()
|
|
53
|
+
expect(stylesFirst['--0-g-cur']).toBe(
|
|
54
|
+
'linear-gradient(to right, rgb(var(--0-c-error-v) / 1), rgb(var(--0-c-success-v) / 0.8))'
|
|
55
|
+
)
|
|
56
|
+
expect(classes).toContain(hashClassName('gradiented'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('serves classes and styles from one compute, either call first', () => {
|
|
60
|
+
const provider = setup({ color: 'error' })
|
|
61
|
+
expect(provider.styles()).toEqual({})
|
|
62
|
+
expect(provider.classes()).toContain(hashClassName('c-error'))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('recomputes when the prop or the inherit context changes', () => {
|
|
66
|
+
const props = { color: 'inherit' as any }
|
|
67
|
+
const provider = setup(props)
|
|
68
|
+
const context = { value: 'highlight' }
|
|
69
|
+
provider.setContext({ color: context as any })
|
|
70
|
+
|
|
71
|
+
expect(provider.classes()).toContain(hashClassName('c-highlight'))
|
|
72
|
+
context.value = 'error'
|
|
73
|
+
expect(provider.classes()).toContain(hashClassName('c-error'))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The color contract's processing endpoint (v2): resolves the color/fill
|
|
2
|
+
// props onto hashed setter classes (via the token parser + serializer) and
|
|
3
|
+
// the gradient prop onto the `--0-g-cur` inline style. One parse pass per
|
|
4
|
+
// prop change, shared by classes() and styles().
|
|
5
|
+
import type { ClassList, StyleList } from '@ui0/types'
|
|
6
|
+
import type { ContractProvider, ThemeContractDeal } from '@ui0/types'
|
|
7
|
+
import { isString, mergeCssModulesStyles } from '@ui0/util'
|
|
8
|
+
|
|
9
|
+
import { gradientStyle } from './color.gradient'
|
|
10
|
+
import { serializeTokens } from './color.serialize'
|
|
11
|
+
import { parseColorProp } from './color.token'
|
|
12
|
+
import type { ColorProps, FillProps, GradientProps } from './color.types'
|
|
13
|
+
|
|
14
|
+
import $color from './color.module.css'
|
|
15
|
+
|
|
16
|
+
type Painted = ColorProps & FillProps & GradientProps
|
|
17
|
+
|
|
18
|
+
export function colorDeal(instance: ContractProvider): ThemeContractDeal {
|
|
19
|
+
const props = instance.props
|
|
20
|
+
|
|
21
|
+
// one parse pass per prop change, shared by classes() and styles() -
|
|
22
|
+
// either may be called first; the key also carries the inherited context
|
|
23
|
+
// values so an `inherit` prop follows a context change
|
|
24
|
+
let cache: { key: string; value: { classes: ClassList; styles: StyleList } } | undefined
|
|
25
|
+
|
|
26
|
+
const norm = (v: unknown) => (isString(v) ? v : Array.isArray(v) ? v.join(' ') : '')
|
|
27
|
+
|
|
28
|
+
const ensure = () => {
|
|
29
|
+
const key = [
|
|
30
|
+
norm(props.color),
|
|
31
|
+
norm(props.fill),
|
|
32
|
+
norm(props.gradient),
|
|
33
|
+
norm(instance.context?.color?.value),
|
|
34
|
+
norm(instance.context?.fill?.value)
|
|
35
|
+
].join('|')
|
|
36
|
+
if (cache && cache.key === key) return cache.value
|
|
37
|
+
const value = paint(resolveProps())
|
|
38
|
+
cache = { key, value }
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// the `inherit` passthrough: an `inherit` prop re-reads the provided
|
|
43
|
+
// context ref (a parent's prop), everything else resolves its own value
|
|
44
|
+
const resolveProps = (): Painted => {
|
|
45
|
+
const out: Record<string, any> = {}
|
|
46
|
+
const color = props.color
|
|
47
|
+
const fill = props.fill
|
|
48
|
+
|
|
49
|
+
if (isString(color) && color.split(' ')[0] === 'inherit') {
|
|
50
|
+
const v = instance.context?.color?.value
|
|
51
|
+
if (v) out['color'] = v
|
|
52
|
+
} else if (color) {
|
|
53
|
+
out['color'] = color
|
|
54
|
+
}
|
|
55
|
+
if (isString(fill) && fill.split(' ')[0] === 'inherit') {
|
|
56
|
+
const v = instance.context?.fill?.value
|
|
57
|
+
if (v) out['fill'] = v
|
|
58
|
+
} else if (fill) {
|
|
59
|
+
out['fill'] = fill
|
|
60
|
+
}
|
|
61
|
+
if (props.gradient) out['gradient'] = props.gradient
|
|
62
|
+
return out as Painted
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const paint = (p: Painted): { classes: ClassList; styles: StyleList } => {
|
|
66
|
+
const classes: ClassList = []
|
|
67
|
+
const styles: StyleList = {}
|
|
68
|
+
const $ = mergeCssModulesStyles($color, instance.$styles)
|
|
69
|
+
|
|
70
|
+
if (p.color) {
|
|
71
|
+
const out = serializeTokens(parseColorProp(p.color), 'c', $)
|
|
72
|
+
classes.push(...out.classes)
|
|
73
|
+
Object.assign(styles, out.styles)
|
|
74
|
+
classes.push($.colored)
|
|
75
|
+
}
|
|
76
|
+
if (p.fill) {
|
|
77
|
+
const out = serializeTokens(parseColorProp(p.fill), 'f', $)
|
|
78
|
+
classes.push(...out.classes)
|
|
79
|
+
Object.assign(styles, out.styles)
|
|
80
|
+
classes.push($.filled)
|
|
81
|
+
}
|
|
82
|
+
if (p.gradient) {
|
|
83
|
+
const g = gradientStyle(p.gradient)
|
|
84
|
+
if (g) {
|
|
85
|
+
styles['--0-g-cur'] = g
|
|
86
|
+
classes.push($.gradiented)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { classes, styles }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
classes: () =>
|
|
94
|
+
props.color || props.fill || props.gradient ? instance.inject(ensure().classes) : instance.inject([]),
|
|
95
|
+
styles: () => instance.inject(ensure().styles)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ThemePresetName, Ui0ThemePresetAppearanceConfig } from '@ui0/types'
|
|
2
|
+
import { hashClassName as viteHashClassName } from '@ui0/vite/css-modules'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { hashClassName } from '../../generator/hash'
|
|
6
|
+
import type { ContractContext } from '../generator'
|
|
7
|
+
import createColorGenerator from './color.generator'
|
|
8
|
+
|
|
9
|
+
const ctx = (
|
|
10
|
+
config: Ui0ThemePresetAppearanceConfig = {},
|
|
11
|
+
settings: ContractContext['settings'] = {}
|
|
12
|
+
): ContractContext => ({
|
|
13
|
+
presetName: 'test' as ThemePresetName,
|
|
14
|
+
config,
|
|
15
|
+
settings,
|
|
16
|
+
hash: hashClassName
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('createColorGenerator - setters', () => {
|
|
20
|
+
it('emits swatch setters per slot and scheme gating, one line per rule', () => {
|
|
21
|
+
const css = createColorGenerator(ctx({ color: { error: '#f00' } })).css()
|
|
22
|
+
expect(css).toContain(`.${hashClassName('c-error')} { --0-c-cur-v: var(--0-c-error-v); }`)
|
|
23
|
+
expect(css).toContain(`.${hashClassName('f-error')} { --0-f-cur-v: var(--0-c-error-v); }`)
|
|
24
|
+
expect(css).toContain(`.theme_dark .${hashClassName('dark-c-error')} { --0-c-cur-v: var(--0-c-error-v); }`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('emits pseudo setters with pseudo-class selectors', () => {
|
|
28
|
+
const css = createColorGenerator(ctx({ color: { error: '#f00' } })).css()
|
|
29
|
+
expect(css).toContain(`.${hashClassName('hover-c-error')}:hover { --0-c-cur-v: var(--0-c-error-v); }`)
|
|
30
|
+
expect(css).toContain(
|
|
31
|
+
`.theme_dark .${hashClassName('dark-active-f-error')}:active { --0-f-cur-v: var(--0-c-error-v); }`
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('carries NO opacity rules - values ride element inline styles', () => {
|
|
36
|
+
const css = createColorGenerator(ctx({ color: { error: '#f00' } })).css()
|
|
37
|
+
expect(css).not.toContain('--0-c-cur-o:')
|
|
38
|
+
expect(css).not.toContain('--0-f-cur-o:')
|
|
39
|
+
expect(css).not.toContain('co-40')
|
|
40
|
+
const classes = createColorGenerator(ctx({ color: { error: '#f00' } })).class()
|
|
41
|
+
expect(Object.keys(classes).some((k) => k.includes('co-'))).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('registers the swatch grid in the class map, in distiller lockstep', () => {
|
|
45
|
+
const classes = createColorGenerator(ctx({ color: { error: '#f00' } })).class()
|
|
46
|
+
for (const name of [
|
|
47
|
+
'colored',
|
|
48
|
+
'filled',
|
|
49
|
+
'gradiented',
|
|
50
|
+
'c-error',
|
|
51
|
+
'light-c-error',
|
|
52
|
+
'dark-hover-c-error',
|
|
53
|
+
'focus-f-error'
|
|
54
|
+
]) {
|
|
55
|
+
expect(classes[name]).toBe(hashClassName(name))
|
|
56
|
+
expect(classes[name]).toBe(viteHashClassName(name))
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('emits the current slots from foreground/background', () => {
|
|
61
|
+
const vars = createColorGenerator(ctx({ color: { foreground: '#111', background: '#fff' } })).vars()
|
|
62
|
+
expect(vars.c['cur-v']).toBe('var(--0-c-foreground-v)')
|
|
63
|
+
expect(vars.f['cur-v']).toBe('var(--0-c-background-v)')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('emits only markers for an empty pool', () => {
|
|
67
|
+
const gen = createColorGenerator(ctx())
|
|
68
|
+
expect(gen.css()).toBe(
|
|
69
|
+
['colored', 'filled', 'gradiented'].map((n) => `.${hashClassName(n)} { /* slot marker */ }`).join('\n')
|
|
70
|
+
)
|
|
71
|
+
expect(Object.keys(gen.class())).toEqual(['colored', 'filled', 'gradiented'])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The color contract's generator endpoint (v2): turns the preset's color
|
|
2
|
+
// pool into the --0-c-* swatch vars + ramps and the swatch setter classes the
|
|
3
|
+
// runtime resolves against. Setter family:
|
|
4
|
+
// [scheme-][pseudo-]{c|f}-<name> re-points the current slot
|
|
5
|
+
// gating: ungated (both schemes / no pseudo), .theme_light, .theme_dark for
|
|
6
|
+
// schemes; :hover/:active/:focus pseudo-classes for pseudos. The full grid
|
|
7
|
+
// ships per pool name (the used-tokens resolver is planned to prune it).
|
|
8
|
+
// Opacity VALUES never appear here - they ride element inline styles and the
|
|
9
|
+
// color.use() scss bridges pick the active context (see color.scss). The
|
|
10
|
+
// slot marker classes are static - they ship in color.inject.module.css;
|
|
11
|
+
// class() re-exports the registry map plus the config-generated grid.
|
|
12
|
+
import type { CSSModuleClassList, RgbTuple, VarList } from '@ui0/types'
|
|
13
|
+
import type { ThemeContractGenerator } from '@ui0/types'
|
|
14
|
+
|
|
15
|
+
import { buildPool, resolvePool } from '../../generator/pool'
|
|
16
|
+
import type { ContractFactory } from '../generator'
|
|
17
|
+
import { ColorPseudoValues, ColorSchemeValues } from './color.token'
|
|
18
|
+
import type { ColorThemeAppearanceConfig, ThemeColorPool } from './color.types'
|
|
19
|
+
|
|
20
|
+
import colorCss from './color.inject.module.css'
|
|
21
|
+
import $color from './color.module.css'
|
|
22
|
+
|
|
23
|
+
// the ramp math (hsl/oklch) yields fractional channels - the css space-
|
|
24
|
+
// separated syntax wants integers (also what hexToRgb/named colors produce)
|
|
25
|
+
function channels(rgb: RgbTuple): string {
|
|
26
|
+
return rgb.map((v) => Math.round(v)).join(' ')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PSEUDOS: ReadonlyArray<string | undefined> = [undefined, ...ColorPseudoValues]
|
|
30
|
+
const GATINGS: ReadonlyArray<{ prefix: string; selector: string }> = [
|
|
31
|
+
{ prefix: '', selector: '' },
|
|
32
|
+
...ColorSchemeValues.map((s) => ({
|
|
33
|
+
prefix: `${s}-`,
|
|
34
|
+
selector: `.theme_${s} `
|
|
35
|
+
}))
|
|
36
|
+
]
|
|
37
|
+
const SLOTS = ['c', 'f'] as const
|
|
38
|
+
|
|
39
|
+
const colorGenerator: ContractFactory = (ctx): ThemeContractGenerator => {
|
|
40
|
+
const { config, hash } = ctx
|
|
41
|
+
|
|
42
|
+
function vars(): Record<string, VarList> {
|
|
43
|
+
const pool = (config as { color?: ColorThemeAppearanceConfig }).color
|
|
44
|
+
const merged: ThemeColorPool = buildPool(pool, undefined)
|
|
45
|
+
if (!Object.keys(merged).length) return {}
|
|
46
|
+
|
|
47
|
+
const { base, ramps } = resolvePool(merged)
|
|
48
|
+
const c: VarList = {}
|
|
49
|
+
// every pool entry, foreground/background like any other - full word
|
|
50
|
+
for (const [name, rgb] of Object.entries(base)) {
|
|
51
|
+
c[`${name}-v`] = channels(rgb)
|
|
52
|
+
const ramp = ramps[name]
|
|
53
|
+
if (!ramp) continue
|
|
54
|
+
for (const [type, steps] of Object.entries(ramp)) {
|
|
55
|
+
steps?.forEach((rgbStep, i) => {
|
|
56
|
+
c[`${name}-${type}${i + 1}-v`] = channels(rgbStep)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// the composed current slots a component consumes (color.scss mixins).
|
|
61
|
+
// The base theme block assigns the current channel slots from the
|
|
62
|
+
// foreground/background values; a .c-*/.f-* setter re-declares the -v slot
|
|
63
|
+
// on its element to override
|
|
64
|
+
if ('foreground' in base) c['cur-v'] = 'var(--0-c-foreground-v)'
|
|
65
|
+
c.cur = 'rgb(var(--0-c-cur-v, var(--0-c-foreground-v)) / var(--0-c-o, 1))'
|
|
66
|
+
const f: VarList = {}
|
|
67
|
+
if ('background' in base) f['cur-v'] = 'var(--0-c-background-v)'
|
|
68
|
+
f.cur = 'rgb(var(--0-f-cur-v, var(--0-c-background-v)) / var(--0-f-o, 1))'
|
|
69
|
+
return { c, f }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function css(): string {
|
|
73
|
+
const pool = (config as { color?: ColorThemeAppearanceConfig }).color
|
|
74
|
+
const names = Object.keys(buildPool(pool, undefined))
|
|
75
|
+
if (!names.length) return colorCss
|
|
76
|
+
|
|
77
|
+
const lines: string[] = []
|
|
78
|
+
for (const { prefix, selector } of GATINGS) {
|
|
79
|
+
for (const pseudo of PSEUDOS) {
|
|
80
|
+
const pseudoPrefix = pseudo ? `${pseudo}-` : ''
|
|
81
|
+
const pseudoSuffix = pseudo ? `:${pseudo}` : ''
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
for (const slot of SLOTS) {
|
|
84
|
+
const cls = `${prefix}${pseudoPrefix}${slot}-${name}`
|
|
85
|
+
const ns = slot === 'c' ? '--0-c' : '--0-f'
|
|
86
|
+
lines.push(`${selector}.${hash(cls)}${pseudoSuffix} { ${ns}-cur-v: var(--0-c-${name}-v); }`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return `${colorCss}\n${lines.join('\n')}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function classes(): CSSModuleClassList {
|
|
95
|
+
// the slot markers come from the registry map; the swatch grid is
|
|
96
|
+
// config-generated and rides the same name-only hasher
|
|
97
|
+
const out: CSSModuleClassList = { ...$color }
|
|
98
|
+
const pool = (config as { color?: ColorThemeAppearanceConfig }).color
|
|
99
|
+
const names = Object.keys(buildPool(pool, undefined))
|
|
100
|
+
for (const { prefix } of GATINGS) {
|
|
101
|
+
for (const pseudo of PSEUDOS) {
|
|
102
|
+
const pseudoPrefix = pseudo ? `${pseudo}-` : ''
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
for (const slot of SLOTS) {
|
|
105
|
+
out[`${prefix}${pseudoPrefix}${slot}-${name}`] = hash(`${prefix}${pseudoPrefix}${slot}-${name}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
namespace: 'c',
|
|
115
|
+
vars: (scheme) => (scheme ? {} : vars()),
|
|
116
|
+
css,
|
|
117
|
+
class: classes,
|
|
118
|
+
styles: () => ({})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default colorGenerator
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { gradientStyle } from './color.gradient'
|
|
4
|
+
|
|
5
|
+
describe('gradientStyle', () => {
|
|
6
|
+
it('composes two stops with the default direction', () => {
|
|
7
|
+
expect(gradientStyle('error->success')).toBe(
|
|
8
|
+
'linear-gradient(to bottom, rgb(var(--0-c-error-v) / 1), rgb(var(--0-c-success-v) / 1))'
|
|
9
|
+
)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('supports a direction keyword and stop opacity', () => {
|
|
13
|
+
expect(gradientStyle('to-br error->success@80->highlight')).toBe(
|
|
14
|
+
'linear-gradient(to bottom right, rgb(var(--0-c-error-v) / 1), rgb(var(--0-c-success-v) / 0.8), rgb(var(--0-c-highlight-v) / 1))'
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('tolerates spaces around arrows', () => {
|
|
19
|
+
expect(gradientStyle('to-right error -> success')).toBe(
|
|
20
|
+
'linear-gradient(to right, rgb(var(--0-c-error-v) / 1), rgb(var(--0-c-success-v) / 1))'
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
it('returns undefined for an empty value', () => {
|
|
24
|
+
expect(gradientStyle('')).toBeUndefined()
|
|
25
|
+
expect(gradientStyle(undefined)).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('throws on a single stop or an unknown direction', () => {
|
|
29
|
+
expect(() => gradientStyle('error')).toThrow()
|
|
30
|
+
expect(() => gradientStyle('to-diagonal error->success')).toThrow()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// the `gradient` prop -> the `--0-g-cur` value (a linear-gradient()
|
|
2
|
+
// composed of live var() references to pool channels). Delivered as an
|
|
3
|
+
// element inline style, NOT a class: a gradient is a composition of values,
|
|
4
|
+
// and class-per-combination would explode combinatorially. The references
|
|
5
|
+
// stay live - a scheme block revalues the underlying --0-c-* channels and
|
|
6
|
+
// the gradient re-paints without any gradient-specific rule.
|
|
7
|
+
//
|
|
8
|
+
// gradient="error->success"
|
|
9
|
+
// gradient="to-br error->success@80->highlight"
|
|
10
|
+
import { isString } from '@ui0/util'
|
|
11
|
+
|
|
12
|
+
import { parseRef } from './color.token'
|
|
13
|
+
|
|
14
|
+
const DIRECTIONS: Record<string, string> = {
|
|
15
|
+
right: 'to right',
|
|
16
|
+
left: 'to left',
|
|
17
|
+
top: 'to top',
|
|
18
|
+
bottom: 'to bottom',
|
|
19
|
+
tr: 'to top right',
|
|
20
|
+
tl: 'to top left',
|
|
21
|
+
br: 'to bottom right',
|
|
22
|
+
bl: 'to bottom left'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** compile a gradient prop value into a `linear-gradient(...)` string */
|
|
26
|
+
export function gradientStyle(value: string | undefined | null): string | undefined {
|
|
27
|
+
if (!isString(value) || !value.trim()) return undefined
|
|
28
|
+
const parts = value.trim().split(/\s+/)
|
|
29
|
+
|
|
30
|
+
let direction = DIRECTIONS.bottom
|
|
31
|
+
if (parts[0]?.startsWith('to-')) {
|
|
32
|
+
const css = DIRECTIONS[parts[0].slice(3)]
|
|
33
|
+
if (!css)
|
|
34
|
+
throw new Error(
|
|
35
|
+
`ui0 gradient: unknown direction "${parts[0]}" (expected to-right|to-left|to-top|to-bottom|to-tr|to-tl|to-br|to-bl)`
|
|
36
|
+
)
|
|
37
|
+
direction = css
|
|
38
|
+
parts.shift()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// rejoin so spaced arrows (`error -> success`) compile too
|
|
42
|
+
const stops = parts
|
|
43
|
+
.join('')
|
|
44
|
+
.split('->')
|
|
45
|
+
.map((s) => parseRef(s))
|
|
46
|
+
.filter((s) => s.ref)
|
|
47
|
+
|
|
48
|
+
if (stops.length < 2) throw new Error(`ui0 gradient: "${value}" needs at least two stops (ref[->ref...])`)
|
|
49
|
+
|
|
50
|
+
const paint = stops.map((s) => `rgb(var(--0-c-${s.ref}-v) / ${s.opacity ?? 1})`).join(', ')
|
|
51
|
+
return `linear-gradient(${direction}, ${paint})`
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentColor } from '@ui0/types'
|
|
2
|
+
import type { ThemeContractDeal } from '@ui0/types'
|
|
3
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
export const ColorContractKey = Symbol('ColorContractKey') as InjectionKey<ThemeContractDeal>
|
|
6
|
+
export const ColorRefKey = Symbol('ColorRefKey') as InjectionKey<Ref<ComponentColor>>
|
|
7
|
+
export const FillRefKey = Symbol('FillRefKey') as InjectionKey<Ref<ComponentColor>>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Composed component slots + the opacity selection bridges.
|
|
2
|
+
//
|
|
3
|
+
// The deal delivers every opacity context as an ELEMENT INLINE var:
|
|
4
|
+
// --0-{c|f}-src ungated (both schemes, no pseudo)
|
|
5
|
+
// --0-{c|f}-src-l|-d scheme only
|
|
6
|
+
// --0-{c|f}-src-{hover|active|focus} pseudo only
|
|
7
|
+
// --0-{c|f}-src-{l|d}-{hover|active|focus} scheme + pseudo
|
|
8
|
+
// Inline styles are unconditional - only a stylesheet can select by scheme
|
|
9
|
+
// or pseudo state, so use() emits the picker: per context a rule re-declares
|
|
10
|
+
// the selected --0-{c|f}-o, and the composed cur reads it. src vars are
|
|
11
|
+
// reset to `initial` per element so a translucent parent does not paint its
|
|
12
|
+
// children (custom property `initial` is guaranteed-invalid, so var()
|
|
13
|
+
// fallbacks keep working). The generated theme css carries NO opacity rules.
|
|
14
|
+
@mixin use {
|
|
15
|
+
@include _slot('c');
|
|
16
|
+
@include _slot('f');
|
|
17
|
+
// gradients arrive as an element inline style - redeclare to stop them
|
|
18
|
+
// inheriting into children (initial = no background-image)
|
|
19
|
+
--0-g-cur: initial;
|
|
20
|
+
}
|
|
21
|
+
@mixin use-state {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@mixin _slot($x) {
|
|
25
|
+
// context sources (set inline by the contract; initial = unset)
|
|
26
|
+
--0-#{$x}-src: initial;
|
|
27
|
+
--0-#{$x}-src-l: initial;
|
|
28
|
+
--0-#{$x}-src-d: initial;
|
|
29
|
+
--0-#{$x}-src-hover: initial;
|
|
30
|
+
--0-#{$x}-src-active: initial;
|
|
31
|
+
--0-#{$x}-src-focus: initial;
|
|
32
|
+
--0-#{$x}-src-l-hover: initial;
|
|
33
|
+
--0-#{$x}-src-l-active: initial;
|
|
34
|
+
--0-#{$x}-src-l-focus: initial;
|
|
35
|
+
--0-#{$x}-src-d-hover: initial;
|
|
36
|
+
--0-#{$x}-src-d-active: initial;
|
|
37
|
+
--0-#{$x}-src-d-focus: initial;
|
|
38
|
+
|
|
39
|
+
// selection bridges - the fallback chain never crosses schemes: a
|
|
40
|
+
// scheme-tagged source applies to that scheme only, the default is 1.
|
|
41
|
+
// :global(...) keeps the theme's scheme classes unscoped - component
|
|
42
|
+
// module styles hash every bare class literal.
|
|
43
|
+
--0-#{$x}-o: var(--0-#{$x}-src, var(--0-#{$x}-src-l, 1));
|
|
44
|
+
--0-#{$x}-cur: rgb(var(--0-#{$x}-cur-v) / var(--0-#{$x}-o));
|
|
45
|
+
:global(.theme_dark) & {
|
|
46
|
+
--0-#{$x}-o: var(--0-#{$x}-src-d, var(--0-#{$x}-src, 1));
|
|
47
|
+
}
|
|
48
|
+
&:hover {
|
|
49
|
+
--0-#{$x}-o: var(--0-#{$x}-src-hover, var(--0-#{$x}-src, var(--0-#{$x}-src-l, 1)));
|
|
50
|
+
}
|
|
51
|
+
&:active {
|
|
52
|
+
--0-#{$x}-o: var(--0-#{$x}-src-active, var(--0-#{$x}-src, var(--0-#{$x}-src-l, 1)));
|
|
53
|
+
}
|
|
54
|
+
&:focus {
|
|
55
|
+
--0-#{$x}-o: var(--0-#{$x}-src-focus, var(--0-#{$x}-src, var(--0-#{$x}-src-l, 1)));
|
|
56
|
+
}
|
|
57
|
+
:global(.theme_dark) &:hover {
|
|
58
|
+
--0-#{$x}-o: var(--0-#{$x}-src-d-hover, var(--0-#{$x}-src-hover, var(--0-#{$x}-src-d, var(--0-#{$x}-src, 1))));
|
|
59
|
+
}
|
|
60
|
+
:global(.theme_dark) &:active {
|
|
61
|
+
--0-#{$x}-o: var(--0-#{$x}-src-d-active, var(--0-#{$x}-src-active, var(--0-#{$x}-src-d, var(--0-#{$x}-src, 1))));
|
|
62
|
+
}
|
|
63
|
+
:global(.theme_dark) &:focus {
|
|
64
|
+
--0-#{$x}-o: var(--0-#{$x}-src-d-focus, var(--0-#{$x}-src-focus, var(--0-#{$x}-src-d, var(--0-#{$x}-src, 1))));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@mixin color {
|
|
69
|
+
color: var(--0-c-cur);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@mixin fill {
|
|
73
|
+
background-color: var(--0-f-cur);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@mixin gradient {
|
|
77
|
+
background-image: var(--0-g-cur);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@mixin apply {
|
|
81
|
+
@include color();
|
|
82
|
+
@include fill();
|
|
83
|
+
@include gradient();
|
|
84
|
+
}
|