@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 +21 -0
- package/README.md +178 -0
- package/dist/css.d.ts +61 -0
- package/dist/css.d.ts.map +1 -0
- package/dist/global.d.ts +20 -0
- package/dist/global.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/sheet.d.ts +24 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/styled.d.ts +32 -0
- package/dist/styled.d.ts.map +1 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.d.ts.map +1 -0
- package/package.json +49 -0
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"}
|
package/dist/global.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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};
|
package/dist/sheet.d.ts
ADDED
|
@@ -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"}
|
package/dist/styled.d.ts
ADDED
|
@@ -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"}
|
package/dist/theme.d.ts
ADDED
|
@@ -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
|
+
}
|