@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.
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,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,5 @@
1
+ @value colored, filled, gradiented from './color.module.css';
2
+
3
+ .colored { /* slot marker */ }
4
+ .filled { /* slot marker */ }
5
+ .gradiented { /* slot marker */ }
@@ -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,5 @@
1
+ .colored,
2
+ .filled,
3
+ .gradiented {
4
+ /*@*/
5
+ }
@@ -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
+ }