@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/context/baseContext.d.ts +7 -0
  4. package/dist/context/baseContext.js +8 -0
  5. package/dist/context/baseContext.js.map +1 -0
  6. package/dist/context/index.d.ts +29 -0
  7. package/dist/context/index.js +17 -0
  8. package/dist/context/index.js.map +1 -0
  9. package/dist/index.d.ts +352 -0
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/internal.d.ts +473 -0
  13. package/dist/internal.js +382 -0
  14. package/dist/internal.js.map +1 -0
  15. package/dist/isolated-source-eval/index.d.ts +30 -0
  16. package/dist/isolated-source-eval/index.js +315 -0
  17. package/dist/isolated-source-eval/index.js.map +1 -0
  18. package/dist/isolated-source-eval/worker.js +59 -0
  19. package/dist/isolated-source-eval/worker.js.map +1 -0
  20. package/dist/loaders/vite-plugin.d.ts +116 -0
  21. package/dist/loaders/vite-plugin.js +862 -0
  22. package/dist/loaders/vite-plugin.js.map +1 -0
  23. package/loaders/vite-plugin.ts +29 -0
  24. package/package.json +108 -0
  25. package/runtime/__tests__/mergeClasses.test.ts +22 -0
  26. package/runtime/__tests__/reactivity.test.tsx +157 -0
  27. package/runtime/__tests__/styled.test.tsx +270 -0
  28. package/runtime/__tests__/testUtils.tsx +25 -0
  29. package/runtime/__tests__/tsconfig.json +9 -0
  30. package/runtime/__tests__/typeTest.tsx +62 -0
  31. package/runtime/atoms.ts +44 -0
  32. package/runtime/context/baseContext.ts +5 -0
  33. package/runtime/context/index.ts +37 -0
  34. package/runtime/cssLiteral.ts +215 -0
  35. package/runtime/cssProp.ts +26 -0
  36. package/runtime/globalStyle.ts +54 -0
  37. package/runtime/index.ts +39 -0
  38. package/runtime/internal.ts +41 -0
  39. package/runtime/internals/mergeClasses.ts +36 -0
  40. package/runtime/internals/mergeCssProp.ts +54 -0
  41. package/runtime/internals/unitPostFix.ts +34 -0
  42. package/runtime/keyframes.ts +30 -0
  43. package/runtime/mocks/cssLiteral.ts +74 -0
  44. package/runtime/mocks/globalStyle.ts +14 -0
  45. package/runtime/mocks/keyframes.ts +29 -0
  46. package/runtime/mocks/styled.ts +8 -0
  47. package/runtime/publicStyledApi.ts +177 -0
  48. package/runtime/styled.ts +393 -0
  49. 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,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "noUnusedLocals": false,
5
+ "types": ["node", "vitest/globals"]
6
+ },
7
+ "exclude": [],
8
+ "include": ["./**/*", "../**/*"]
9
+ }
@@ -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 };
@@ -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,5 @@
1
+ import type { YakTheme } from "@yak/solid";
2
+
3
+ export function getYakThemeContext() {
4
+ return {} as YakTheme | undefined;
5
+ }
@@ -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
+ };