@paganaye/stylets 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/BaseStyles.d.ts +44 -0
- package/dist/BaseStyles.js +166 -0
- package/dist/Button.d.ts +69 -0
- package/dist/Button.js +56 -0
- package/dist/CssColor.d.ts +44 -0
- package/dist/CssColor.js +196 -0
- package/dist/CssColor.test.d.ts +1 -0
- package/dist/CssColor.test.js +68 -0
- package/dist/CssExpr.d.ts +9 -0
- package/dist/CssExpr.js +35 -0
- package/dist/CssFilter.d.ts +18 -0
- package/dist/CssFilter.js +40 -0
- package/dist/CssNum.d.ts +30 -0
- package/dist/CssNum.js +106 -0
- package/dist/CssNum.test.d.ts +1 -0
- package/dist/CssNum.test.js +30 -0
- package/dist/CssReset.d.ts +1 -0
- package/dist/CssReset.js +15 -0
- package/dist/CssShadow.d.ts +19 -0
- package/dist/CssShadow.js +42 -0
- package/dist/DefaultTheme.d.ts +14 -0
- package/dist/DefaultTheme.js +6 -0
- package/dist/EmptyTheme.d.ts +14 -0
- package/dist/EmptyTheme.js +2 -0
- package/dist/HTML.d.ts +9 -0
- package/dist/HTML.jsx +7 -0
- package/dist/ScopeStyles.d.ts +24 -0
- package/dist/ScopeStyles.js +67 -0
- package/dist/State.d.ts +36 -0
- package/dist/State.js +107 -0
- package/dist/State.test.d.ts +1 -0
- package/dist/State.test.js +41 -0
- package/dist/StyleWriter.d.ts +25 -0
- package/dist/StyleWriter.js +156 -0
- package/dist/Theme.d.ts +56 -0
- package/dist/Theme.js +29 -0
- package/dist/Tone.d.ts +1 -0
- package/dist/Tone.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +9 -0
- package/dist/props.d.ts +7 -0
- package/dist/props.js +104 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.js +1 -0
- package/package.json +36 -0
- package/src/BaseStyles.ts +192 -0
- package/src/Button.ts +68 -0
- package/src/CssColor.test.ts +82 -0
- package/src/CssColor.ts +175 -0
- package/src/CssExpr.ts +25 -0
- package/src/CssFilter.ts +44 -0
- package/src/CssNum.test.ts +37 -0
- package/src/CssNum.ts +93 -0
- package/src/CssReset.ts +17 -0
- package/src/CssShadow.ts +46 -0
- package/src/DefaultTheme.ts +8 -0
- package/src/HTML.tsx +17 -0
- package/src/ScopeStyles.ts +100 -0
- package/src/State.test.ts +47 -0
- package/src/State.ts +164 -0
- package/src/StyleWriter.ts +163 -0
- package/src/Theme.ts +95 -0
- package/src/Tone.ts +1 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +1 -0
- package/src/props.ts +118 -0
- package/src/types.ts +311 -0
package/src/State.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export type EqualityFunction<T> = (a: T, b: T) => boolean;
|
|
2
|
+
|
|
3
|
+
export abstract class BaseState<T = any> {
|
|
4
|
+
protected _listeners: Array<(data: T) => void> = [];
|
|
5
|
+
protected _value: T;
|
|
6
|
+
protected valueEquals: EqualityFunction<T>;
|
|
7
|
+
|
|
8
|
+
constructor(initialValue: T, equalityFunction?: EqualityFunction<T>) {
|
|
9
|
+
this._value = initialValue;
|
|
10
|
+
this.valueEquals = equalityFunction ?? ((a, b) => a === b);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get value(): T {
|
|
14
|
+
return this._value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected internalSetValue(newValue: T): void {
|
|
18
|
+
let previousValue = this._value;
|
|
19
|
+
if (this.valueEquals(newValue, previousValue as T)) return;
|
|
20
|
+
this._value = newValue;
|
|
21
|
+
this.emit(newValue);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public subscribe(listener: (data: T) => void): void {
|
|
25
|
+
this._listeners.push(listener);
|
|
26
|
+
let currentValue = this._value;
|
|
27
|
+
listener(currentValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public unsubscribe(listener: (data: T) => void): void {
|
|
31
|
+
this._listeners = this._listeners.filter(l => l !== listener);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected emit(data: T): void {
|
|
35
|
+
for (const listener of this._listeners) {
|
|
36
|
+
listener(data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public dispose(): void {
|
|
41
|
+
this._listeners = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export class State<T = any> extends BaseState<T> {
|
|
47
|
+
constructor(initialValue: T, equals?: EqualityFunction<T>) {
|
|
48
|
+
super(initialValue, equals);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public setValue(newValue: T): void {
|
|
52
|
+
super.internalSetValue(newValue);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createState<T>(value: T, equals?: EqualityFunction<T>): State<T>;
|
|
57
|
+
export function createState<T>(value?: T, equals?: EqualityFunction<T | undefined>): State<T | undefined>;
|
|
58
|
+
export function createState<T>(value?: T, equals?: EqualityFunction<any>): State<any> {
|
|
59
|
+
return new State<any>(value, equals);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class Effect {
|
|
63
|
+
readonly dependencies: Array<BaseState>;
|
|
64
|
+
readonly recomputeBound = this.recomputeValue.bind(this);
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
watch: BaseState | Array<BaseState>,
|
|
68
|
+
private compute: () => void
|
|
69
|
+
) {
|
|
70
|
+
this.dependencies = Array.isArray(watch) ? watch : [watch];
|
|
71
|
+
|
|
72
|
+
for (let state of this.dependencies) {
|
|
73
|
+
state.subscribe(this.recomputeBound);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected async recomputeValue() {
|
|
78
|
+
try {
|
|
79
|
+
this.compute();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Error during effect computation:", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public dispose(): void {
|
|
86
|
+
for (let state of this.dependencies) {
|
|
87
|
+
state.unsubscribe(this.recomputeBound);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function effect<T>(
|
|
93
|
+
watch: BaseState | Array<BaseState>,
|
|
94
|
+
compute: () => T | undefined,
|
|
95
|
+
) {
|
|
96
|
+
return new Effect(watch, compute);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class Computed<T = any> extends BaseState<T> {
|
|
100
|
+
private readonly effect: Effect;
|
|
101
|
+
|
|
102
|
+
constructor(
|
|
103
|
+
watch: BaseState | Array<BaseState>,
|
|
104
|
+
private compute: () => T,
|
|
105
|
+
initialValue: T,
|
|
106
|
+
isValidValue: boolean,
|
|
107
|
+
equals?: EqualityFunction<T>
|
|
108
|
+
) {
|
|
109
|
+
super(initialValue, equals);
|
|
110
|
+
|
|
111
|
+
let recompute = () => {
|
|
112
|
+
const newValue = this.compute();
|
|
113
|
+
super.internalSetValue(newValue as unknown as T);
|
|
114
|
+
}
|
|
115
|
+
this.effect = new Effect(watch, recompute);
|
|
116
|
+
if (!isValidValue) recompute();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public dispose(): void {
|
|
120
|
+
this.effect.dispose();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function computed<T>(
|
|
125
|
+
watch: BaseState | Array<BaseState>,
|
|
126
|
+
compute: () => T,
|
|
127
|
+
initialValue: T,
|
|
128
|
+
equals?: EqualityFunction<T>
|
|
129
|
+
): Computed<T>;
|
|
130
|
+
|
|
131
|
+
export function computed<T>(
|
|
132
|
+
watch: BaseState | Array<BaseState>,
|
|
133
|
+
compute: () => T | undefined,
|
|
134
|
+
equals?: EqualityFunction<T | undefined>
|
|
135
|
+
): Computed<T | undefined>;
|
|
136
|
+
|
|
137
|
+
export function computed<T>(
|
|
138
|
+
watch: BaseState | Array<BaseState>,
|
|
139
|
+
compute: () => T | undefined,
|
|
140
|
+
initialOrEquals?: T | EqualityFunction<T | undefined>,
|
|
141
|
+
maybeEquals?: EqualityFunction<T>
|
|
142
|
+
) {
|
|
143
|
+
if (typeof initialOrEquals === 'function' || initialOrEquals === undefined) {
|
|
144
|
+
const equals = initialOrEquals as EqualityFunction<T | undefined> | undefined;
|
|
145
|
+
return new Computed<T | undefined>(
|
|
146
|
+
watch,
|
|
147
|
+
compute as () => T | undefined,
|
|
148
|
+
/*initialValue*/ undefined,
|
|
149
|
+
/*isValidValue*/ false,
|
|
150
|
+
equals
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
const initialValue = initialOrEquals as T;
|
|
154
|
+
const equals = maybeEquals as EqualityFunction<T> | undefined;
|
|
155
|
+
return new Computed<T>(
|
|
156
|
+
watch,
|
|
157
|
+
compute as () => T,
|
|
158
|
+
/*initialValue*/ initialValue,
|
|
159
|
+
/*isValidValue*/ true,
|
|
160
|
+
equals
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { CssExpr } from "./CssExpr";
|
|
2
|
+
import { CssProperties, ITheme } from "./types";
|
|
3
|
+
|
|
4
|
+
export function toKebabCase(str: string): string {
|
|
5
|
+
if (str.startsWith('--')) return str;
|
|
6
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class StyleWriter {
|
|
10
|
+
readonly element: HTMLStyleElement;
|
|
11
|
+
readonly lines: string[] = [];
|
|
12
|
+
static readonly builders = new Map<string, StyleWriter>;
|
|
13
|
+
private indentString: string = '';
|
|
14
|
+
private _indentLevel: number = 0;
|
|
15
|
+
|
|
16
|
+
public get indentLevel() {
|
|
17
|
+
return this._indentLevel;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public set indentLevel(value: number) {
|
|
21
|
+
this._indentLevel = value;
|
|
22
|
+
this.indentString = ' '.repeat(this._indentLevel);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private constructor(public readonly name: string) {
|
|
26
|
+
this.element = document.createElement('style');
|
|
27
|
+
this.element.setAttribute("data-section", name);
|
|
28
|
+
document.head.appendChild(this.element);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static byName(name: string): StyleWriter {
|
|
32
|
+
let existing = StyleWriter.builders.get(name);
|
|
33
|
+
if (existing) return existing;
|
|
34
|
+
let newBuilder = new StyleWriter(name);
|
|
35
|
+
StyleWriter.builders.set(name, newBuilder);
|
|
36
|
+
return newBuilder;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
this.element.innerHTML = this.lines.join("\n");
|
|
41
|
+
this.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear() {
|
|
45
|
+
this.lines.length = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
addLines(lines: string[]) {
|
|
49
|
+
lines.forEach(s => this.addLine(s))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addLine(line: string | undefined) {
|
|
53
|
+
if (line === undefined) return;
|
|
54
|
+
this.lines.push(this.indentString + line);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addRule(selector: string, styles: CssProperties | string) {
|
|
58
|
+
this.addLine(`${selector} {`)
|
|
59
|
+
this.indentLevel += 1;
|
|
60
|
+
if (typeof styles === 'string') {
|
|
61
|
+
this.addLine(styles)
|
|
62
|
+
} else if (Array.isArray(styles)) {
|
|
63
|
+
this.addLines(styles);
|
|
64
|
+
} else {
|
|
65
|
+
this.addCssProperties(styles);
|
|
66
|
+
}
|
|
67
|
+
this.indentLevel -= 1;
|
|
68
|
+
this.addLine(`}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected addCssProperties(properties: CssProperties) {
|
|
72
|
+
if (properties == null) return;
|
|
73
|
+
let entries = Object.entries(properties);
|
|
74
|
+
if (entries.length === 0) return;
|
|
75
|
+
|
|
76
|
+
let stringifyValue = (key: string, v: any): string | undefined => {
|
|
77
|
+
if (v === null || v === undefined) return undefined;
|
|
78
|
+
if (typeof v === 'string' || typeof v === 'number') {
|
|
79
|
+
return StyleWriter.stringifyStyle(key, v as any);
|
|
80
|
+
} else if (v instanceof CssExpr) {
|
|
81
|
+
let s = v.toString();
|
|
82
|
+
return s;
|
|
83
|
+
} else if (Array.isArray(v)) {
|
|
84
|
+
let s = v.map(i => stringifyValue(key, i)).join(', ');
|
|
85
|
+
return s;
|
|
86
|
+
} else {
|
|
87
|
+
throw Error("Unexpected type in style value");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (let [k, v] of entries) {
|
|
91
|
+
if (!k.startsWith('--')) {
|
|
92
|
+
k = toKebabCase(k);
|
|
93
|
+
}
|
|
94
|
+
v = stringifyValue(k, v);
|
|
95
|
+
if (v === undefined || v === null) continue;
|
|
96
|
+
this.addLine(`${k}: ${v};`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static stringifyStyle(key: string, value: CssProperties, theme?: ITheme): string | undefined {
|
|
102
|
+
if (value === null || value === undefined) return undefined;
|
|
103
|
+
if (key === 'content' && typeof value === 'string') {
|
|
104
|
+
return `content: ${value};`;
|
|
105
|
+
}
|
|
106
|
+
let result: string;
|
|
107
|
+
switch (typeof value) {
|
|
108
|
+
case "number":
|
|
109
|
+
result = StyleWriter.convertNumberToCssValue(key, value as number);
|
|
110
|
+
break;
|
|
111
|
+
case "function":
|
|
112
|
+
result = (value as any)(theme);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
result = String(value);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static spacingProps = new Set<string>([
|
|
122
|
+
'width', 'height', 'max-width', 'min-width', 'max-height', 'min-height',
|
|
123
|
+
'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
|
124
|
+
'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
|
125
|
+
'gap', 'row-gap', 'column-gap',
|
|
126
|
+
'top', 'right', 'bottom', 'left',
|
|
127
|
+
'inset', 'inset-top', 'inset-right', 'inset-bottom', 'inset-left',
|
|
128
|
+
'border', 'border-radius', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-left-radius', 'border-bottom-right-radius'
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// Properties that are unitless in CSS
|
|
132
|
+
static unitlessProps = new Set<string>([
|
|
133
|
+
'line-height', 'z-index', 'opacity', 'font-weight', 'order', 'flex', 'flex-grow', 'flex-shrink', 'flex-basis'
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
static convertNumberToCssValue(prop: string, value: number): string {
|
|
137
|
+
// normalize prop name to kebab-case as used in spacingPropsSet
|
|
138
|
+
const p = prop.toLowerCase();
|
|
139
|
+
if (StyleWriter.unitlessProps.has(p)) return String(value);
|
|
140
|
+
|
|
141
|
+
if (StyleWriter.spacingProps && StyleWriter.spacingProps.has(p)) {
|
|
142
|
+
// default conversion: treat number as rem with one decimal (value * 0.25 -> 0.25 increments?)
|
|
143
|
+
// original behaviour used Math.round(value * 2.5) / 10 to produce tenths
|
|
144
|
+
return `${Math.round(value * 2.5) / 10}rem`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// fallback: return numeric as-is
|
|
148
|
+
return String(value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function addStyles(section: string, callback: (builder: StyleWriter) => void, thisArg?: any) {
|
|
153
|
+
|
|
154
|
+
let builder = StyleWriter.byName(section);
|
|
155
|
+
try {
|
|
156
|
+
callback.call(thisArg, builder);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (typeof document === 'undefined') return;
|
|
160
|
+
builder.render()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
package/src/Theme.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ITheme, IThemeParts, IScopeStylesParts, MergeThemeArgs, ThemeArgs, ExtractVariants } from "./types";
|
|
2
|
+
import { ScopeStylesBuilder, ScopeStylesPublic } from "./ScopeStyles";
|
|
3
|
+
import { BaseStyles } from "./BaseStyles";
|
|
4
|
+
export type ThemeCSSClass = keyof ITheme['classes'];
|
|
5
|
+
export type ThemeClassList = Partial<Record<ThemeCSSClass, boolean | undefined>>;
|
|
6
|
+
|
|
7
|
+
export function createTheme<
|
|
8
|
+
Colors extends string = never,
|
|
9
|
+
Sizes extends string = never,
|
|
10
|
+
Paddings extends string = never,
|
|
11
|
+
Margins extends string = never,
|
|
12
|
+
Borders extends string = never,
|
|
13
|
+
Layouts extends string = never,
|
|
14
|
+
Backgrounds extends string = never,
|
|
15
|
+
Typography extends string = never,
|
|
16
|
+
Shadows extends string = never,
|
|
17
|
+
Vars extends string = never,
|
|
18
|
+
Classes extends string = never,
|
|
19
|
+
Variants extends Record<string, Record<string, any>> = Record<string, never>
|
|
20
|
+
>(themeParts: IThemeParts<{
|
|
21
|
+
colors: Colors;
|
|
22
|
+
sizes: Sizes;
|
|
23
|
+
paddings: Paddings;
|
|
24
|
+
margins: Margins;
|
|
25
|
+
borders: Borders;
|
|
26
|
+
backgrounds: Backgrounds;
|
|
27
|
+
layouts: Layouts;
|
|
28
|
+
typography: Typography;
|
|
29
|
+
shadows: Shadows;
|
|
30
|
+
vars: Vars;
|
|
31
|
+
classes: Classes;
|
|
32
|
+
variants: Variants
|
|
33
|
+
}>) {
|
|
34
|
+
type A = {
|
|
35
|
+
colors: Colors;
|
|
36
|
+
sizes: Sizes; paddings:
|
|
37
|
+
Paddings; margins:
|
|
38
|
+
Margins; borders: Borders;
|
|
39
|
+
backgrounds: Backgrounds;
|
|
40
|
+
layouts: Layouts;
|
|
41
|
+
typography: Typography;
|
|
42
|
+
shadows: Shadows;
|
|
43
|
+
vars: Vars;
|
|
44
|
+
classes: Classes;
|
|
45
|
+
variants: Variants
|
|
46
|
+
};
|
|
47
|
+
let theme = new ThemeBuilder<A>(themeParts as any);
|
|
48
|
+
return theme as ThemeBuilder<A>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export class ThemeBuilder<A extends ThemeArgs> extends BaseStyles<A> {
|
|
53
|
+
|
|
54
|
+
constructor(themeParts: IThemeParts<A>) {
|
|
55
|
+
super({ ...themeParts } as any);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declareScope<P extends IScopeStylesParts<any> & { class: string, scopeStart?: string, scopeEnd?: string }>(parts: P): ScopeStylesBuilder<MergeThemeArgs<A, P>, MergeThemeArgs<A, P>['variants']>;
|
|
59
|
+
// overload that infers Added from a callback returning parts
|
|
60
|
+
declareScope<P extends (theme: ThemeBuilder<A>) => IScopeStylesParts<any> & { scopeStart: string, scopeEnd?: string }>(parts: P): ScopeStylesBuilder<MergeThemeArgs<A, ReturnType<P>>, MergeThemeArgs<A, ReturnType<P>>['variants']>;
|
|
61
|
+
declareScope(partsOrLambda: any) {
|
|
62
|
+
const resolved = (typeof partsOrLambda === 'function') ? partsOrLambda(this as any) : partsOrLambda;
|
|
63
|
+
return new ScopeStylesBuilder(
|
|
64
|
+
this as any,
|
|
65
|
+
{ ...resolved } as any
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
// overload that infers Added from a concrete parts object
|
|
69
|
+
add<P extends IThemeParts<any>>(parts: P): ThemeBuilder<MergeThemeArgs<A, P>>;
|
|
70
|
+
// overload that infers Added from a callback returning parts
|
|
71
|
+
add<P extends (theme: ThemeBuilder<A>) => IThemeParts<any>>(parts: P): ThemeBuilder<MergeThemeArgs<A, ReturnType<P>>>;
|
|
72
|
+
add(parts: any): any {
|
|
73
|
+
|
|
74
|
+
if (typeof parts === 'function') parts = parts(this as any);
|
|
75
|
+
super.add(parts as any);
|
|
76
|
+
return this as any;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public render() {
|
|
80
|
+
super.render();
|
|
81
|
+
let result = (this as unknown) as ThemePublic<A>;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ThemePublic<A extends ThemeArgs> = {
|
|
87
|
+
declareScope: ThemeBuilder<A>['declareScope'];
|
|
88
|
+
colors: A['colors'] extends string ? Record<A['colors'], string> : Record<string, string>;
|
|
89
|
+
vars: A['vars'] extends string ? Record<A['vars'], string> : Record<string, string>;
|
|
90
|
+
classes: A['classes'] extends string ? Record<A['classes'], string> : Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class Theme<A extends ThemeArgs> extends BaseStyles<A> {
|
|
94
|
+
declareScope!: ThemeBuilder<A>['declareScope'];
|
|
95
|
+
}
|
package/src/Tone.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Tone = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createTheme } from './Theme';
|
|
3
|
+
|
|
4
|
+
describe('createTheme', () => {
|
|
5
|
+
it('returns a theme object', () => {
|
|
6
|
+
const theme = createTheme({}).render();
|
|
7
|
+
expect(theme).toBeDefined();
|
|
8
|
+
expect(theme.classes).toStrictEqual({});
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Theme";
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ClassList, CssProperties } from "./types";
|
|
2
|
+
import { IDivProps } from "./HTML";
|
|
3
|
+
|
|
4
|
+
function classListToRecord<T extends string>(classList?: ClassList<T>): Record<string, boolean> {
|
|
5
|
+
const result: Record<string, boolean> = {};
|
|
6
|
+
if (!classList) return result;
|
|
7
|
+
|
|
8
|
+
if (Array.isArray(classList)) {
|
|
9
|
+
for (const item of classList) {
|
|
10
|
+
if (!item) continue;
|
|
11
|
+
if (typeof item === 'string') {
|
|
12
|
+
result[item] = true;
|
|
13
|
+
} else if (typeof item === 'object') {
|
|
14
|
+
Object.assign(result, item);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} else if (typeof classList === 'string') {
|
|
18
|
+
result[classList] = true;
|
|
19
|
+
} else {
|
|
20
|
+
Object.assign(result, classList);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Normalizes classes and variants. User code can intercept between prepareProps/completeProps to add classes.
|
|
26
|
+
export function prepareProps<T extends IDivProps = IDivProps>(
|
|
27
|
+
propsIn: T,
|
|
28
|
+
styles?: { variants?: Record<string, any>, class?: string }): Record<string, any> {
|
|
29
|
+
|
|
30
|
+
const classRecord = classListToRecord(propsIn.class);
|
|
31
|
+
const dataAttrs: Record<string, string> = {};
|
|
32
|
+
|
|
33
|
+
if (styles?.class) {
|
|
34
|
+
classRecord[styles.class] = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Apply variants as attributes, use 'default' value if not provided
|
|
38
|
+
if (styles?.variants) {
|
|
39
|
+
for (const variantKey of Object.keys(styles.variants)) {
|
|
40
|
+
let variantValue = (propsIn as any)[variantKey];
|
|
41
|
+
|
|
42
|
+
if (variantValue === undefined) {
|
|
43
|
+
const variantGroup = styles.variants[variantKey];
|
|
44
|
+
if (variantGroup && 'default' in variantGroup) {
|
|
45
|
+
variantValue = 'default';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (variantValue !== undefined) {
|
|
50
|
+
dataAttrs[variantKey] = variantValue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
__prepared: true,
|
|
57
|
+
style: styleToString(propsIn.style),
|
|
58
|
+
class: classRecord,
|
|
59
|
+
...dataAttrs,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Converts classRecord to space-separated string
|
|
64
|
+
export function completeProps(propsIn: Record<string, any>): Record<string, any> {
|
|
65
|
+
if (!('__prepared' in propsIn)) {
|
|
66
|
+
propsIn = prepareProps(propsIn as any);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
delete propsIn.__prepared;
|
|
70
|
+
|
|
71
|
+
if (typeof propsIn.class === 'object') {
|
|
72
|
+
propsIn.class = Object.entries(propsIn.class)
|
|
73
|
+
.filter(([_, value]) => value)
|
|
74
|
+
.map(([key]) => key)
|
|
75
|
+
.join(' ');
|
|
76
|
+
}
|
|
77
|
+
return propsIn;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function styleToString(styles?: CssProperties): string | undefined {
|
|
81
|
+
if (!styles) return undefined;
|
|
82
|
+
|
|
83
|
+
let result = '';
|
|
84
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
const cssKey = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
87
|
+
result += `${cssKey}: ${value}; `;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result || undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function Elt(tag: string, props: Record<string, any>, children?: any[]): string {
|
|
94
|
+
const finalProps = completeProps(props) as Record<string, any>;
|
|
95
|
+
|
|
96
|
+
const escapeAttr = (s: string): string =>
|
|
97
|
+
s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
98
|
+
|
|
99
|
+
const attrs = Object.entries(finalProps)
|
|
100
|
+
.map(([k, v]) => {
|
|
101
|
+
if (v === undefined || v === null) return '';
|
|
102
|
+
if (k === 'children') return '';
|
|
103
|
+
if (typeof v === 'boolean') return v ? `${k}` : '';
|
|
104
|
+
return `${k}="${escapeAttr(String(v))}"`;
|
|
105
|
+
})
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(' ');
|
|
108
|
+
|
|
109
|
+
const renderChildren = (c: any): string => {
|
|
110
|
+
if (c === undefined || c === null) return '';
|
|
111
|
+
if (Array.isArray(c)) return c.map(renderChildren).join('');
|
|
112
|
+
return String(c);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const childrenStr = renderChildren(children);
|
|
116
|
+
|
|
117
|
+
return `<${tag}${attrs ? ' ' + attrs : ''}>${childrenStr}</${tag}>`;
|
|
118
|
+
}
|