@neomorph/sdk 0.1.0 β 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -143
- package/dist/index.js +1 -1
- package/dist/loomer.d.ts +9 -0
- package/dist/types.d.ts +1 -1
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -1,189 +1,149 @@
|
|
|
1
1
|
# @neomorph/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **headless, framework-agnostic** core of Neomorph. Use it to add live CSS-variable theming to your own product β load a target app, read its theme, write a new one.
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
[](https://opensource.org/licenses/ISC)
|
|
5
|
+
The SDK has no UI of its own. [Neomorph Studio](../studio) is a visual designer built on top of this SDK; this package is what you reach for when you want to build that experience into your own app instead.
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## What's in the box
|
|
9
8
|
|
|
10
|
-
The
|
|
9
|
+
The SDK exports two classes:
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
| Class | Side | Responsibility |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `Loomer` | Designer side (parent window) | Loads a target app in an iframe and drives it: scrape variables, apply a theme, reset, configure, tear down. |
|
|
14
|
+
| `Weaver` | Designer side | One helper β injects the Weaver script into a page from the CDN. |
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
βββββββββββββββββββ βββββββββββββββββββ
|
|
16
|
-
β Your Applicationββββββ Neomorph Bridge ββββββ Theme Designer β
|
|
17
|
-
βββββββββββββββββββ€ βββββββββββββββββββ€
|
|
18
|
-
β CSS Variables β β Custom UI β
|
|
19
|
-
β β’ --primary β β β’ Color Pickers β
|
|
20
|
-
β β’ --font-size β β β’ Sliders β
|
|
21
|
-
β β’ --border β β β’ Input Fields β
|
|
22
|
-
βββββββββββββββββββ βββββββββββββββββββ
|
|
23
|
-
```
|
|
16
|
+
> **Naming note:** `Loomer` is the *controller class*. "Studio" is the separate ready-made *app*. You use `Loomer`; Studio uses it too.
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
The target app itself runs the [`@neomorph/weaver`](../weaver) script β that's a separate package, loaded inside the app being themed, not imported here.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
26
21
|
|
|
27
22
|
```bash
|
|
28
23
|
npm install @neomorph/sdk
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
### Prerequisites
|
|
34
|
-
|
|
35
|
-
Your application must use CSS custom properties for theming:
|
|
36
|
-
|
|
37
|
-
```css
|
|
38
|
-
:root {
|
|
39
|
-
--primary-color: #3498db;
|
|
40
|
-
--secondary-color: #2ecc71;
|
|
41
|
-
--background-color: #ffffff;
|
|
42
|
-
--text-color: #333333;
|
|
43
|
-
--border-radius: 4px;
|
|
44
|
-
--font-size-base: 16px;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.button {
|
|
48
|
-
background-color: var(--primary-color);
|
|
49
|
-
color: var(--text-color);
|
|
50
|
-
border-radius: var(--border-radius);
|
|
51
|
-
font-size: var(--font-size-base);
|
|
52
|
-
}
|
|
53
|
-
```
|
|
26
|
+
Or load the CDN build, which exposes `window.Neomorph.Loomer` and `window.Neomorph.Weaver`.
|
|
54
27
|
|
|
55
|
-
##
|
|
28
|
+
## Prerequisites
|
|
56
29
|
|
|
57
|
-
|
|
30
|
+
The target app must theme itself with CSS custom properties (`--color-primary`, etc.) and must have the Weaver script loaded. See the [root README](../../README.md) for the full picture.
|
|
58
31
|
|
|
59
|
-
|
|
32
|
+
## Usage
|
|
60
33
|
|
|
61
|
-
|
|
34
|
+
### Load a target app and read its theme
|
|
62
35
|
|
|
63
|
-
```
|
|
64
|
-
import { Loomer
|
|
36
|
+
```ts
|
|
37
|
+
import { Loomer } from '@neomorph/sdk';
|
|
65
38
|
|
|
66
|
-
// Initialize Loomer for theme designer communication
|
|
67
39
|
const loomer = new Loomer();
|
|
68
40
|
|
|
69
|
-
//
|
|
70
|
-
loomer.loadApplication('https://your-app.com', (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// payload: { action: "listenCssVariables", ...cssData }
|
|
79
|
-
// }
|
|
80
|
-
|
|
81
|
-
// YOU process this data and build UI
|
|
82
|
-
buildYourThemeDesigner(response);
|
|
83
|
-
}
|
|
41
|
+
// Creates an iframe for the target app inside the given container
|
|
42
|
+
loomer.loadApplication('https://your-app.com', document.getElementById('preview'));
|
|
43
|
+
|
|
44
|
+
// Ask the app for its CSS variables. The callback fires with the scraped
|
|
45
|
+
// data, and again every time the app's stylesheets change.
|
|
46
|
+
loomer.listenCssVariables((variables) => {
|
|
47
|
+
console.log('current theme:', variables);
|
|
48
|
+
// variables is keyed by host ("document", shadow-root tag names),
|
|
49
|
+
// then by CSS selector, then a list of { property, value } pairs.
|
|
84
50
|
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Messages are queued until the iframe finishes loading, so you can call these
|
|
54
|
+
immediately after `loadApplication` without waiting.
|
|
55
|
+
|
|
56
|
+
### Apply a theme
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
loomer.applyCssVariables(
|
|
60
|
+
{
|
|
61
|
+
document: {
|
|
62
|
+
'--color-primary': '#e11d48',
|
|
63
|
+
'--color-bg': '#0f172a'
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
/* persist */ true
|
|
67
|
+
);
|
|
68
|
+
```
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
70
|
+
The outer key is the host (`'document'` or a shadow-root host's tag name). Pass
|
|
71
|
+
`persist: true` to save the theme to the target app's `localStorage` so it
|
|
72
|
+
survives reloads.
|
|
90
73
|
|
|
91
|
-
|
|
92
|
-
function buildYourThemeDesigner(response: SDKResponse) {
|
|
93
|
-
// Extract data from the response payload
|
|
94
|
-
const cssData = response.payload;
|
|
74
|
+
### Reset, configure, tear down
|
|
95
75
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
```ts
|
|
77
|
+
loomer.clearTheme(); // remove overrides + clear persisted theme
|
|
78
|
+
loomer.configure({ debounceMs: 500 }); // tune Weaver's runtime behavior
|
|
79
|
+
loomer.teardown(); // disconnect observers + listeners
|
|
100
80
|
```
|
|
101
81
|
|
|
102
|
-
|
|
82
|
+
### Inject the Weaver script programmatically
|
|
103
83
|
|
|
104
|
-
|
|
105
|
-
import { Loomer, type SDKResponse } from '@neomorph/sdk';
|
|
84
|
+
If you control the target page, you can inject Weaver instead of adding a `<script>` tag by hand:
|
|
106
85
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
container: document.getElementById('preview-container'),
|
|
86
|
+
```ts
|
|
87
|
+
import { Weaver } from '@neomorph/sdk';
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
|
|
89
|
+
Weaver.inject(); // appends the Weaver CDN script to document.head
|
|
90
|
+
```
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
debug: true
|
|
116
|
-
});
|
|
92
|
+
## API reference
|
|
117
93
|
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
await loomer.loadApplication('https://your-app.com', {
|
|
121
|
-
onLoad: () => console.log('Application loaded'),
|
|
122
|
-
onError: (error) => console.error('Load failed:', error),
|
|
123
|
-
onThemeChange: (variables) => console.log('Theme applied:', variables)
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Request available CSS variables
|
|
127
|
-
const response = await loomer.getCssVariables();
|
|
128
|
-
|
|
129
|
-
if (response.success) {
|
|
130
|
-
const { cssVariables, metadata } = response.data;
|
|
131
|
-
|
|
132
|
-
// Process and group the JSON data
|
|
133
|
-
const groupedVars = groupVariablesByCategory(cssVariables);
|
|
134
|
-
|
|
135
|
-
// Build your custom theme designer UI with the data
|
|
136
|
-
buildYourThemeDesignerUI(groupedVars);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error('Integration failed:', error);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Batch update multiple variables
|
|
144
|
-
function applyTheme(theme: Record<string, string>) {
|
|
145
|
-
Object.entries(theme).forEach(([variable, value]) => {
|
|
146
|
-
loomer.updateCssVariable(variable, value);
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
```
|
|
94
|
+
### `class Loomer`
|
|
150
95
|
|
|
151
|
-
|
|
96
|
+
| Method | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `loadApplication(url, container?)` | Create an iframe for `url` inside `container` (defaults to `document.body`). |
|
|
99
|
+
| `listenCssVariables(callback)` | Scrape the target's CSS variables; `callback` fires now and on every later change. |
|
|
100
|
+
| `applyCssVariables(variables, persist?)` | Apply CSS variable overrides. `persist` saves them to the target's `localStorage`. |
|
|
101
|
+
| `clearTheme()` | Remove all applied overrides and clear the persisted theme. |
|
|
102
|
+
| `configure(config)` | Update Weaver's runtime config (e.g. `debounceMs`, `hostFilter`). |
|
|
103
|
+
| `teardown()` | Disconnect Weaver's observers and listeners in the target app. |
|
|
152
104
|
|
|
153
|
-
|
|
154
|
-
- **CSS Variable Exposure**: Only explicitly exposed variables are accessible via Weaver
|
|
155
|
-
- **Iframe Sandboxing**: Consider appropriate iframe sandbox attributes for security
|
|
156
|
-
- **Input Validation**: Always validate CSS variable values before applying
|
|
105
|
+
### `class Weaver`
|
|
157
106
|
|
|
158
|
-
|
|
107
|
+
| Method | Description |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `Weaver.inject(version?)` | Append the Weaver script (from jsDelivr) to `document.head`. Defaults to the latest pinned version. |
|
|
159
110
|
|
|
160
|
-
|
|
111
|
+
## How communication works
|
|
161
112
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
113
|
+
`Loomer` and the in-app Weaver script talk over the `postMessage` API. Every
|
|
114
|
+
message is JSON with a `skinweaver` service identifier and a `requestId`.
|
|
115
|
+
`Loomer` queues outgoing messages until the iframe's `load` event fires, then
|
|
116
|
+
flushes them β so ordering and timing are handled for you:
|
|
166
117
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
118
|
+
```mermaid
|
|
119
|
+
sequenceDiagram
|
|
120
|
+
participant App as Your code
|
|
121
|
+
participant L as Loomer (SDK)
|
|
122
|
+
participant I as Target iframe
|
|
123
|
+
participant W as Weaver
|
|
171
124
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
125
|
+
App->>L: loadApplication(url, container)
|
|
126
|
+
L->>I: create iframe
|
|
127
|
+
App->>L: listenCssVariables(cb)
|
|
128
|
+
Note over L: iframe not ready β<br/>message is queued
|
|
176
129
|
|
|
177
|
-
|
|
130
|
+
I-->>L: iframe "load" event
|
|
131
|
+
L->>W: flush queued messages
|
|
178
132
|
|
|
179
|
-
|
|
133
|
+
W-->>L: scraped CSS variables
|
|
134
|
+
L->>App: cb(variables)
|
|
180
135
|
|
|
181
|
-
|
|
136
|
+
App->>L: applyCssVariables(overrides)
|
|
137
|
+
L->>W: applyCssVariables
|
|
138
|
+
W-->>L: applied β
|
|
139
|
+
```
|
|
182
140
|
|
|
183
|
-
|
|
141
|
+
## Security notes
|
|
184
142
|
|
|
185
|
-
|
|
143
|
+
- **Origins** β messages are validated by service identifier. When embedding untrusted apps, also apply iframe `sandbox` attributes.
|
|
144
|
+
- **Exposure** β only CSS variables actually present in the target's stylesheets are visible; Weaver does not expose anything else.
|
|
145
|
+
- **Validation** β validate CSS values before calling `applyCssVariables` if they come from user input.
|
|
186
146
|
|
|
187
|
-
|
|
147
|
+
## License
|
|
188
148
|
|
|
189
|
-
|
|
149
|
+
ISC β see [LICENSE](../../LICENSE).
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){return"string"==typeof e?e:null}function
|
|
1
|
+
function e(e){return"string"==typeof e?e:null}function s(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}const a=function(a){if(s(a)){const t=e(a.requestId),n=e(a.service),i=function(e){if(s(e))return{...e};return null}(a.payload);if(null!==t&&null!==n&&null!==i)return{requestId:t,service:n,payload:i}}return null};class t{applicationFrame;callbacks;ready;pendingMessages;constructor(){this.applicationFrame=null,this.callbacks=[],this.ready=!1,this.pendingMessages=[]}loadApplication(e,s=document.body){try{this.applicationFrame=document.createElement("iframe"),this.applicationFrame.src=e,this.applicationFrame.style.width="100%",this.applicationFrame.style.height="100%",this.applicationFrame.style.border="none",s.appendChild(this.applicationFrame),this.applicationFrame.addEventListener("load",()=>{this.ready=!0,this.flushPendingMessages()}),window.addEventListener("message",e=>{const s="string"==typeof e.data?function(e){try{return JSON.parse(e)}catch{return null}}(e.data):e.data,t=a(s);null!==t&&("action"in t.payload||this.callbacks.forEach(e=>e(t.payload)))})}catch(e){console.error("Loomer: Failed to load application:",e)}}flushPendingMessages(){for(const e of this.pendingMessages)this.postMessage(e);this.pendingMessages=[]}postMessage(e){try{if(null===this.applicationFrame||null===this.applicationFrame.contentWindow)return;this.applicationFrame.contentWindow.postMessage(JSON.stringify({source:"skinweaver",payload:JSON.stringify({requestId:crypto.randomUUID(),service:"skinweaver",payload:e})}),"*")}catch(e){console.error("Loomer: Failed to send message to iframe:",e)}}sendMessage(e){this.ready?this.postMessage(e):this.pendingMessages.push(e)}listenCssVariables(e){this.sendMessage({action:"listenCssVariables"}),this.callbacks.push(e)}applyCssVariables(e,s){this.sendMessage({action:"applyCssVariables",variables:e,persist:s})}clearTheme(){this.sendMessage({action:"clearTheme"})}configure(e){this.sendMessage({action:"configure",config:e})}teardown(){this.sendMessage({action:"teardown"})}}class n{static inject(e="1.0.0"){if("undefined"!=typeof document)try{const s=document.createElement("script");s.src=`https://cdn.jsdelivr.net/gh/sinha-sahil/neomorph/build/weaver/${e}/index.js`,s.type="text/javascript",document.head.appendChild(s)}catch(e){console.error("πΈοΈ Weaver: Failed to inject weaver script:",e)}else console.warn("πΈοΈ Weaver: inject() called in non-browser environment")}}export{t as Loomer,n as Weaver};
|
package/dist/loomer.d.ts
CHANGED
|
@@ -2,7 +2,16 @@ import { StylesCallback } from "./types";
|
|
|
2
2
|
export declare class Loomer {
|
|
3
3
|
private applicationFrame;
|
|
4
4
|
private callbacks;
|
|
5
|
+
private ready;
|
|
6
|
+
private pendingMessages;
|
|
5
7
|
constructor();
|
|
6
8
|
loadApplication(url: string, container?: HTMLElement): void;
|
|
9
|
+
private flushPendingMessages;
|
|
10
|
+
private postMessage;
|
|
11
|
+
private sendMessage;
|
|
7
12
|
listenCssVariables(callback: StylesCallback): void;
|
|
13
|
+
applyCssVariables(variables: Record<string, Record<string, string>>, persist?: boolean): void;
|
|
14
|
+
clearTheme(): void;
|
|
15
|
+
configure(config: Record<string, unknown>): void;
|
|
16
|
+
teardown(): void;
|
|
8
17
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type SDKPayload = {
|
|
|
15
15
|
};
|
|
16
16
|
export type SDKPayloadPayload = Record<string, unknown>;
|
|
17
17
|
export type SDKResponse = SDKPayload;
|
|
18
|
-
export type StylesCallback = (val:
|
|
18
|
+
export type StylesCallback = (val: SDKPayloadPayload) => void;
|
|
19
19
|
export declare function decodeCssProperty(rawInput: unknown): CSSProperty | null;
|
|
20
20
|
export declare function decodeSDKPayload(rawInput: unknown): SDKPayload | null;
|
|
21
21
|
export declare function decodeSDKPayloadPayload(rawInput: unknown): SDKPayloadPayload | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neomorph/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Core SDK for Neomorph - Advanced theme generator toolkit for web applications",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -15,14 +15,6 @@
|
|
|
15
15
|
"dist/",
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"dev": "rollup -c rollup.config.js --watch",
|
|
20
|
-
"build": "rollup --config rollup.config.js",
|
|
21
|
-
"build:cdn": "rollup --config rollup.config.js --environment buildType=cdn",
|
|
22
|
-
"format": "npx prettier --write .",
|
|
23
|
-
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
|
24
|
-
"clean": "rm -rf dist"
|
|
25
|
-
},
|
|
26
18
|
"keywords": [
|
|
27
19
|
"neomorph",
|
|
28
20
|
"theme",
|
|
@@ -47,9 +39,8 @@
|
|
|
47
39
|
"publishConfig": {
|
|
48
40
|
"access": "public"
|
|
49
41
|
},
|
|
50
|
-
"packageManager": "pnpm@10.18.0",
|
|
51
42
|
"dependencies": {
|
|
52
|
-
"type-decoder": "^2.1
|
|
43
|
+
"type-decoder": "^2.3.1"
|
|
53
44
|
},
|
|
54
45
|
"devDependencies": {
|
|
55
46
|
"@eslint/eslintrc": "^3.3.1",
|
|
@@ -65,7 +56,17 @@
|
|
|
65
56
|
"eslint-config-prettier": "^10.1.8",
|
|
66
57
|
"prettier": "^3.6.2",
|
|
67
58
|
"rollup": "^4.52.0",
|
|
59
|
+
"rollup-plugin-serve": "^3.0.0",
|
|
68
60
|
"tslib": "^2.8.1",
|
|
69
61
|
"typescript": "^5.9.2"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "rollup -c rollup.config.js --watch",
|
|
65
|
+
"dev:cdn": "rollup -c rollup.config.js --watch --environment buildType=cdn",
|
|
66
|
+
"build": "rollup --config rollup.config.js",
|
|
67
|
+
"build:cdn": "rollup --config rollup.config.js --environment buildType=cdn",
|
|
68
|
+
"format": "npx prettier --write .",
|
|
69
|
+
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
|
70
|
+
"clean": "rm -rf dist"
|
|
70
71
|
}
|
|
71
|
-
}
|
|
72
|
+
}
|