@ui0/theme 0.0.0-stage → 0.1.1
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,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { hashClassName } from '../../generator/hash'
|
|
4
|
+
import { resolveClass, serializeTokens } from './color.serialize'
|
|
5
|
+
import type { ColorToken } from './color.types'
|
|
6
|
+
|
|
7
|
+
describe('serializeTokens', () => {
|
|
8
|
+
it('maps a bare token to the swatch setter', () => {
|
|
9
|
+
expect(serializeTokens([{ ref: 'error' }], 'c')).toEqual({
|
|
10
|
+
classes: [hashClassName('c-error')],
|
|
11
|
+
styles: {}
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('maps scheme, pseudo and opacity onto the class name and inline vars', () => {
|
|
16
|
+
const tokens: ColorToken[] = [{ scheme: 'dark', pseudo: 'hover', ref: 'warning', opacity: 0.9 }]
|
|
17
|
+
expect(serializeTokens(tokens, 'c')).toEqual({
|
|
18
|
+
classes: [hashClassName('dark-hover-c-warning')],
|
|
19
|
+
styles: { '--0-c-src-d-hover': '0.9' }
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('delivers a plain opacity as an ungated inline source var', () => {
|
|
24
|
+
expect(serializeTokens([{ ref: 'error', opacity: 0.4 }], 'c')).toEqual({
|
|
25
|
+
classes: [hashClassName('c-error')],
|
|
26
|
+
styles: { '--0-c-src': '0.4' }
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('keeps every context on its own source var', () => {
|
|
31
|
+
const tokens: ColorToken[] = [
|
|
32
|
+
{ ref: 'error', opacity: 0.5 },
|
|
33
|
+
{ scheme: 'light', ref: 'error', opacity: 0.8 },
|
|
34
|
+
{ pseudo: 'focus', ref: 'error', opacity: 0.6 }
|
|
35
|
+
]
|
|
36
|
+
expect(serializeTokens(tokens, 'f')).toEqual({
|
|
37
|
+
classes: [hashClassName('f-error'), hashClassName('light-f-error'), hashClassName('focus-f-error')],
|
|
38
|
+
styles: {
|
|
39
|
+
'--0-f-src': '0.5',
|
|
40
|
+
'--0-f-src-l': '0.8',
|
|
41
|
+
'--0-f-src-focus': '0.6'
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('full opacity emits nothing extra', () => {
|
|
47
|
+
expect(serializeTokens([{ ref: 'error', opacity: 1 }], 'f')).toEqual({
|
|
48
|
+
classes: [hashClassName('f-error')],
|
|
49
|
+
styles: {}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('resolveClass prefers the module map, hashes otherwise', () => {
|
|
54
|
+
expect(resolveClass('c-error', { 'c-error': 'x_1' })).toBe('x_1')
|
|
55
|
+
expect(resolveClass('c-error', {})).toBe(hashClassName('c-error'))
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// tokens -> paint. A token's color selection always compiles to a hashed
|
|
2
|
+
// swatch setter class (`[scheme-][pseudo-]{c|f}-<ref>`) - selections are
|
|
3
|
+
// conditional by nature. Opacity VALUES always ride element inline styles as
|
|
4
|
+
// context source vars (`--0-{slot}-src[-{l|d}][-{pseudo}]`); the conditional
|
|
5
|
+
// pick happens in the component's color.use() scss bridges, so the generated
|
|
6
|
+
// theme css carries no opacity rules at all.
|
|
7
|
+
import type { ClassList, CSSModuleClassList, StyleList } from '@ui0/types'
|
|
8
|
+
|
|
9
|
+
import { hashClassName } from '../../generator/hash'
|
|
10
|
+
import type { ColorToken } from './color.types'
|
|
11
|
+
|
|
12
|
+
export function resolveClass(full: string, $?: CSSModuleClassList): string {
|
|
13
|
+
return ($ && $[full]) || hashClassName(full)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function serializeTokens(
|
|
17
|
+
tokens: ColorToken[],
|
|
18
|
+
slot: 'c' | 'f',
|
|
19
|
+
$?: CSSModuleClassList
|
|
20
|
+
): { classes: ClassList; styles: StyleList } {
|
|
21
|
+
const classes: ClassList = []
|
|
22
|
+
const styles: StyleList = {}
|
|
23
|
+
const ns = slot === 'c' ? '--0-c' : '--0-f'
|
|
24
|
+
|
|
25
|
+
for (const t of tokens) {
|
|
26
|
+
const pre = `${t.scheme ? t.scheme + '-' : ''}${t.pseudo ? t.pseudo + '-' : ''}${slot}`
|
|
27
|
+
classes.push(resolveClass(`${pre}-${t.ref}`, $))
|
|
28
|
+
|
|
29
|
+
if (t.opacity !== undefined && t.opacity < 1) {
|
|
30
|
+
const ctx = `${t.scheme === 'light' ? '-l' : t.scheme === 'dark' ? '-d' : ''}${t.pseudo ? `-${t.pseudo}` : ''}`
|
|
31
|
+
styles[`${ns}-src${ctx}`] = `${t.opacity}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { classes, styles }
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { parseColorProp, parseRef } from './color.token'
|
|
4
|
+
|
|
5
|
+
describe('parseColorProp - token grammar', () => {
|
|
6
|
+
it('parses a bare ref for both schemes', () => {
|
|
7
|
+
expect(parseColorProp('success')).toEqual([{ ref: 'success' }])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('parses ramp refs and opacity via @', () => {
|
|
11
|
+
expect(parseColorProp('brand-l3@50')).toEqual([{ ref: 'brand-l3', opacity: 0.5 }])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('parses a positional light/dark pair with per-scheme opacity', () => {
|
|
15
|
+
expect(parseColorProp('success@80/warning@100')).toEqual([
|
|
16
|
+
{ scheme: 'light', ref: 'success', opacity: 0.8 },
|
|
17
|
+
{ scheme: 'dark', ref: 'warning', opacity: 1 }
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('parses named scheme and pseudo prefixes, order-independent', () => {
|
|
22
|
+
expect(parseColorProp('dark:warning')).toEqual([{ scheme: 'dark', ref: 'warning' }])
|
|
23
|
+
expect(parseColorProp('hover:success@90')).toEqual([{ pseudo: 'hover', ref: 'success', opacity: 0.9 }])
|
|
24
|
+
expect(parseColorProp('dark:hover:warning')).toEqual([{ scheme: 'dark', pseudo: 'hover', ref: 'warning' }])
|
|
25
|
+
expect(parseColorProp('hover:dark:warning')).toEqual([{ scheme: 'dark', pseudo: 'hover', ref: 'warning' }])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('parses space-separated layers: base + pseudo override', () => {
|
|
29
|
+
expect(parseColorProp('success hover:warning')).toEqual([{ ref: 'success' }, { pseudo: 'hover', ref: 'warning' }])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects half pairs (single scheme is spelled dark:ref)', () => {
|
|
33
|
+
expect(() => parseColorProp('/warning')).toThrow()
|
|
34
|
+
expect(() => parseColorProp('warning/')).toThrow()
|
|
35
|
+
expect(() => parseColorProp('a/b/c')).toThrow()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('rejects prefixed tokens inside a positional pair', () => {
|
|
39
|
+
expect(() => parseColorProp('black@50/hover:warning')).toThrow()
|
|
40
|
+
expect(() => parseColorProp('light:black/warning')).toThrow()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('rejects mixing a positional pair with named schemes', () => {
|
|
44
|
+
expect(() => parseColorProp('success/warning dark:highlight')).toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('rejects unknown prefixes and bad opacity', () => {
|
|
48
|
+
expect(() => parseColorProp('hove:success')).toThrow()
|
|
49
|
+
expect(() => parseColorProp('success@120')).toThrow()
|
|
50
|
+
expect(() => parseColorProp('success@abc')).toThrow()
|
|
51
|
+
expect(() => parseColorProp('hover:')).toThrow()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('parseRef keeps names without @ untouched', () => {
|
|
55
|
+
expect(parseRef('my-brand-2')).toEqual({ ref: 'my-brand-2' })
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The color/fill prop-dsl parser (v2 grammar):
|
|
2
|
+
//
|
|
3
|
+
// prop := token (' ' token)* tokens = layers: base + overrides
|
|
4
|
+
// token := ref -> both schemes, no pseudo
|
|
5
|
+
// | ref '/' ref -> positional pair: light / dark
|
|
6
|
+
// | [scheme ':'][pseudo ':'] ref
|
|
7
|
+
// ref := name ['-' ramp] ['@' pct]
|
|
8
|
+
//
|
|
9
|
+
// examples:
|
|
10
|
+
// success both schemes
|
|
11
|
+
// success@80/warning@100 light success@80, dark warning@100
|
|
12
|
+
// dark:warning dark only (light falls back to the inherited/current)
|
|
13
|
+
// success hover:success@90 base + full pseudo override (no opacity sugar)
|
|
14
|
+
//
|
|
15
|
+
// scheme is EITHER a positional pair OR named prefixes, never both; a
|
|
16
|
+
// trailing/leading slash pair member must be a full ref (dark-only is
|
|
17
|
+
// spelled `dark:...`).
|
|
18
|
+
import { isArray, isNumberish, isString } from '@ui0/util'
|
|
19
|
+
|
|
20
|
+
import type { ColorPseudo, ColorScheme, ColorToken } from './color.types'
|
|
21
|
+
|
|
22
|
+
export const ColorSchemeValues: ReadonlyArray<ColorScheme> = ['light', 'dark']
|
|
23
|
+
export const ColorPseudoValues: ReadonlyArray<ColorPseudo> = ['hover', 'active', 'focus']
|
|
24
|
+
|
|
25
|
+
const SPACE = /\s+/
|
|
26
|
+
|
|
27
|
+
/** parse a `<ref>[@pct]` tail into ref + opacity (0..1) */
|
|
28
|
+
export function parseRef(raw: string): { ref: string; opacity?: number } {
|
|
29
|
+
const at = raw.lastIndexOf('@')
|
|
30
|
+
if (at === -1) return { ref: raw }
|
|
31
|
+
const ref = raw.slice(0, at)
|
|
32
|
+
const pct = raw.slice(at + 1)
|
|
33
|
+
if (!ref || !isNumberish(pct)) throw new Error(`ui0 color: invalid ref "${raw}" (expected name[@pct])`)
|
|
34
|
+
const n = Number(pct)
|
|
35
|
+
if (n < 0 || n > 100) throw new Error(`ui0 color: opacity ${pct} out of 0..100 in "${raw}"`)
|
|
36
|
+
return { ref, opacity: n / 100 }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseToken(raw: string): ColorToken {
|
|
40
|
+
const segments = raw.split(':')
|
|
41
|
+
const out: ColorToken = { ref: '' }
|
|
42
|
+
while (segments.length > 1) {
|
|
43
|
+
const head = segments[0]
|
|
44
|
+
if ((ColorSchemeValues as ReadonlyArray<string>).includes(head)) {
|
|
45
|
+
out.scheme = head as ColorScheme
|
|
46
|
+
} else if ((ColorPseudoValues as ReadonlyArray<string>).includes(head)) {
|
|
47
|
+
out.pseudo = head as ColorPseudo
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error(`ui0 color: unknown prefix "${head}" in "${raw}" (expected light|dark|hover|active|focus)`)
|
|
50
|
+
}
|
|
51
|
+
segments.shift()
|
|
52
|
+
}
|
|
53
|
+
Object.assign(out, parseRef(segments[0]))
|
|
54
|
+
if (!out.ref) throw new Error(`ui0 color: missing color ref in "${raw}"`)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** parse a color/fill prop value into tokens (one per layer) */
|
|
59
|
+
export function parseColorProp(value: string | string[] | undefined | null): ColorToken[] {
|
|
60
|
+
const raws = (isString(value) ? value.split(SPACE) : isArray(value) ? value : [])
|
|
61
|
+
.map((x) => String(x).trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
|
|
64
|
+
const tokens: ColorToken[] = []
|
|
65
|
+
let sawPair = false
|
|
66
|
+
let sawNamedScheme = false
|
|
67
|
+
for (const raw of raws) {
|
|
68
|
+
if (raw.includes('/')) {
|
|
69
|
+
const pair = raw.split('/')
|
|
70
|
+
if (pair.length !== 2 || !pair[0] || !pair[1])
|
|
71
|
+
throw new Error(`ui0 color: "${raw}" is not a light/dark pair (spell single-scheme as dark:ref)`)
|
|
72
|
+
if (pair[0].includes(':') || pair[1].includes(':'))
|
|
73
|
+
throw new Error(
|
|
74
|
+
`ui0 color: "${raw}" - pair segments must be plain refs (name[-ramp][@pct]); ` +
|
|
75
|
+
'prefixed parts need the named form: light:<ref> [pseudo:]<ref>'
|
|
76
|
+
)
|
|
77
|
+
tokens.push({ scheme: 'light', ...parseRef(pair[0]) })
|
|
78
|
+
tokens.push({ scheme: 'dark', ...parseRef(pair[1]) })
|
|
79
|
+
sawPair = true
|
|
80
|
+
} else {
|
|
81
|
+
const token = parseToken(raw)
|
|
82
|
+
if (token.scheme) sawNamedScheme = true
|
|
83
|
+
tokens.push(token)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (sawPair && sawNamedScheme)
|
|
87
|
+
throw new Error('ui0 color: a positional `a/b` pair cannot mix with light:/dark: prefixes in one prop')
|
|
88
|
+
return tokens
|
|
89
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ComponentColor, RgbTuple, Ui0ThemeColorRef } from '@ui0/types'
|
|
2
|
+
|
|
3
|
+
/** the `color` prop: text/foreground paint. See color.token.ts for the dsl. */
|
|
4
|
+
export interface ColorProps {
|
|
5
|
+
color?: Ui0ThemeColorRef
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** the `fill` prop: background paint. Same dsl as `color`. */
|
|
9
|
+
export interface FillProps {
|
|
10
|
+
fill?: Ui0ThemeColorRef
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** the `gradient` prop: background-image linear-gradient composed from pool
|
|
14
|
+
* refs (`to-br error->success@80`). Resolved onto `--0-g-cur` as an
|
|
15
|
+
* inline style - see color.gradient.ts. */
|
|
16
|
+
export interface GradientProps {
|
|
17
|
+
gradient?: Ui0ThemeColorRef
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** legacy alias kept for the package's public type exports */
|
|
21
|
+
export interface Gradientable {
|
|
22
|
+
color?: ComponentColor
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ThemeColorName = string
|
|
26
|
+
export type ThemeColorPool = Record<ThemeColorName, ThemeColorEntry>
|
|
27
|
+
|
|
28
|
+
/** the color contract's settings inside a theme preset (appearance config) -
|
|
29
|
+
* a flat pool: role/arbitrary name -> color entry (ramp expressions allowed) */
|
|
30
|
+
export type ColorThemeAppearanceConfig = ThemeColorPool
|
|
31
|
+
|
|
32
|
+
/** (l)ighten, (d)arken, (c)ontrast, (s)hade. */
|
|
33
|
+
export type ThemeColorRampType = 'l' | 'd' | 'c' | 's'
|
|
34
|
+
/** Color space the ramp math runs in - perceptual `oklch` or legacy `hsl`. */
|
|
35
|
+
export type ThemeColorSpace = 'hsl' | 'oklch'
|
|
36
|
+
|
|
37
|
+
export interface ColorOptions {
|
|
38
|
+
ramps?: ThemeColorRampType[]
|
|
39
|
+
space?: ThemeColorSpace
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `[ '#AAAAAA', '#FF11FF', 50 ]` - mix two colors (literals or `[r,g,b]`
|
|
43
|
+
// triples) by `depth` percent.
|
|
44
|
+
export type ThemeColorBlendExpr = [string | RgbTuple, string | RgbTuple, number]
|
|
45
|
+
|
|
46
|
+
// "#hex" / "rgb(...)" / "{name}" / "{name}-{l|d|c|s}{step}"
|
|
47
|
+
export type ThemeColorRef = string
|
|
48
|
+
|
|
49
|
+
export type ThemeColorEntry =
|
|
50
|
+
| ThemeColorRef // bare alias, e.g. hlbg: 'sea-l4'
|
|
51
|
+
// `[ color, steps, options? ]` - base color (or a blend of two), ramp step
|
|
52
|
+
// count, and optional extra ramps / color space.
|
|
53
|
+
| [ThemeColorRef | ThemeColorBlendExpr, number, (ThemeColorRampType[] | ColorOptions)?]
|
|
54
|
+
|
|
55
|
+
/** consumer-side color contract settings (ThemePluginConfig.contracts.color) */
|
|
56
|
+
// the parsed prop-dsl token (see color.token.ts):
|
|
57
|
+
// [light:|dark:][hover:|active:|focus:]<ref>[already split into opacity]
|
|
58
|
+
export type ColorScheme = 'light' | 'dark'
|
|
59
|
+
export type ColorPseudo = 'hover' | 'active' | 'focus'
|
|
60
|
+
|
|
61
|
+
export interface ColorToken {
|
|
62
|
+
scheme?: ColorScheme
|
|
63
|
+
pseudo?: ColorPseudo
|
|
64
|
+
/** pool reference: `name` or `name-{l|d|c|s}{step}` */
|
|
65
|
+
ref: string
|
|
66
|
+
/** 0..1; undefined = fully opaque */
|
|
67
|
+
opacity?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// registration: the contract announces itself to the package registries
|
|
71
|
+
// (interface merging - no central import of the contracts anywhere)
|
|
72
|
+
declare module '@ui0/types/theme' {
|
|
73
|
+
interface Ui0ThemePresetAppearanceConfig {
|
|
74
|
+
color?: ColorThemeAppearanceConfig
|
|
75
|
+
}
|
|
76
|
+
interface Ui0ThemeContractSettings {
|
|
77
|
+
/** `false` (or `true`) toggles the contract wholesale; an object is
|
|
78
|
+
* reserved for color contract options (none yet) */
|
|
79
|
+
color?: boolean | Record<string, unknown>
|
|
80
|
+
}
|
|
81
|
+
interface Ui0ThemeContracts extends ColorProps, FillProps, GradientProps {}
|
|
82
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
color/ - the paint contract: `color` (text), `fill` (background), `gradient`
|
|
2
|
+
(background-image). v2 grammar (the v1 contract is archived at
|
|
3
|
+
`../color-legacy/`, unregistered).
|
|
4
|
+
|
|
5
|
+
THE PROP DSL (color.token.ts)
|
|
6
|
+
|
|
7
|
+
prop := token (' ' token)* tokens = layers: base + overrides
|
|
8
|
+
token := ref -> both schemes, no pseudo
|
|
9
|
+
| ref '/' ref -> positional pair: light / dark
|
|
10
|
+
| [scheme ':'][pseudo ':'] ref
|
|
11
|
+
ref := name ['-' ramp] ['@' pct]
|
|
12
|
+
|
|
13
|
+
scheme := light | dark pseudo := hover | active | focus
|
|
14
|
+
|
|
15
|
+
success both schemes
|
|
16
|
+
success@80/warning@100 light success@80, dark warning@100
|
|
17
|
+
dark:warning dark only (light keeps the inherited/current)
|
|
18
|
+
success hover:warning@90 base + full pseudo override (no opacity sugar)
|
|
19
|
+
dark:hover:warning@90 pseudo override in one scheme
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
|
|
23
|
+
- `@pct` is the only opacity spelling (no `-N`); `brand-2@50` is unambiguous
|
|
24
|
+
because the ramp suffix stays part of the ref.
|
|
25
|
+
- Scheme is EITHER a positional pair OR named prefixes, never both in one
|
|
26
|
+
prop. Pair segments must be plain refs - a prefixed part
|
|
27
|
+
(`black@50/hover:warning`) throws, pointing to the named form
|
|
28
|
+
(`light:black@50 hover:warning`). Single-scheme is `dark:...`, never a
|
|
29
|
+
half-empty pair.
|
|
30
|
+
- Prefix keywords are a closed set ({light, dark} x {hover, active, focus}),
|
|
31
|
+
parsed order-independently; anything else before `:` throws.
|
|
32
|
+
|
|
33
|
+
THE PARSED TOKEN (color.types.ts)
|
|
34
|
+
|
|
35
|
+
interface ColorToken {
|
|
36
|
+
scheme?: 'light' | 'dark' // undefined = both
|
|
37
|
+
pseudo?: 'hover' | 'active' | 'focus' // undefined = none
|
|
38
|
+
ref: string // 'name' or 'name-{l|d|c|s}{step}', opaque
|
|
39
|
+
opacity?: number // 0..1, undefined = opaque (normalized at parse)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
parseColorProp(value: string | string[] | undefined | null): ColorToken[]
|
|
43
|
+
|
|
44
|
+
DELIVERY (color.serialize.ts / color.contract.ts / color.scss)
|
|
45
|
+
|
|
46
|
+
Selections ride classes, VALUES ride inline styles:
|
|
47
|
+
|
|
48
|
+
- swatch setters ALWAYS classes: `[scheme-][pseudo-]{c|f}-<ref>` - selection
|
|
49
|
+
is conditional by nature (scheme/pseudo), and classes keep the theme layer
|
|
50
|
+
weak (apps override with their own css).
|
|
51
|
+
- opacity VALUES always ride ELEMENT INLINE STYLES as context source vars:
|
|
52
|
+
`--0-{c|f}-src` (ungated), `-src-l|-d` (scheme), `-src-{hover|active|focus}`
|
|
53
|
+
(pseudo), `-src-{l|d}-{pseudo}` (combo). The generated theme css carries
|
|
54
|
+
NO opacity rules. The conditional pick lives in the component's color.use()
|
|
55
|
+
scss: per context a bridge rule (`:global(.theme_dark) &`, `&:hover`, ...)
|
|
56
|
+
re-declares the selected `--0-{c|f}-o` and the composed current reads it.
|
|
57
|
+
Fallback chains never cross schemes (a scheme-tagged source applies to
|
|
58
|
+
that scheme only; default 1). src vars reset to `initial` per element so
|
|
59
|
+
a translucent parent does not paint its children. `:global()` keeps the
|
|
60
|
+
theme's scheme classes unscoped - component module styles hash every bare
|
|
61
|
+
class literal.
|
|
62
|
+
- gradient: `gradient="[to-<dir> ]ref[->ref...]"` (to-right|to-left|to-top|
|
|
63
|
+
to-bottom|to-tr|to-tl|to-br|to-bl, default to-bottom) compiles to
|
|
64
|
+
`--0-g-cur` as an inline style of LIVE var() references - gradients are
|
|
65
|
+
compositions, not selections, so they get scheme-adaptivity through token
|
|
66
|
+
indirection with zero gradient-specific classes. Consumed by the
|
|
67
|
+
`background-image` part of the color.apply() mixin.
|
|
68
|
+
|
|
69
|
+
GENERATOR (color.generator.ts)
|
|
70
|
+
|
|
71
|
+
Emits per pool name: swatch setters x 2 slots x {ungated, .theme_light,
|
|
72
|
+
.theme_dark} x {none, hover, active, focus} - nothing else. Names hash with
|
|
73
|
+
the same name-only sha256 the css-modules distiller uses (src/generator/hash.ts),
|
|
74
|
+
so the runtime resolves any class without a registry. The used-tokens
|
|
75
|
+
resolver is planned to prune the grid.
|
|
76
|
+
|
|
77
|
+
FILES
|
|
78
|
+
|
|
79
|
+
color.types.ts props + ColorToken + pool config types (shared with generator/pool.ts)
|
|
80
|
+
color.token.ts the prop-dsl parser (parseColorProp / parseRef)
|
|
81
|
+
color.serialize.ts tokens -> {classes, styles}
|
|
82
|
+
color.gradient.ts the gradient prop -> `--0-g-cur` value
|
|
83
|
+
color.contract.ts the deal: one parse pass per prop change, shared by classes()/styles()
|
|
84
|
+
color.generator.ts the build-time endpoint (vars + setter classes)
|
|
85
|
+
color.scss use/apply mixins (composed slots incl. --0-g-cur)
|
|
86
|
+
color.module.css markers: colored, filled, gradiented
|
|
87
|
+
color.keys.ts provide/inject keys for the `inherit` passthrough
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ThemeContract } from '@ui0/types'
|
|
2
|
+
|
|
3
|
+
import { ThemeContractFlags } from '../../constants'
|
|
4
|
+
import { colorDeal } from './color.contract'
|
|
5
|
+
import { ColorContractKey, ColorRefKey } from './color.keys'
|
|
6
|
+
import type { ColorProps, GradientProps, FillProps } from './color.types'
|
|
7
|
+
|
|
8
|
+
export { default as $style } from './color.module.css'
|
|
9
|
+
export type * from './color.types'
|
|
10
|
+
export * from './color.keys'
|
|
11
|
+
|
|
12
|
+
export const ColorPropsKeys: ReadonlyArray<keyof (GradientProps & ColorProps & FillProps)> = [
|
|
13
|
+
'color',
|
|
14
|
+
'fill',
|
|
15
|
+
'gradient'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export const color: ThemeContract = {
|
|
19
|
+
name: 'color',
|
|
20
|
+
key: ColorContractKey,
|
|
21
|
+
flag: ThemeContractFlags.dealer + ThemeContractFlags.generator,
|
|
22
|
+
ref: ColorRefKey,
|
|
23
|
+
deal: colorDeal,
|
|
24
|
+
props: ColorPropsKeys
|
|
25
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AnyProps, ClassList, CSSModuleClassList, Indexed, StyleList } from '@ui0/types'
|
|
2
|
+
import type { ContractProvider, ContractRefs, ThemeContract, ThemeContractName, Ui0ThemeContracts } from '@ui0/types'
|
|
3
|
+
import { isArray, isString } from '@ui0/util'
|
|
4
|
+
function createProviderInstance(name: string, props?: Ui0ThemeContracts, styles?: CSSModuleClassList) {
|
|
5
|
+
const provider = {}
|
|
6
|
+
}
|
|
7
|
+
export class ThemeContractInstance implements ContractProvider {
|
|
8
|
+
props: AnyProps
|
|
9
|
+
/**
|
|
10
|
+
* context have any provided props from styler's
|
|
11
|
+
*/
|
|
12
|
+
context: ContractRefs | null
|
|
13
|
+
$styles: CSSModuleClassList
|
|
14
|
+
name: string
|
|
15
|
+
classList: ClassList = []
|
|
16
|
+
styleList: StyleList = {}
|
|
17
|
+
contracts: Partial<Record<ThemeContractName, ThemeContract>>
|
|
18
|
+
|
|
19
|
+
constructor(name: string, props?: Ui0ThemeContracts, styles?: CSSModuleClassList) {
|
|
20
|
+
this.$styles = styles ?? {}
|
|
21
|
+
this.props = props ?? {}
|
|
22
|
+
this.name = name
|
|
23
|
+
this.context = null
|
|
24
|
+
this.contracts = {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public add(contract: ThemeContract) {
|
|
28
|
+
this.contracts[contract.name] = contract
|
|
29
|
+
}
|
|
30
|
+
public prop(name: keyof Ui0ThemeContracts | Array<keyof Ui0ThemeContracts>) {
|
|
31
|
+
const jobs: Indexed<ThemeContract> = {}
|
|
32
|
+
const instance = this
|
|
33
|
+
|
|
34
|
+
for (const contract of Object.values(this.contracts)) {
|
|
35
|
+
if (isArray(name)) {
|
|
36
|
+
name.forEach((n) => {
|
|
37
|
+
if (contract.props?.includes(n)) {
|
|
38
|
+
jobs[contract.name] = contract.deal
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
} else if (isString(name)) {
|
|
42
|
+
if (contract.props?.includes(name)) {
|
|
43
|
+
jobs[contract.name] = contract
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
contract?.props?.includes(name)
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
styles() {
|
|
50
|
+
let out: StyleList = {}
|
|
51
|
+
for (const contract of Object.values(jobs)) {
|
|
52
|
+
const deal = contract?.deal?.(instance)
|
|
53
|
+
if (deal) out = { ...out, ...deal.styles() }
|
|
54
|
+
// if (deal && CONTRACT_PROPS[contract].some((p) => props[p])) {
|
|
55
|
+
// out = { ...out, ...deal.styles(props) }
|
|
56
|
+
// }
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
},
|
|
60
|
+
classes() {
|
|
61
|
+
const out: ClassList = []
|
|
62
|
+
for (const contract of Object.values(jobs)) {
|
|
63
|
+
const deal = contract?.deal?.(instance)
|
|
64
|
+
if (deal) out.push(...deal.classes())
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public remove(name: ThemeContractName) {
|
|
72
|
+
delete this.contracts[name]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public setContext(p: ContractRefs) {
|
|
76
|
+
this.context = p
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public classes(props_?: AnyProps, $styles = this.$styles) {
|
|
80
|
+
const props = props_ ?? this.props
|
|
81
|
+
const out: ClassList = []
|
|
82
|
+
for (const contract of Object.values(this.contracts)) {
|
|
83
|
+
const deal = contract?.deal?.(this)
|
|
84
|
+
if (deal) out.push(...deal.classes(props as Ui0ThemeContracts, $styles))
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public styles(props_?: AnyProps) {
|
|
90
|
+
const props = props_ ?? this.props
|
|
91
|
+
let out: StyleList = {}
|
|
92
|
+
for (const contract of Object.keys(this.contracts) as ThemeContractName[]) {
|
|
93
|
+
const deal = this.contracts[contract]?.deal?.(this)
|
|
94
|
+
if (deal) out = { ...out, ...deal.styles(props) }
|
|
95
|
+
// if (deal && CONTRACT_PROPS[contract].some((p) => props[p])) {
|
|
96
|
+
// out = { ...out, ...deal.styles(props) }
|
|
97
|
+
// }
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Virtual's classes and styles injector
|
|
104
|
+
* @param data
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
public inject(data?: any) {
|
|
108
|
+
if (isArray(data)) {
|
|
109
|
+
// dirty hack to compute it faster
|
|
110
|
+
// in reality we need to check-mark every item of array1 in array2
|
|
111
|
+
if (data.indexOf(this.classList[0]) === -1) return [...this.classList, ...data]
|
|
112
|
+
else return data
|
|
113
|
+
} else return { ...this.styleList, ...data }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The disabled contract's processing endpoint: the `disabled` prop maps to
|
|
2
|
+
// the hashed `.disabled` setter class; `inherit` reads the provided ref.
|
|
3
|
+
import type { ClassList, CSSModuleClassList } from '@ui0/types'
|
|
4
|
+
import type { ContractProvider, ThemeContractDeal } from '@ui0/types'
|
|
5
|
+
import { mergeCssModulesStyles } from '@ui0/util'
|
|
6
|
+
|
|
7
|
+
import { EMPTY_OBJ } from '../../constants'
|
|
8
|
+
import type { DisabledProps } from './disabled.types'
|
|
9
|
+
|
|
10
|
+
import $disabled from './disabled.module.css'
|
|
11
|
+
|
|
12
|
+
function disabledClasses(p: DisabledProps, s?: CSSModuleClassList): ClassList {
|
|
13
|
+
const $ = mergeCssModulesStyles($disabled, s)
|
|
14
|
+
if (p.disabled) return [$.disabled]
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function disabledDeal(instance: ContractProvider): ThemeContractDeal {
|
|
19
|
+
const props = instance.props
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
classes: () => {
|
|
23
|
+
if (instance.context) {
|
|
24
|
+
if (props.disabled === 'inherit' && instance.context.disabled) {
|
|
25
|
+
return instance.inject(disabledClasses({ disabled: instance.context.disabled.value }, instance.$styles))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return instance.inject(disabledClasses(props, instance.$styles))
|
|
29
|
+
},
|
|
30
|
+
styles: () => instance.inject(EMPTY_OBJ)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The disabled contract's generator endpoint: the .disabled setter applies
|
|
2
|
+
// the disabled-state look (shipped as static css in disabled.inject.module.css,
|
|
3
|
+
// selectors hashed at build); the opacity value is theme-authored
|
|
4
|
+
// (Ui0ThemePresetAppearanceConfig.disabled.opacity) and lands on the theme block.
|
|
5
|
+
import type { CSSModuleClassList, VarList } from '@ui0/types'
|
|
6
|
+
import type { ThemeContractGenerator } from '@ui0/types'
|
|
7
|
+
|
|
8
|
+
import type { ContractFactory } from '../generator'
|
|
9
|
+
import type { DisabledThemeAppearanceConfig } from './disabled.types'
|
|
10
|
+
|
|
11
|
+
import disabledCss from './disabled.inject.module.css'
|
|
12
|
+
import $disabled from './disabled.module.css'
|
|
13
|
+
|
|
14
|
+
const disabledGenerator: ContractFactory = (ctx): ThemeContractGenerator => {
|
|
15
|
+
const { config } = ctx
|
|
16
|
+
|
|
17
|
+
function vars(): Record<string, VarList> {
|
|
18
|
+
const disabled = (config as { disabled?: DisabledThemeAppearanceConfig }).disabled
|
|
19
|
+
if (disabled?.opacity === undefined) return {}
|
|
20
|
+
return { dis: { o: disabled.opacity } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
namespace: 'dis',
|
|
25
|
+
vars: (scheme) => (scheme ? {} : vars()),
|
|
26
|
+
css: () => disabledCss,
|
|
27
|
+
class: (): CSSModuleClassList => ({ ...$disabled }),
|
|
28
|
+
styles: () => ({})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default disabledGenerator
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ThemeContractDeal } from '@ui0/types'
|
|
2
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export const DisabledRefKey = Symbol('DisabledRefKey') as InjectionKey<Ref<boolean>>
|
|
5
|
+
|
|
6
|
+
export const DisabledContractKey = Symbol('DisabledContractKey') as InjectionKey<ThemeContractDeal>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface DisabledProps {
|
|
2
|
+
disabled?: boolean
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface DisabledThemeAppearanceConfig {
|
|
6
|
+
opacity?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// registration: the contract announces itself to the package registries
|
|
10
|
+
// (interface merging - no central import of the contracts anywhere)
|
|
11
|
+
declare module '@ui0/types/theme' {
|
|
12
|
+
interface Ui0ThemePresetAppearanceConfig {
|
|
13
|
+
disabled?: DisabledThemeAppearanceConfig
|
|
14
|
+
}
|
|
15
|
+
interface Ui0ThemeContractSettings {
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
}
|
|
18
|
+
interface Ui0ThemeContracts extends DisabledProps {}
|
|
19
|
+
}
|