@yak/solid 0.0.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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/context/baseContext.d.ts +7 -0
- package/dist/context/baseContext.js +8 -0
- package/dist/context/baseContext.js.map +1 -0
- package/dist/context/index.d.ts +29 -0
- package/dist/context/index.js +17 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +352 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +473 -0
- package/dist/internal.js +382 -0
- package/dist/internal.js.map +1 -0
- package/dist/isolated-source-eval/index.d.ts +30 -0
- package/dist/isolated-source-eval/index.js +315 -0
- package/dist/isolated-source-eval/index.js.map +1 -0
- package/dist/isolated-source-eval/worker.js +59 -0
- package/dist/isolated-source-eval/worker.js.map +1 -0
- package/dist/loaders/vite-plugin.d.ts +116 -0
- package/dist/loaders/vite-plugin.js +862 -0
- package/dist/loaders/vite-plugin.js.map +1 -0
- package/loaders/vite-plugin.ts +29 -0
- package/package.json +108 -0
- package/runtime/__tests__/mergeClasses.test.ts +22 -0
- package/runtime/__tests__/reactivity.test.tsx +157 -0
- package/runtime/__tests__/styled.test.tsx +270 -0
- package/runtime/__tests__/testUtils.tsx +25 -0
- package/runtime/__tests__/tsconfig.json +9 -0
- package/runtime/__tests__/typeTest.tsx +62 -0
- package/runtime/atoms.ts +44 -0
- package/runtime/context/baseContext.ts +5 -0
- package/runtime/context/index.ts +37 -0
- package/runtime/cssLiteral.ts +215 -0
- package/runtime/cssProp.ts +26 -0
- package/runtime/globalStyle.ts +54 -0
- package/runtime/index.ts +39 -0
- package/runtime/internal.ts +41 -0
- package/runtime/internals/mergeClasses.ts +36 -0
- package/runtime/internals/mergeCssProp.ts +54 -0
- package/runtime/internals/unitPostFix.ts +34 -0
- package/runtime/keyframes.ts +30 -0
- package/runtime/mocks/cssLiteral.ts +74 -0
- package/runtime/mocks/globalStyle.ts +14 -0
- package/runtime/mocks/keyframes.ts +29 -0
- package/runtime/mocks/styled.ts +8 -0
- package/runtime/publicStyledApi.ts +177 -0
- package/runtime/styled.ts +393 -0
- package/runtime/styledDom.ts +139 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// We are testing internal functionality which does not match
|
|
3
|
+
// 1:1 the API exposed to the user before compilation.
|
|
4
|
+
// Therefore types are not matching and need to be ignored.
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import { YakThemeProvider } from "../context/index.ts";
|
|
7
|
+
import { css } from "../cssLiteral.ts";
|
|
8
|
+
import { mergeCssProp } from "../internals/mergeCssProp.ts";
|
|
9
|
+
import { styled as styledFn } from "../styled.ts";
|
|
10
|
+
import { renderInto } from "./testUtils.tsx";
|
|
11
|
+
|
|
12
|
+
// This transform is usually done by the SWC plugin.
|
|
13
|
+
// However this `styled.test.tsx` does not compile
|
|
14
|
+
// the code before testing
|
|
15
|
+
const styled = Object.assign(styledFn, {
|
|
16
|
+
div: styledFn("div"),
|
|
17
|
+
input: styledFn("input"),
|
|
18
|
+
button: styledFn("button"),
|
|
19
|
+
}) as typeof styledFn;
|
|
20
|
+
|
|
21
|
+
it("should render a literal element", () => {
|
|
22
|
+
const Component = styled.input``;
|
|
23
|
+
const container = renderInto(() => <Component />);
|
|
24
|
+
expect(container.innerHTML).toBe("<input>");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should render a literal element with styles", () => {
|
|
28
|
+
const Component = styled.input("cssClass");
|
|
29
|
+
const container = renderInto(() => <Component />);
|
|
30
|
+
expect(container.querySelector("input")?.className).toBe("cssClass");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should forward properties", () => {
|
|
34
|
+
const Component = styled.input("cssClass");
|
|
35
|
+
const container = renderInto(() => <Component type="text" />);
|
|
36
|
+
const input = container.querySelector("input")!;
|
|
37
|
+
expect(input.className).toBe("cssClass");
|
|
38
|
+
expect(input.getAttribute("type")).toBe("text");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should forward children", () => {
|
|
42
|
+
const Component = styled.div``;
|
|
43
|
+
const container = renderInto(() => (
|
|
44
|
+
<Component>
|
|
45
|
+
<button>Click me!</button>
|
|
46
|
+
</Component>
|
|
47
|
+
));
|
|
48
|
+
expect(container.innerHTML).toBe("<div><button>Click me!</button></div>");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should filter out properties starting with $", () => {
|
|
52
|
+
const Component = styled.input``;
|
|
53
|
+
const container = renderInto(() => <Component $forwardedProp="notForwarded" />);
|
|
54
|
+
const input = container.querySelector("input")!;
|
|
55
|
+
expect(input.getAttributeNames()).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should filter out properties starting with $ when passing to custom components", () => {
|
|
59
|
+
let forwardedKeys: string[] | null = null;
|
|
60
|
+
const Custom = (props) => {
|
|
61
|
+
forwardedKeys = Object.keys(props).filter((key) => props[key] !== undefined);
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
const StyledCustom = styled(Custom)``;
|
|
65
|
+
renderInto(() => <StyledCustom $forwardedProp="notForwarded" />);
|
|
66
|
+
expect(forwardedKeys).not.toContain("$forwardedProp");
|
|
67
|
+
expect(forwardedKeys).not.toContain("theme");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should pass the generated class to custom components", () => {
|
|
71
|
+
let receivedClass: string | undefined;
|
|
72
|
+
const Custom = (props) => {
|
|
73
|
+
receivedClass = props.class;
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
const StyledCustom = styled(Custom)("customClass");
|
|
77
|
+
renderInto(() => <StyledCustom />);
|
|
78
|
+
expect(receivedClass).toBe("customClass");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should merge the class names of a styled(styled()) chain in order", () => {
|
|
82
|
+
const Inner = styled.input("innerClass");
|
|
83
|
+
const Outer = styled(Inner)("outerClass");
|
|
84
|
+
const container = renderInto(() => <Outer />);
|
|
85
|
+
expect(container.querySelector("input")?.className).toBe("innerClass outerClass");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should render a styled(styled()) chain as a single element", () => {
|
|
89
|
+
const Inner = styled.div("innerClass");
|
|
90
|
+
const Outer = styled(Inner)("outerClass");
|
|
91
|
+
const container = renderInto(() => <Outer />);
|
|
92
|
+
expect(container.querySelectorAll("div").length).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should execute each dynamic style function exactly once per render", () => {
|
|
96
|
+
const innerFn = vi.fn(() => css("innerDynamic"));
|
|
97
|
+
const Inner = styled.input("innerClass", innerFn);
|
|
98
|
+
const Outer = styled(Inner)("outerClass");
|
|
99
|
+
const container = renderInto(() => <Outer />);
|
|
100
|
+
expect(innerFn).toHaveBeenCalledTimes(1);
|
|
101
|
+
expect(container.querySelector("input")?.className).toBe("innerClass innerDynamic outerClass");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should support attrs objects", () => {
|
|
105
|
+
const Component = styled.button.attrs({ type: "button" })("cssClass");
|
|
106
|
+
const container = renderInto(() => <Component />);
|
|
107
|
+
const button = container.querySelector("button")!;
|
|
108
|
+
expect(button.getAttribute("type")).toBe("button");
|
|
109
|
+
expect(button.className).toBe("cssClass");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should support attrs functions receiving props", () => {
|
|
113
|
+
const Component = styled.input.attrs((props) => ({
|
|
114
|
+
"aria-label": props.$text,
|
|
115
|
+
}))``;
|
|
116
|
+
const container = renderInto(() => <Component $text="hello world" />);
|
|
117
|
+
const input = container.querySelector("input")!;
|
|
118
|
+
expect(input.getAttribute("aria-label")).toBe("hello world");
|
|
119
|
+
expect(input.getAttribute("$text")).toBe(null);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should preserve attrs order across styled() levels", () => {
|
|
123
|
+
const SubmitButton = styled.button.attrs({ type: "submit" })("submit_btn");
|
|
124
|
+
const ResetButton = styled(SubmitButton).attrs({ type: "reset" })("reset_btn");
|
|
125
|
+
const container = renderInto(() => <ResetButton />);
|
|
126
|
+
const button = container.querySelector("button")!;
|
|
127
|
+
expect(button.getAttribute("type")).toBe("reset");
|
|
128
|
+
expect(button.className).toBe("submit_btn reset_btn");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should flatten chains through a get-only proxy (solid-refresh dev wrapper)", () => {
|
|
132
|
+
const SubmitButton = styled.button.attrs({ type: "submit" })("submit_btn");
|
|
133
|
+
// solid-refresh registers components behind a Proxy that forwards property
|
|
134
|
+
// reads but defines no `has` trap — chain detection must still see the
|
|
135
|
+
// yak component behind it
|
|
136
|
+
const Registered = new Proxy(function HMRComp() {}, {
|
|
137
|
+
get: (_, property) => (SubmitButton as never as Record<PropertyKey, unknown>)[property],
|
|
138
|
+
});
|
|
139
|
+
const ResetButton = styled(Registered as typeof SubmitButton).attrs({ type: "reset" })(
|
|
140
|
+
"reset_btn",
|
|
141
|
+
);
|
|
142
|
+
const container = renderInto(() => <ResetButton />);
|
|
143
|
+
const button = container.querySelector("button")!;
|
|
144
|
+
expect(button.getAttribute("type")).toBe("reset");
|
|
145
|
+
expect(button.className).toBe("submit_btn reset_btn");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should run parent attrs functions before own attrs functions", () => {
|
|
149
|
+
const order: string[] = [];
|
|
150
|
+
const Parent = styled.input.attrs(() => {
|
|
151
|
+
order.push("parent");
|
|
152
|
+
return { "aria-label": "parent" };
|
|
153
|
+
})``;
|
|
154
|
+
const Child = styled(Parent).attrs(() => {
|
|
155
|
+
order.push("child");
|
|
156
|
+
return {};
|
|
157
|
+
})``;
|
|
158
|
+
renderInto(() => <Child />);
|
|
159
|
+
expect(order).toEqual(["parent", "child"]);
|
|
160
|
+
expect(order.filter((entry) => entry === "parent").length).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should merge a user provided class", () => {
|
|
164
|
+
const Component = styled.input("cssClass");
|
|
165
|
+
const container = renderInto(() => <Component class="userClass" />);
|
|
166
|
+
expect(container.querySelector("input")?.className).toBe("userClass cssClass");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should merge a user provided style with dynamic styles", () => {
|
|
170
|
+
const Component = styled.input("cssClass", {
|
|
171
|
+
style: { "--x": () => "10px" },
|
|
172
|
+
});
|
|
173
|
+
const container = renderInto(() => <Component style={{ padding: "5px" }} />);
|
|
174
|
+
const input = container.querySelector("input")!;
|
|
175
|
+
expect(input.style.getPropertyValue("--x")).toBe("10px");
|
|
176
|
+
expect(input.style.padding).toBe("5px");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should set css variables from dynamic props", () => {
|
|
180
|
+
const Component = styled.input("cssClass", {
|
|
181
|
+
style: { "--width": (props) => `${props.$width * 2}px` },
|
|
182
|
+
});
|
|
183
|
+
const container = renderInto(() => <Component $width={10} />);
|
|
184
|
+
expect(container.querySelector("input")?.style.getPropertyValue("--width")).toBe("20px");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should support conditional dynamic class functions", () => {
|
|
188
|
+
const Component = styled.input("base", (props) => props.$active && css("active"));
|
|
189
|
+
const active = renderInto(() => <Component $active />);
|
|
190
|
+
const inactive = renderInto(() => <Component />);
|
|
191
|
+
expect(active.querySelector("input")?.className).toBe("base active");
|
|
192
|
+
expect(inactive.querySelector("input")?.className).toBe("base");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should provide the theme to dynamic style functions", () => {
|
|
196
|
+
const Component = styled.input("base", {
|
|
197
|
+
style: { "--color": (props) => props.theme().color },
|
|
198
|
+
});
|
|
199
|
+
const container = renderInto(() => (
|
|
200
|
+
<YakThemeProvider theme={{ color: "red" }}>
|
|
201
|
+
<Component />
|
|
202
|
+
</YakThemeProvider>
|
|
203
|
+
));
|
|
204
|
+
expect(container.querySelector("input")?.style.getPropertyValue("--color")).toBe("red");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should provide the theme to attrs functions", () => {
|
|
208
|
+
const Component = styled.input.attrs((props) => ({
|
|
209
|
+
"aria-label": props.theme().label,
|
|
210
|
+
}))``;
|
|
211
|
+
const container = renderInto(() => (
|
|
212
|
+
<YakThemeProvider theme={{ label: "themed" }}>
|
|
213
|
+
<Component />
|
|
214
|
+
</YakThemeProvider>
|
|
215
|
+
));
|
|
216
|
+
expect(container.querySelector("input")?.getAttribute("aria-label")).toBe("themed");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should merge the css prop with class and style (mergeCssProp)", () => {
|
|
220
|
+
const result = mergeCssProp(
|
|
221
|
+
{ class: "test-class", style: { padding: "5px" } },
|
|
222
|
+
css("cssPropClass"),
|
|
223
|
+
);
|
|
224
|
+
expect(result).toEqual({
|
|
225
|
+
class: "test-class cssPropClass",
|
|
226
|
+
style: { padding: "5px" },
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should not emit an empty style object from mergeCssProp", () => {
|
|
231
|
+
const result = mergeCssProp({}, css("onlyClass"));
|
|
232
|
+
expect(result).toEqual({ class: "onlyClass" });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should forward unrelated props through mergeCssProp", () => {
|
|
236
|
+
// the compiled output spreads only mergeCssProp's result onto the element,
|
|
237
|
+
// so onClick, data-*, children etc. must survive the merge
|
|
238
|
+
const onClick = () => {};
|
|
239
|
+
const result = mergeCssProp(
|
|
240
|
+
{ "data-testid": "button", onClick, children: "label", class: "user" },
|
|
241
|
+
css("cssPropClass"),
|
|
242
|
+
);
|
|
243
|
+
expect(result).toEqual({
|
|
244
|
+
"data-testid": "button",
|
|
245
|
+
onClick,
|
|
246
|
+
children: "label",
|
|
247
|
+
class: "user cssPropClass",
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should apply no styles for a falsy css prop", () => {
|
|
252
|
+
// e.g. `css={on && css`...`}` with `on` being false
|
|
253
|
+
const result = mergeCssProp({ class: "user" }, false);
|
|
254
|
+
expect(result).toEqual({ class: "user" });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should forward a `component` prop to wrapped components", () => {
|
|
258
|
+
// polymorphic targets (e.g. router links) accept a `component` prop -
|
|
259
|
+
// it must reach them instead of being consumed by the render mechanism
|
|
260
|
+
const Polymorphic = (props: any) => (
|
|
261
|
+
<a data-received={props.component} class={props.class}>
|
|
262
|
+
{props.children}
|
|
263
|
+
</a>
|
|
264
|
+
);
|
|
265
|
+
const Styled = styledFn(Polymorphic)("styledClass");
|
|
266
|
+
const container = renderInto(() => <Styled component="button">link</Styled>);
|
|
267
|
+
const anchor = container.querySelector("a");
|
|
268
|
+
expect(anchor?.getAttribute("data-received")).toBe("button");
|
|
269
|
+
expect(anchor?.className).toContain("styledClass");
|
|
270
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import { render } from "@solidjs/web";
|
|
3
|
+
import { afterEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
const cleanups: Array<() => void> = [];
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
while (cleanups.length) {
|
|
9
|
+
cleanups.pop()!();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders JSX into a detached container (jsdom) and cleans up after each test.
|
|
15
|
+
*/
|
|
16
|
+
export const renderInto = (fn: () => JSX.Element): HTMLElement => {
|
|
17
|
+
const container = document.createElement("div");
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
const dispose = render(fn as () => any, container);
|
|
20
|
+
cleanups.push(() => {
|
|
21
|
+
dispose();
|
|
22
|
+
container.remove();
|
|
23
|
+
});
|
|
24
|
+
return container;
|
|
25
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-only tests for the public @yak/solid API (pre-compilation typings).
|
|
3
|
+
* Checked by `pnpm test:types`, never executed.
|
|
4
|
+
*/
|
|
5
|
+
import { css, styled, keyframes, useTheme, YakThemeProvider, YakThemeContext } from "@yak/solid";
|
|
6
|
+
|
|
7
|
+
// styled with intrinsic elements infers tag props
|
|
8
|
+
const Button = styled.button<{ $primary?: boolean }>`
|
|
9
|
+
color: ${({ $primary }) => ($primary ? "red" : "blue")};
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
const buttonUsage = (
|
|
13
|
+
<Button type="submit" $primary onClick={(event) => event.currentTarget.disabled} />
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// @ts-expect-error - unknown props are rejected
|
|
17
|
+
const invalidPropUsage = <Button unknownProp />;
|
|
18
|
+
|
|
19
|
+
// styled(Component) requires class/style compatible props
|
|
20
|
+
const Custom = (props: { class?: string; label: string }) => <div class={props.class} />;
|
|
21
|
+
const StyledCustom = styled(Custom)`
|
|
22
|
+
color: red;
|
|
23
|
+
`;
|
|
24
|
+
const customUsage = <StyledCustom label="hello" />;
|
|
25
|
+
|
|
26
|
+
// @ts-expect-error - missing required prop
|
|
27
|
+
const missingPropUsage = <StyledCustom />;
|
|
28
|
+
|
|
29
|
+
// attrs narrow the outer props
|
|
30
|
+
const WithAttrs = styled.input.attrs({ type: "text" })`
|
|
31
|
+
color: red;
|
|
32
|
+
`;
|
|
33
|
+
const withAttrsUsage = <WithAttrs value="x" />;
|
|
34
|
+
|
|
35
|
+
// keyframes returns an animation-name usable in interpolations
|
|
36
|
+
const rotate = keyframes`
|
|
37
|
+
from { transform: rotate(0deg); }
|
|
38
|
+
to { transform: rotate(360deg); }
|
|
39
|
+
`;
|
|
40
|
+
const Spinner = styled.div`
|
|
41
|
+
animation: ${rotate} 1s linear infinite;
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
// css prop is available on intrinsic elements (via css-prop.d.ts)
|
|
45
|
+
const cssPropUsage = (
|
|
46
|
+
<div
|
|
47
|
+
css={css`
|
|
48
|
+
color: red;
|
|
49
|
+
`}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// theme APIs: useTheme returns an accessor to the (augmentable) theme
|
|
54
|
+
const theme = useTheme();
|
|
55
|
+
const themeValue: import("../context/index.ts").YakTheme = theme();
|
|
56
|
+
const provider = <YakThemeProvider theme={{}}>{cssPropUsage}</YakThemeProvider>;
|
|
57
|
+
// the context itself is a provider taking an accessor (plain objects are rejected)
|
|
58
|
+
const override = <YakThemeContext value={() => ({})}>{cssPropUsage}</YakThemeContext>;
|
|
59
|
+
// @ts-expect-error - the context value must be an accessor, not a theme object
|
|
60
|
+
const invalidOverride = <YakThemeContext value={{}}>{cssPropUsage}</YakThemeContext>;
|
|
61
|
+
|
|
62
|
+
export { buttonUsage, customUsage, withAttrsUsage, Spinner, provider, themeValue, override, invalidOverride };
|
package/runtime/atoms.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ComponentStyles, css } from "./cssLiteral.js";
|
|
2
|
+
import { RuntimeStyleProcessor } from "./publicStyledApi.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Allows to use atomic CSS classes in a styled or css block
|
|
6
|
+
*
|
|
7
|
+
* @usage
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { styled, atoms } from "@yak/solid";
|
|
11
|
+
*
|
|
12
|
+
* const Button = styled.button<{ $primary?: boolean }>`
|
|
13
|
+
* ${atoms("text-teal-600", "text-base", "rounded-md")}
|
|
14
|
+
* ${props => props.$primary && atoms("shadow-md")}
|
|
15
|
+
* `;
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const atoms = <T>(
|
|
19
|
+
...atoms: (string | RuntimeStyleProcessor<T> | false)[]
|
|
20
|
+
): ComponentStyles<T> => {
|
|
21
|
+
const staticClasses: string[] = [];
|
|
22
|
+
const dynamicFunctions: RuntimeStyleProcessor<T>[] = [];
|
|
23
|
+
|
|
24
|
+
for (const atom of atoms) {
|
|
25
|
+
if (typeof atom === "string") {
|
|
26
|
+
staticClasses.push(...atom.split(" "));
|
|
27
|
+
} else if (typeof atom === "function") {
|
|
28
|
+
dynamicFunctions.push(atom);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const runtimeFunctions: RuntimeStyleProcessor<T>[] =
|
|
33
|
+
staticClasses.length > 0
|
|
34
|
+
? [
|
|
35
|
+
(_, classes) => {
|
|
36
|
+
staticClasses.forEach((cls) => classes.add(cls));
|
|
37
|
+
},
|
|
38
|
+
...dynamicFunctions,
|
|
39
|
+
]
|
|
40
|
+
: dynamicFunctions;
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error the internal implementation of css is not typed
|
|
43
|
+
return css(...runtimeFunctions);
|
|
44
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createComponent, createContext, createMemo, useContext } from "solid-js";
|
|
2
|
+
import type { Accessor } from "solid-js";
|
|
3
|
+
import type { JSX } from "@solidjs/web";
|
|
4
|
+
|
|
5
|
+
export interface YakTheme {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The yak theme context
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/DigitecGalaxus/next-yak/blob/main/packages/next-yak/runtime/context/README.md
|
|
11
|
+
*/
|
|
12
|
+
export const YakThemeContext = /* @__PURE__ */ createContext<Accessor<YakTheme>>(() => ({}));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns an accessor to the current yak theme
|
|
16
|
+
*/
|
|
17
|
+
export const useTheme = (): Accessor<YakTheme> => useContext(YakThemeContext);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Yak theme context provider
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <YakThemeProvider theme={{ highContrast: highContrast() }}>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const YakThemeProvider = (props: {
|
|
27
|
+
children?: JSX.Element;
|
|
28
|
+
theme?: YakTheme;
|
|
29
|
+
}): JSX.Element => {
|
|
30
|
+
const theme = createMemo<YakTheme>(() => props.theme ?? {});
|
|
31
|
+
return createComponent(YakThemeContext, {
|
|
32
|
+
value: theme,
|
|
33
|
+
get children() {
|
|
34
|
+
return props.children;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { YakTheme } from "./index.ts";
|
|
2
|
+
import type { Accessor } from "solid-js";
|
|
3
|
+
import { ClassCollector, RuntimeStyleProcessor, StyleObject } from "./publicStyledApi.js";
|
|
4
|
+
|
|
5
|
+
export const yakComponentSymbol = Symbol("yak");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collects class names as a single space-separated string.
|
|
9
|
+
*
|
|
10
|
+
* `add` is the render hot path and stays a plain string append;
|
|
11
|
+
* `has`/`delete` keep a Set-like contract for the rare runtime
|
|
12
|
+
* functions that remove classes again (e.g. atoms).
|
|
13
|
+
*/
|
|
14
|
+
export class Classes implements ClassCollector {
|
|
15
|
+
value: string;
|
|
16
|
+
constructor(initial?: string) {
|
|
17
|
+
this.value = initial || "";
|
|
18
|
+
}
|
|
19
|
+
add(name: string) {
|
|
20
|
+
if (!this.value) {
|
|
21
|
+
this.value = name;
|
|
22
|
+
} else if (!this.has(name)) {
|
|
23
|
+
this.value += " " + name;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
has(name: string) {
|
|
27
|
+
return (" " + this.value + " ").includes(" " + name + " ");
|
|
28
|
+
}
|
|
29
|
+
delete(name: string) {
|
|
30
|
+
if (this.has(name)) {
|
|
31
|
+
this.value = this.value
|
|
32
|
+
.split(" ")
|
|
33
|
+
.filter((existing) => existing !== name)
|
|
34
|
+
.join(" ");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ComponentStyles<TProps> = (props: TProps) => {
|
|
40
|
+
class: string;
|
|
41
|
+
style?: {
|
|
42
|
+
[key: string]: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type CSSInterpolation<TProps> =
|
|
47
|
+
| string
|
|
48
|
+
| number
|
|
49
|
+
| undefined
|
|
50
|
+
| null
|
|
51
|
+
| false
|
|
52
|
+
| ComponentStyles<TProps>
|
|
53
|
+
| {
|
|
54
|
+
// type only identifier to allow targeting components
|
|
55
|
+
// e.g. styled.svg`${Button}:hover & { fill: red; }`
|
|
56
|
+
[yakComponentSymbol]: any;
|
|
57
|
+
}
|
|
58
|
+
| ((props: TProps) => CSSInterpolation<TProps>);
|
|
59
|
+
|
|
60
|
+
type CSSStyles<TProps = {}> = {
|
|
61
|
+
style: { [key: string]: string | ((props: TProps) => string) };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type CSSFunction = <TProps = {}>(
|
|
65
|
+
styles: TemplateStringsArray,
|
|
66
|
+
...values: CSSInterpolation<TProps & { theme: Accessor<YakTheme> }>[]
|
|
67
|
+
) => ComponentStyles<TProps>;
|
|
68
|
+
|
|
69
|
+
export type NestedRuntimeStyleProcessor = (
|
|
70
|
+
props: unknown,
|
|
71
|
+
classes: ClassCollector,
|
|
72
|
+
style: StyleObject,
|
|
73
|
+
) =>
|
|
74
|
+
| {
|
|
75
|
+
class?: string;
|
|
76
|
+
style?: StyleObject;
|
|
77
|
+
}
|
|
78
|
+
| void
|
|
79
|
+
| NestedRuntimeStyleProcessor;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* css() runtime factory of css``
|
|
83
|
+
*
|
|
84
|
+
* /!\ @yak/solid transpiles css`` and styled``
|
|
85
|
+
*
|
|
86
|
+
* This changes the typings of the css`` and styled`` functions.
|
|
87
|
+
* During development the user of @yak/solid wants to work with the
|
|
88
|
+
* typings BEFORE compilation.
|
|
89
|
+
*
|
|
90
|
+
* Therefore this is only an internal function only and it must be cast to any
|
|
91
|
+
* before exported to the user.
|
|
92
|
+
*
|
|
93
|
+
* The internal functioning of css`` is to return a single callback function that runs all functions
|
|
94
|
+
* (or creates new ones if needed) that are passed as arguments. These functions receive the props, classes, and style object as arguments
|
|
95
|
+
* and operate directly on the classes and style objects.
|
|
96
|
+
*/
|
|
97
|
+
export function css<TProps>(
|
|
98
|
+
styles: TemplateStringsArray,
|
|
99
|
+
...values: CSSInterpolation<NoInfer<TProps> & { theme: Accessor<YakTheme> }>[]
|
|
100
|
+
): ComponentStyles<TProps>;
|
|
101
|
+
export function css<TProps>(...args: Array<any>): RuntimeStyleProcessor<TProps> {
|
|
102
|
+
// Normally this could be an array of strings passed, but as we transpile the usage of css`` ourselves, we control the arguments
|
|
103
|
+
// and ensure that only the first argument is a string (class name of the non-dynamic styles)
|
|
104
|
+
let staticClass: string | undefined;
|
|
105
|
+
const dynamicCssFunctions: NestedRuntimeStyleProcessor[] = [];
|
|
106
|
+
for (const arg of args as Array<string | CSSFunction | CSSStyles<any>>) {
|
|
107
|
+
// A CSS-module class name which got auto generated during build from static css
|
|
108
|
+
// e.g. css`color: red;`
|
|
109
|
+
// compiled -> css("yak31e4")
|
|
110
|
+
if (typeof arg === "string") {
|
|
111
|
+
staticClass = arg;
|
|
112
|
+
}
|
|
113
|
+
// Dynamic CSS e.g.
|
|
114
|
+
// css`${props => props.active && css`color: red;`}`
|
|
115
|
+
// compiled -> css((props: { active: boolean }) => props.active && css("yak31e4"))
|
|
116
|
+
else if (typeof arg === "function") {
|
|
117
|
+
dynamicCssFunctions.push(arg as unknown as NestedRuntimeStyleProcessor);
|
|
118
|
+
}
|
|
119
|
+
// Dynamic CSS with css variables e.g.
|
|
120
|
+
// css`transform: translate(${props => props.x}, ${props => props.y});`
|
|
121
|
+
// compiled -> css("yak31e4", { style: { "--yakVarX": props => props.x }, "--yakVarY": props => props.y }})
|
|
122
|
+
else if (typeof arg === "object" && "style" in arg) {
|
|
123
|
+
dynamicCssFunctions.push((props, _, style) => {
|
|
124
|
+
for (const key in arg.style) {
|
|
125
|
+
const value = arg.style[key];
|
|
126
|
+
if (typeof value === "function") {
|
|
127
|
+
style[key as keyof StyleObject] = String(
|
|
128
|
+
// The value for a css value can be a theme dependent function e.g.:
|
|
129
|
+
// const borderColor = (props: { theme: { mode: "dark" | "light" } }) => props.theme === "dark" ? "black" : "white";
|
|
130
|
+
// css`border-color: ${borderColor};`
|
|
131
|
+
// Therefore the value has to be extracted recursively
|
|
132
|
+
recursivePropExecution(props, value),
|
|
133
|
+
) as never;
|
|
134
|
+
} else {
|
|
135
|
+
style[key as keyof StyleObject] = String(value) as never;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Non Dynamic CSS
|
|
143
|
+
// This is just an optimization for the common case where there are no dynamic css functions
|
|
144
|
+
// `$dynamic: false` lets the styled runtime skip theme lookup and
|
|
145
|
+
// style-object allocation entirely for static components
|
|
146
|
+
if (dynamicCssFunctions.length === 0) {
|
|
147
|
+
return Object.assign(
|
|
148
|
+
(_: unknown, classes: ClassCollector) => {
|
|
149
|
+
if (staticClass) {
|
|
150
|
+
classes.add(staticClass);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{ $dynamic: false },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Object.assign(
|
|
158
|
+
(props: TProps, classes: ClassCollector, allStyles: StyleObject) => {
|
|
159
|
+
if (staticClass) {
|
|
160
|
+
classes.add(staticClass);
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < dynamicCssFunctions.length; i++) {
|
|
163
|
+
unwrapProps(props, dynamicCssFunctions[i], classes, allStyles);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{ $dynamic: true },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Dynamic CSS with runtime logic
|
|
171
|
+
const unwrapProps = (
|
|
172
|
+
props: unknown,
|
|
173
|
+
fn: NestedRuntimeStyleProcessor,
|
|
174
|
+
classes: ClassCollector,
|
|
175
|
+
style: StyleObject,
|
|
176
|
+
) => {
|
|
177
|
+
let result = fn(props, classes, style);
|
|
178
|
+
while (result) {
|
|
179
|
+
if (typeof result === "function") {
|
|
180
|
+
result = result(props, classes, style);
|
|
181
|
+
continue;
|
|
182
|
+
} else if (typeof result === "object") {
|
|
183
|
+
const resultClass = "class" in result ? result.class : undefined;
|
|
184
|
+
if (resultClass) {
|
|
185
|
+
classes.add(resultClass);
|
|
186
|
+
}
|
|
187
|
+
if ("style" in result && result.style) {
|
|
188
|
+
for (const key in result.style) {
|
|
189
|
+
// This is hard for typescript to infer
|
|
190
|
+
style[key as keyof StyleObject] = result.style[key as keyof StyleObject] as any;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const recursivePropExecution = (props: unknown, fn: (props: unknown) => any): string | number => {
|
|
199
|
+
const result = fn(props);
|
|
200
|
+
if (typeof result === "function") {
|
|
201
|
+
return recursivePropExecution(props, result);
|
|
202
|
+
}
|
|
203
|
+
// typeof guards first: the `process.env` lookup is a real per-call cost
|
|
204
|
+
// for unbundled Node consumers, so it must only run on the invalid path
|
|
205
|
+
if (typeof result !== "string" && typeof result !== "number" && !(result instanceof String)) {
|
|
206
|
+
if (process.env.NODE_ENV === "development") {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Dynamic CSS functions must return a string or number but returned ${JSON.stringify(
|
|
209
|
+
result,
|
|
210
|
+
)}\n\nDynamic CSS function: ${fn.toString()}\n`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
};
|