@firsthandjs/styled 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Firsthand contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # @firsthandjs/styled
2
+
3
+ styled-components' API, on a framework that renders once.
4
+
5
+ **Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/07-styling.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/styled.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
6
+
7
+ ```
8
+ npm install @firsthandjs/styled
9
+ ```
10
+
11
+ 1.97 kB gzip, against styled-components v6's 13.03 kB with React already
12
+ external — both measured the same way, by bundling the package with its peers
13
+ excluded. It needs no React, and no preprocessor.
14
+
15
+ ```tsx
16
+ import { styled, css, ThemeContext } from '@firsthandjs/styled';
17
+
18
+ const Button = styled.button<{ $primary?: boolean }>`
19
+ padding: 0.5rem 1rem;
20
+ border: 1px solid ${(props) => props.theme.line};
21
+ background: ${(props) => (props.$primary === true ? 'rebeccapurple' : 'transparent')};
22
+ border-radius: 4px;
23
+
24
+ &:hover {
25
+ filter: brightness(1.15);
26
+ }
27
+
28
+ @media (min-width: 40rem) {
29
+ padding: 0.5rem 1.5rem;
30
+ }
31
+ `;
32
+
33
+ <Button $primary onClick={save}>
34
+ Save
35
+ </Button>;
36
+ ```
37
+
38
+ ## What is different, and why it is faster
39
+
40
+ styled-components has one mechanism: resolve the template against the props,
41
+ hash the result, insert a class, put it on the element. Every distinct prop
42
+ value is a new rule. A table with a colour per row produces a rule per row.
43
+
44
+ Here, the template is read **once, when you write it**, and each interpolation
45
+ is classified by where it sits:
46
+
47
+ | Where it sits | What it becomes | What a change costs |
48
+ | --------------------------------------- | --------------------- | ------------------------- |
49
+ | A declaration's value — `color: ${…};` | A CSS custom property | One `setProperty` |
50
+ | Anywhere else — `${(p) => p.x && css…}` | Part of the class | A class swap, rule reused |
51
+
52
+ So this:
53
+
54
+ ```tsx
55
+ const Row = styled.div<{ $hue: number }>`
56
+ color: hsl(${(props) => props.$hue} 70% 50%);
57
+ `;
58
+ ```
59
+
60
+ produces **one** rule for a thousand rows with a thousand different hues. The
61
+ test suite asserts exactly that, and the browser check in
62
+ [`integrations/interop`](../../integrations/interop) asserts it again on a real
63
+ page.
64
+
65
+ Blocks still work the way you expect, because nothing else can express them:
66
+
67
+ ```tsx
68
+ const Button = styled.button<{ $on?: boolean }>`
69
+ padding: 1rem;
70
+ ${(props) =>
71
+ props.$on === true &&
72
+ css`
73
+ background: purple;
74
+ color: white;
75
+ `}
76
+ `;
77
+ ```
78
+
79
+ Two instances that resolve the same way share one rule, and switching back to a
80
+ resolution already in the sheet reuses it.
81
+
82
+ ## Nesting
83
+
84
+ `&:hover`, `@media`, `& > li` — all of it is **native CSS nesting**, which
85
+ every browser this framework supports has. There is no preprocessor in this
86
+ package, which is most of why it is 1.97 kB rather than 13.
87
+
88
+ ## Props
89
+
90
+ A prop reaches the element if the element has it: `disabled` does, `primary`
91
+ does not. `data-*`, `aria-*`, `role`, `style` and event handlers are forwarded
92
+ too, and a `$`-prefixed prop never is — styled-components' transient-prop
93
+ convention, and the reason it exists.
94
+
95
+ ```tsx
96
+ <Button $primary disabled data-role="save" onClick={save} />
97
+ // disabled, data-role and the handler land on the <button>; $primary does not.
98
+ ```
99
+
100
+ styled-components answers this question with a list of every valid HTML
101
+ attribute. The element already knows.
102
+
103
+ ## Theme
104
+
105
+ ```tsx
106
+ const theme = signal({ line: '#d8d8e0' });
107
+ provide(ThemeContext, theme); // a cell, so it can be swapped
108
+
109
+ const Panel = styled.section`
110
+ border: 1px solid ${(props) => props.theme.line};
111
+ `;
112
+ ```
113
+
114
+ A theme swap is one signal write. It updates the custom properties that read
115
+ the theme — not the components, and not the rules that never mentioned it.
116
+ `useTheme()` reads it directly.
117
+
118
+ ## The rest of the API
119
+
120
+ ```tsx
121
+ const spin = keyframes`
122
+ from { transform: rotate(0deg); }
123
+ to { transform: rotate(360deg); }
124
+ `;
125
+
126
+ const GlobalStyle = createGlobalStyle`
127
+ body { background: ${(props) => props.theme.background}; }
128
+ `;
129
+
130
+ const Fancy = styled(Link)`
131
+ color: rebeccapurple;
132
+ `;
133
+ ```
134
+
135
+ `styled(Component)` hands the class to the component as a `class` prop, and the
136
+ component is responsible for putting it on its root — the same contract
137
+ styled-components has with `className`. Every interpolation resolves into the
138
+ class there, because the element belongs to that component and there is nothing
139
+ to set a custom property on.
140
+
141
+ ## Deliberately missing
142
+
143
+ - **`.attrs()`** — write the default in the component instead.
144
+ - **`shouldForwardProp`** — the `$` convention and "does the element have it"
145
+ cover the cases it exists for.
146
+ - **A preprocessor.** No autoprefixing, no `&` rewriting: the browser does
147
+ nesting, and prefixes are for browsers this framework does not support.
148
+ - **SSR collection.** There is no server rendering to collect for.
149
+
150
+ ## Restyling, and a typed theme
151
+
152
+ A styled component can be styled again, to any depth. Both classes land on the
153
+ element and the **outer** declaration wins — each level repeats its class in
154
+ the selector, so specificity decides rather than which rule reached the sheet
155
+ first.
156
+
157
+ ```tsx
158
+ const Panel = styled.section`
159
+ padding: 1rem;
160
+ `;
161
+ const AccentPanel = styled(Panel)`
162
+ padding: 1.25rem;
163
+ `; // wins
164
+ ```
165
+
166
+ Declare the theme once and every interpolation is typed:
167
+
168
+ ```ts
169
+ // The empty import matters: a file may only augment a module it imports.
170
+ import type {} from '@firsthandjs/styled';
171
+
172
+ declare module '@firsthandjs/styled' {
173
+ interface FirsthandTheme {
174
+ background: string;
175
+ text: string;
176
+ }
177
+ }
178
+ ```
package/dist/css.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Reading a tagged template once, so that instances do not re-read it.
3
+ *
4
+ * A template is compiled the first time it is written, never per instance and
5
+ * never per update. What compilation decides is *where* each interpolation
6
+ * sits, because that decides what it costs at runtime:
7
+ *
8
+ * - a value position — `color: ${…};` — becomes a CSS custom property. Every
9
+ * instance shares one class, and a change is one `setProperty` call.
10
+ * - anything else — `${(p) => p.big && css`…`}` — is a block, and blocks are
11
+ * resolved into a class per distinct result, the way styled-components does
12
+ * it for everything.
13
+ *
14
+ * The split matters at scale: a thousand rows with a colour each produce one
15
+ * rule here and a thousand rules in a library that cannot tell the difference.
16
+ */
17
+ /** What a `css` template is before anything reads it. */
18
+ export interface CssFragment {
19
+ readonly strings: readonly string[];
20
+ readonly values: readonly unknown[];
21
+ }
22
+ /**
23
+ * A reusable piece of CSS.
24
+ *
25
+ * Composes into another template, and may be returned from an interpolation
26
+ * function — which is how a conditional block is written.
27
+ */
28
+ export declare function css(strings: TemplateStringsArray, ...values: unknown[]): CssFragment;
29
+ export declare function isFragment(value: unknown): value is CssFragment;
30
+ /** An interpolation that has to be evaluated per instance. */
31
+ export interface Slot {
32
+ /** `value` becomes a custom property; `block` becomes part of a class. */
33
+ readonly kind: 'value' | 'block';
34
+ /** The custom property's name, for a value slot. */
35
+ readonly property: string;
36
+ readonly fn: (props: Record<string, unknown>) => unknown;
37
+ }
38
+ export interface Compiled {
39
+ /**
40
+ * The template as literal text and block slots, in order.
41
+ *
42
+ * Value slots are already `var(…)` inside the literal chunks; a block slot
43
+ * appears as itself, because its text is only known per instance.
44
+ */
45
+ readonly chunks: readonly (string | Slot)[];
46
+ readonly slots: readonly Slot[];
47
+ /** True when nothing has to be resolved per instance. */
48
+ readonly static: boolean;
49
+ }
50
+ /**
51
+ * Flattens a fragment into text plus the slots that could not be resolved.
52
+ *
53
+ * Nested fragments are inlined, so a mixin costs nothing at runtime unless it
54
+ * contains a function of its own.
55
+ */
56
+ export declare function compile(fragment: CssFragment, id: string, values?: boolean): Compiled;
57
+ /** Joins the compiled chunks, asking for each block slot's text. */
58
+ export declare function join(compiled: Compiled, resolve: (slot: Slot) => string): string;
59
+ /** Resolves one block slot's value, including a nested fragment. */
60
+ export declare function blockText(value: unknown): string;
61
+ //# sourceMappingURL=css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACrC;AAID;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,WAAW,CAEpF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAE/D;AAED,8DAA8D;AAC9D,MAAM,WAAW,IAAI;IACnB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,QAAQ;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AA8BD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAO,GAAG,QAAQ,CAiDlF;AAED,oEAAoE;AACpE,wBAAgB,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,GAAG,MAAM,CAMhF;AAED,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAchD"}
@@ -0,0 +1,20 @@
1
+ import { type Component } from '@firsthandjs/dom';
2
+ import type { Interpolation } from './styled.js';
3
+ import { type Theme } from './theme.js';
4
+ /**
5
+ * A named animation, inserted once.
6
+ *
7
+ * Returns the name, so it reads the way it does in styled-components:
8
+ * `animation: ${spin} 1s linear infinite;`
9
+ */
10
+ export declare function keyframes(strings: TemplateStringsArray, ...values: unknown[]): string;
11
+ /**
12
+ * Styles for the document, attached for as long as the component lives.
13
+ *
14
+ * Interpolations may read the theme, and the rule is replaced when it changes.
15
+ * Everything else about it is global by definition: it is not scoped, and two
16
+ * instances of the same global style share one rule.
17
+ */
18
+ export declare function createGlobalStyle(strings: TemplateStringsArray, ...values: Interpolation<unknown>[]): Component<Record<string, never>>;
19
+ export type { Theme };
20
+ //# sourceMappingURL=global.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global.d.ts","sourceRoot":"","sources":["../src/global.ts"],"names":[],"mappings":"AAOA,OAAO,EAAa,KAAK,SAAS,EAAa,MAAM,kBAAkB,CAAC;AAExE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CASrF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,EAG7B,GAAG,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GAClC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAoClC;AAED,YAAY,EAAE,KAAK,EAAE,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `@firsthandjs/styled` — styled-components' API, on a framework that renders once.
3
+ *
4
+ * ```tsx
5
+ * const Button = styled.button<{ $primary?: boolean }>`
6
+ * padding: 0.5rem 1rem;
7
+ * border: 1px solid ${(p) => p.theme.line};
8
+ * background: ${(p) => (p.$primary ? 'rebeccapurple' : 'transparent')};
9
+ * &:hover {
10
+ * filter: brightness(1.2);
11
+ * }
12
+ * `;
13
+ *
14
+ * <Button $primary onClick={save}>Save</Button>;
15
+ * ```
16
+ *
17
+ * The difference from styled-components is what a prop change costs. There, a
18
+ * new prop value means a new class, inserted into the sheet and swapped on the
19
+ * element — a thousand rows with a colour each produce a thousand rules. Here,
20
+ * an interpolation that sits in a declaration's value becomes a CSS custom
21
+ * property: one rule for the component, and one `setProperty` per change, for
22
+ * any number of instances.
23
+ *
24
+ * Interpolations that produce whole declarations still resolve to a class per
25
+ * distinct result, because nothing else can express them — that is the same
26
+ * mechanism, kept for the cases that need it.
27
+ *
28
+ * Nesting (`&:hover`, `@media`) is the browser's own CSS nesting. There is no
29
+ * preprocessor in this package, which is most of why it is 1 kB.
30
+ */
31
+ export { styled } from './styled.js';
32
+ export type { ElementProps, Interpolation, StyledFactory, StyledProps } from './styled.js';
33
+ export { css, isFragment } from './css.js';
34
+ export type { CssFragment } from './css.js';
35
+ export { keyframes, createGlobalStyle } from './global.js';
36
+ export { ThemeContext, useTheme } from './theme.js';
37
+ export type { Theme } from './theme.js';
38
+ /**
39
+ * Your application's theme, declared by you.
40
+ *
41
+ * Empty here. An application describes its own:
42
+ *
43
+ * ```ts
44
+ * declare module '@firsthandjs/styled' {
45
+ * interface FirsthandTheme {
46
+ * background: string;
47
+ * text: string;
48
+ * }
49
+ * }
50
+ * ```
51
+ *
52
+ * and every interpolation then reads `props.theme.background` with a type
53
+ * rather than `props.theme['background']` with a cast. A project that declares
54
+ * nothing keeps the indexed form, so this costs nothing to ignore.
55
+ *
56
+ * It is declared here, in the entry module, because that is the module an
57
+ * application augments.
58
+ */
59
+ export interface FirsthandTheme {
60
+ }
61
+ export { hash, reset as resetStyles } from './sheet.js';
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,cAAc;CAAG;AAClC,OAAO,EAAE,IAAI,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{bind as j,signal as z,useContext as Y}from"@firsthandjs/core";import{component as _}from"@firsthandjs/dom";import{applyProp as D,insert as K}from"@firsthandjs/dom/internal";var I=Symbol("firsthand.css");function x(e,...t){return{[I]:!0,strings:[...e],values:t}}function $(e){return typeof e=="object"&&e!==null&&I in e}function B(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(n===":")return!0;if(n===";"||n==="{"||n==="}")return!1}return!1}function M(e){return e==null||e===!1||e===!0?"":String(e)}function C(e,t,n=!0){let o=[],s=[],a="",c=()=>{a!==""&&(s.push(a),a="")},g=p=>{for(let[h,b]of p.strings.entries()){if(a+=b,h>=p.values.length)continue;let m=p.values[h];if($(m)){g(m);continue}if(typeof m!="function"){a+=M(m);continue}let i=o.length,v=n&&B(a)?"value":"block",T={kind:v,property:`--${t}-${String(i)}`,fn:m};o.push(T),v==="value"?a+=`var(${T.property})`:(c(),s.push(T))}};return g(e),c(),{chunks:s,slots:o,static:o.length===0}}function k(e,t){let n="";for(let o of e.chunks)n+=typeof o=="string"?o:t(o);return n}function F(e){if($(e)){let t=C(e,"x");if(!t.static)throw new Error("A css`` fragment returned from an interpolation must not contain further functions.");return k(t,()=>"")}return M(e)}var P=new Map,d=null;function y(e){let t=2166136261;for(let n=0;n<e.length;n++)t^=e.charCodeAt(n),t=Math.imul(t,16777619);return(t>>>0).toString(36)}function H(){return d!==null&&d.isConnected||(d=document.createElement("style"),d.setAttribute("data-firsthand-styled",""),document.head.appendChild(d)),d}function w(e,t){let n=P.get(e);if(n!==void 0)return n.count++,!1;let o=document.createTextNode(t);return P.set(e,{node:o,count:1}),H().appendChild(o),!0}function O(e){let t=P.get(e);t!==void 0&&(--t.count>0||(P.delete(e),t.node.remove()))}function J(){P.clear(),d?.remove(),d=null}import{createContext as L,useContext as N}from"@firsthandjs/core";var X=Object.freeze({}),S=L(X,"theme");function q(){return N(S)}function Q(e,t){return t==="children"||t==="class"||t.startsWith("$")?!1:t in e||t.startsWith("data-")||t.startsWith("aria-")||t.startsWith("on")||t==="style"||t==="role"}function U(e,t){let n={};for(let o of Object.keys(e))Object.defineProperty(n,o,{enumerable:!0,get:()=>e[o]});return Object.defineProperty(n,"theme",{enumerable:!0,get:()=>t.value}),n}var Z=0,V=new WeakMap;function W(e,t){let n=typeof e=="string"?e:null,o=n===null?(V.get(e)??0)+1:0,s=`s${y(t.strings.join("|"))}${String(++Z)}`,a=i=>`.${i}`.repeat(o+1),c=C(t,s,n!==null),g=k(c,()=>""),p=`${s}-${y(g)}`,h=c.slots.some(i=>i.kind==="block");h||w(p,`${a(p)}{${g}}`);let m=_(i=>{let v=Y(S),T=U(i,v),f=n===null?null:document.createElement(n),R="",E=f===null?z(""):null,A=r=>{let l=i.class,u=`${r}${typeof l=="string"&&l!==""?` ${l}`:""}`;u!==R&&(R=u,f===null?E.value=u:f.className=u)};h?j(()=>{let r=k(c,u=>F(u.fn(T))),l=`${s}-${y(r)}`;w(l,`${a(l)}{${r}}`),A(l)}):j(()=>{A(p)});for(let r of c.slots){if(r.kind!=="value")continue;let l=f;j(()=>{let u=r.fn(T),G=u==null||u===!1?"":String(u);l.style.setProperty(r.property,G)})}if(f===null){let r={};for(let l of Object.keys(i))l!=="class"&&Object.defineProperty(r,l,{enumerable:!0,get:()=>i[l]});return Object.defineProperty(r,"class",{enumerable:!0,get:()=>E.value}),e(r)}for(let r of Object.keys(i))Q(f,r)&&j(()=>{D(f,r,i[r])});return K(f,()=>i.children),f},void 0,`firsthand/styled:${s}`,n===null?`Styled(${e.name})`:`Styled(${n})`);return V.set(m,o),m}var ee=new Proxy((e=>(t,...n)=>W(e,x(t,...n))),{get:(e,t)=>(n,...o)=>W(t,x(n,...o))});import{bind as te,onCleanup as ne,useContext as oe}from"@firsthandjs/core";import{component as re}from"@firsthandjs/dom";function se(e,...t){let n=C(x(e,...t),"k",!1);if(!n.static)throw new Error("keyframes`` cannot interpolate functions: an animation has no props.");let o=k(n,()=>""),s=`k${y(o)}`;return w(s,`@keyframes ${s}{${o}}`),s}function ie(e,...t){let n=x(e,...t),o=C(n,"g",!1);return re(()=>{let a=oe(S),c=null,g=()=>{c!==null&&(O(c),c=null)};return te(()=>{let p={theme:a.value},h=k(o,m=>F(m.fn(p))),b=`g${y(h)}`;g(),w(b,h),c=b}),ne(g),null},void 0,`firsthand/styled:global${y(e.join("|"))}`,"GlobalStyle")}export{S as ThemeContext,ie as createGlobalStyle,x as css,y as hash,$ as isFragment,se as keyframes,J as resetStyles,ee as styled,q as useTheme};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One stylesheet, one rule per distinct piece of CSS.
3
+ *
4
+ * Every rule this package emits is keyed by a hash of its own text, so the
5
+ * same styles written by a hundred component instances are inserted once. The
6
+ * sheet is append-only for as long as anything uses a rule, and each rule is
7
+ * remembered as the node it became — so removing one is a reference, not a
8
+ * search through the sheet for matching text.
9
+ */
10
+ /**
11
+ * A short, stable name for a piece of CSS.
12
+ *
13
+ * FNV-1a: four lines, no dependency, and a collision would mean two different
14
+ * rules hashing the same — at which point the second is simply not inserted,
15
+ * which is visible immediately rather than subtly wrong.
16
+ */
17
+ export declare function hash(text: string): string;
18
+ /** Inserts a rule unless its key is already there. Returns whether it was new. */
19
+ export declare function insert(key: string, rule: string): boolean;
20
+ /** Removes a rule again, for `createGlobalStyle`'s disposal. */
21
+ export declare function remove(key: string): void;
22
+ /** Drops everything. For tests, and for a hot reload that wants a clean slate. */
23
+ export declare function reset(): void;
24
+ //# sourceMappingURL=sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sheet.d.ts","sourceRoot":"","sources":["../src/sheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOzC;AAYD,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,gEAAgE;AAChE,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAYxC;AAED,kFAAkF;AAClF,wBAAgB,KAAK,IAAI,IAAI,CAI5B"}
@@ -0,0 +1,32 @@
1
+ import { type Component } from '@firsthandjs/dom';
2
+ import { isFragment, type CssFragment } from './css.js';
3
+ import { type Theme } from './theme.js';
4
+ /** Props a styled element accepts beyond the ones you declare. */
5
+ export type ElementProps<T extends string> = T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : Record<string, unknown>;
6
+ /** What an interpolation function is given: the props, plus the theme. */
7
+ export type StyledProps<P> = P & {
8
+ readonly theme: Theme;
9
+ };
10
+ export type Interpolation<P> = string | number | CssFragment | ((props: StyledProps<P>) => unknown);
11
+ export interface StyledFactory<Base> {
12
+ /**
13
+ * `P` is what you add: `styled.button<{ $primary?: boolean }>`.
14
+ *
15
+ * It defaults to `unknown` rather than an empty record, because an empty
16
+ * record's index signature would type every other prop — `children`
17
+ * included — as `never`.
18
+ */
19
+ <P = unknown>(strings: TemplateStringsArray, ...values: Interpolation<P & Base>[]): Component<P & Base>;
20
+ }
21
+ type Tags = {
22
+ [T in keyof JSX.IntrinsicElements]: StyledFactory<ElementProps<T>>;
23
+ };
24
+ /**
25
+ * `styled.div`, `styled.button`, … and `styled(Component)`.
26
+ *
27
+ * The tag list is a proxy rather than a hand-written map: every element the
28
+ * JSX namespace knows is available, and the package stays the same size.
29
+ */
30
+ export declare const styled: Tags & (<P>(base: Component<P>) => StyledFactory<P>);
31
+ export { isFragment };
32
+ //# sourceMappingURL=styled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styled.d.ts","sourceRoot":"","sources":["../src/styled.ts"],"names":[],"mappings":"AAUA,OAAO,EAAa,KAAK,SAAS,EAAa,MAAM,kBAAkB,CAAC;AAKxE,OAAO,EAA2B,UAAU,EAAQ,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvF,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAEtD,kEAAkE;AAClE,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,MAAM,GAAG,CAAC,iBAAiB,GAC9E,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,0EAA0E;AAC1E,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC;AAEpG,MAAM,WAAW,aAAa,CAAC,IAAI;IACjC;;;;;;OAMG;IACH,CAAC,CAAC,GAAG,OAAO,EACV,OAAO,EAAE,oBAAoB,EAC7B,GAAG,MAAM,EAAE,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GACnC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACxB;AAsMD,KAAK,IAAI,GAAG;KACT,CAAC,IAAI,MAAM,GAAG,CAAC,iBAAiB,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;CACnE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,MAAM,WAId,CAAC,QAAQ,SAAS,CAAC,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,CAAC,CAQ5C,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The theme, as an ordinary Firsthand context.
3
+ *
4
+ * Nothing about it is special: it is a context holding an object, so a theme
5
+ * swap is one signal write, and a styled component that reads `theme` updates
6
+ * the custom properties that depend on it — not the component, and not the
7
+ * rules that never mentioned the theme.
8
+ */
9
+ import { type Context, type ReadonlyCell } from '@firsthandjs/core';
10
+ import type { FirsthandTheme } from './index.js';
11
+ /**
12
+ * What an interpolation is handed as `props.theme`.
13
+ *
14
+ * The declared theme once there is one, and an indexable object until then.
15
+ * `keyof` an empty interface is `never`, which is how that is detected.
16
+ */
17
+ export type Theme = [keyof FirsthandTheme] extends [never] ? Readonly<Record<string, unknown>> : Readonly<FirsthandTheme>;
18
+ /**
19
+ * Provide it with `provide(ThemeContext, myTheme)` — or a cell, to swap it.
20
+ *
21
+ * Annotated rather than inferred: an inferred type is *resolved* into the
22
+ * declaration file, and `Theme` would be frozen there as the empty-theme
23
+ * branch — so an application's `declare module` augmentation would change
24
+ * nothing. Written out, the conditional stays a conditional for whoever
25
+ * imports it.
26
+ */
27
+ export declare const ThemeContext: Context<Theme>;
28
+ /** The theme above this component, as a cell. */
29
+ export declare function useTheme(): ReadonlyCell<Theme>;
30
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAA6B,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAI/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC,GACtD,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAEjC,QAAQ,CAAC,cAAc,CAAC,CAAC;AAI7B;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,KAAK,CAAwC,CAAC;AAEjF,iDAAiD;AACjD,wBAAgB,QAAQ,IAAI,YAAY,CAAC,KAAK,CAAC,CAE9C"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@firsthandjs/styled",
3
+ "version": "0.1.0",
4
+ "description": "CSS-in-JS for Firsthand: tagged templates, themes, and fine-grained style updates.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "types": "./dist/index.d.ts",
15
+ "main": "./dist/index.js",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "dependencies": {
22
+ "@firsthandjs/core": "0.1.0",
23
+ "@firsthandjs/dom": "0.1.0",
24
+ "@firsthandjs/jsx-runtime": "0.1.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=20.11.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "provenance": true
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/firsthandjs/firsthand.git",
36
+ "directory": "packages/styled"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/firsthandjs/firsthand/issues"
40
+ },
41
+ "homepage": "https://github.com/firsthandjs/firsthand#readme",
42
+ "keywords": [
43
+ "firsthand",
44
+ "styled-components",
45
+ "css-in-js",
46
+ "styling",
47
+ "theme"
48
+ ]
49
+ }