@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.
Files changed (148) hide show
  1. package/README.md +113 -2
  2. package/dist/backdrop.module.css +1 -0
  3. package/dist/backdrop.scss +7 -0
  4. package/dist/color.inject.module.css +7 -0
  5. package/dist/color.module.css +3 -0
  6. package/dist/color.scss +84 -0
  7. package/dist/components/index.d.ts +4 -0
  8. package/dist/components/index.d.ts.map +1 -0
  9. package/dist/components/index.js +53 -0
  10. package/dist/components/style.css +1 -0
  11. package/dist/components/theme-provider.keys.d.ts +4 -0
  12. package/dist/components/theme-provider.keys.d.ts.map +1 -0
  13. package/dist/components/theme-provider.module.css +1 -0
  14. package/dist/components/theme-provider.types.d.ts +18 -0
  15. package/dist/components/theme-provider.types.d.ts.map +1 -0
  16. package/dist/components/theme-provider.vue.d.ts +19 -0
  17. package/dist/config.default.d.mts +4 -0
  18. package/dist/config.default.d.mts.map +1 -0
  19. package/dist/config.default.mjs +73 -0
  20. package/dist/config.default.mjs.map +1 -0
  21. package/dist/disabled.inject.module.css +5 -0
  22. package/dist/disabled.module.css +1 -0
  23. package/dist/disabled.scss +3 -0
  24. package/dist/display.module.css +2 -0
  25. package/dist/display.scss +63 -0
  26. package/dist/duration.scss +17 -0
  27. package/dist/elevation.module.css +33 -0
  28. package/dist/elevation.scss +46 -0
  29. package/dist/generate-BPYE5ALG.mjs +1037 -0
  30. package/dist/generate-BPYE5ALG.mjs.map +1 -0
  31. package/dist/generator/cli.d.mts +1 -0
  32. package/dist/generator/cli.mjs +103 -0
  33. package/dist/generator/cli.mjs.map +1 -0
  34. package/dist/generator/vite.d.mts +18 -0
  35. package/dist/generator/vite.d.mts.map +1 -0
  36. package/dist/generator/vite.mjs +117 -0
  37. package/dist/generator/vite.mjs.map +1 -0
  38. package/dist/index.d.mts +339 -0
  39. package/dist/index.d.mts.map +1 -0
  40. package/dist/index.mjs +909 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/dist/is.scss +185 -0
  43. package/dist/load-config-64q6nzFy.mjs +48 -0
  44. package/dist/load-config-64q6nzFy.mjs.map +1 -0
  45. package/dist/ripple.scss +57 -0
  46. package/dist/round.inject.module.css +92 -0
  47. package/dist/round.module.css +14 -0
  48. package/dist/round.scss +90 -0
  49. package/dist/size.inject.module.css +50 -0
  50. package/dist/size.module.css +13 -0
  51. package/dist/size.scss +28 -0
  52. package/dist/theme.scss +16 -0
  53. package/dist/typography.inject.module.css +81 -0
  54. package/dist/typography.module.css +32 -0
  55. package/dist/typography.scss +21 -0
  56. package/dist/util.scss +78 -0
  57. package/dist/vars.scss +0 -0
  58. package/package.json +96 -4
  59. package/src/contracts/backdrop/backdrop.contract.ts +44 -0
  60. package/src/contracts/backdrop/backdrop.generator.ts +82 -0
  61. package/src/contracts/backdrop/backdrop.keys.ts +7 -0
  62. package/src/contracts/backdrop/backdrop.module.css +3 -0
  63. package/src/contracts/backdrop/backdrop.scss +7 -0
  64. package/src/contracts/backdrop/backdrop.types.ts +45 -0
  65. package/src/contracts/backdrop/index.ts +21 -0
  66. package/src/contracts/color/color.contract.spec.ts +75 -0
  67. package/src/contracts/color/color.contract.ts +97 -0
  68. package/src/contracts/color/color.generator.spec.ts +73 -0
  69. package/src/contracts/color/color.generator.ts +122 -0
  70. package/src/contracts/color/color.gradient.spec.ts +32 -0
  71. package/src/contracts/color/color.gradient.ts +52 -0
  72. package/src/contracts/color/color.inject.module.css +5 -0
  73. package/src/contracts/color/color.keys.ts +7 -0
  74. package/src/contracts/color/color.module.css +5 -0
  75. package/src/contracts/color/color.scss +84 -0
  76. package/src/contracts/color/color.serialize.spec.ts +57 -0
  77. package/src/contracts/color/color.serialize.ts +35 -0
  78. package/src/contracts/color/color.token.spec.ts +57 -0
  79. package/src/contracts/color/color.token.ts +89 -0
  80. package/src/contracts/color/color.types.ts +82 -0
  81. package/src/contracts/color/index.md +87 -0
  82. package/src/contracts/color/index.ts +25 -0
  83. package/src/contracts/contract.ts +115 -0
  84. package/src/contracts/disabled/disabled.contract.ts +32 -0
  85. package/src/contracts/disabled/disabled.generator.ts +32 -0
  86. package/src/contracts/disabled/disabled.inject.module.css +5 -0
  87. package/src/contracts/disabled/disabled.keys.ts +6 -0
  88. package/src/contracts/disabled/disabled.module.css +3 -0
  89. package/src/contracts/disabled/disabled.scss +3 -0
  90. package/src/contracts/disabled/disabled.types.ts +19 -0
  91. package/src/contracts/disabled/index.ts +21 -0
  92. package/src/contracts/display/display.contract.ts +41 -0
  93. package/src/contracts/display/display.generator.ts +57 -0
  94. package/src/contracts/display/display.instance.ts +173 -0
  95. package/src/contracts/display/display.keys.ts +9 -0
  96. package/src/contracts/display/display.module.css +4 -0
  97. package/src/contracts/display/display.scss +63 -0
  98. package/src/contracts/display/display.types.ts +75 -0
  99. package/src/contracts/display/index.ts +27 -0
  100. package/src/contracts/elevation/elevation.contract.ts +37 -0
  101. package/src/contracts/elevation/elevation.generator.ts +71 -0
  102. package/src/contracts/elevation/elevation.keys.ts +6 -0
  103. package/src/contracts/elevation/elevation.module.css +35 -0
  104. package/src/contracts/elevation/elevation.scss +46 -0
  105. package/src/contracts/elevation/elevation.types.ts +21 -0
  106. package/src/contracts/elevation/index.ts +20 -0
  107. package/src/contracts/generator.ts +38 -0
  108. package/src/contracts/index.md +48 -0
  109. package/src/contracts/index.ts +37 -0
  110. package/src/contracts/round/index.ts +21 -0
  111. package/src/contracts/round/round.contract.ts +75 -0
  112. package/src/contracts/round/round.generator.ts +33 -0
  113. package/src/contracts/round/round.inject.module.scss +36 -0
  114. package/src/contracts/round/round.keys.ts +7 -0
  115. package/src/contracts/round/round.module.css +16 -0
  116. package/src/contracts/round/round.scss +90 -0
  117. package/src/contracts/round/round.types.ts +44 -0
  118. package/src/contracts/size/index.ts +22 -0
  119. package/src/contracts/size/size.contract.ts +98 -0
  120. package/src/contracts/size/size.generator.spec.ts +101 -0
  121. package/src/contracts/size/size.generator.ts +51 -0
  122. package/src/contracts/size/size.inject.module.scss +12 -0
  123. package/src/contracts/size/size.keys.ts +8 -0
  124. package/src/contracts/size/size.module.css +15 -0
  125. package/src/contracts/size/size.scss +28 -0
  126. package/src/contracts/size/size.types.ts +63 -0
  127. package/src/contracts/size/units.scss +17 -0
  128. package/src/contracts/style/index.ts +11 -0
  129. package/src/contracts/style/style.generator.ts +36 -0
  130. package/src/contracts/style/style.key.ts +4 -0
  131. package/src/contracts/style/style.types.ts +16 -0
  132. package/src/contracts/typography/index.ts +26 -0
  133. package/src/contracts/typography/typography.contract.ts +52 -0
  134. package/src/contracts/typography/typography.generator.spec.ts +48 -0
  135. package/src/contracts/typography/typography.generator.ts +44 -0
  136. package/src/contracts/typography/typography.inject.module.css +54 -0
  137. package/src/contracts/typography/typography.keys.ts +12 -0
  138. package/src/contracts/typography/typography.module.css +36 -0
  139. package/src/contracts/typography/typography.scss +21 -0
  140. package/src/contracts/typography/typography.types.ts +90 -0
  141. package/src/generator/theme.default.css +657 -0
  142. package/src/styles/duration.scss +17 -0
  143. package/src/styles/index.md +18 -0
  144. package/src/styles/is.scss +185 -0
  145. package/src/styles/ripple.scss +57 -0
  146. package/src/styles/theme.scss +16 -0
  147. package/src/styles/util.scss +78 -0
  148. 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,5 @@
1
+ @value disabled from './disabled.module.css';
2
+
3
+ .disabled {
4
+ opacity: var(--0-dis-o, 0.5);
5
+ }
@@ -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,3 @@
1
+ .disabled {
2
+ /*@*/
3
+ }
@@ -0,0 +1,3 @@
1
+ @mixin filter {
2
+ filter: grayscale(80%) opacity(70%);
3
+ }
@@ -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
+ }