@neomorph/sdk 0.0.1 β†’ 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 CHANGED
@@ -1,189 +1,149 @@
1
1
  # @neomorph/sdk
2
2
 
3
- Advanced theme transformation SDK for web applications. Enables dynamic theme customization through CSS custom properties with cross-origin communication support.
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
- [![npm version](https://badge.fury.io/js/@neomorph%2Fsdk.svg)](https://www.npmjs.com/package/@neomorph/sdk)
6
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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
- ## 🎯 Overview
7
+ ## What's in the box
9
8
 
10
- The Neomorph SDK provides communication tools for building theme designer interfaces. It enables theme designers to retrieve CSS variable data from target applications and build custom theme customization UIs.
9
+ The SDK exports two classes:
11
10
 
12
- ## πŸ—οΈ Architecture
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
- ## πŸ“¦ Installation
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
- ## πŸš€ Quick Start
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
- ## πŸ“– Use Cases
28
+ ## Prerequisites
56
29
 
57
- ### Loomer Integration (Theme Designer Side)
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
- Use the Loomer class to communicate with applications and retrieve CSS variable data. The SDK provides the data and communication layer - you build the actual theme designer UI based on the returned JSON data.
32
+ ## Usage
60
33
 
61
- #### Basic Data Retrieval
34
+ ### Load a target app and read its theme
62
35
 
63
- ```typescript
64
- import { Loomer, type SDKResponse } from '@neomorph/sdk';
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
- // Load target application in iframe with callback
70
- loomer.loadApplication('https://your-app.com', (response: SDKResponse | null) => {
71
- if (response) {
72
- console.log('Received data:', response);
73
-
74
- // Actual response structure:
75
- // {
76
- // requestId: "randomId",
77
- // service: "skinweaver",
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
- // Request CSS variables from the loaded application
87
- function startListening() {
88
- loomer.listenCssVariables(); // Sends PostMessage to iframe
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
- // Example: Build your own theme designer UI
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
- // Create your own UI based on the received data
97
- // YOU implement the color pickers, sliders, inputs, etc.
98
- createCustomThemeControls(cssData);
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
- #### Advanced Data Retrieval with Configuration
82
+ ### Inject the Weaver script programmatically
103
83
 
104
- ```typescript
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
- const loomer = new Loomer({
108
- // Custom iframe container
109
- container: document.getElementById('preview-container'),
86
+ ```ts
87
+ import { Weaver } from '@neomorph/sdk';
110
88
 
111
- // Communication timeout
112
- timeout: 5000,
89
+ Weaver.inject(); // appends the Weaver CDN script to document.head
90
+ ```
113
91
 
114
- // Enable debugging
115
- debug: true
116
- });
92
+ ## API reference
117
93
 
118
- // Load application with error handling
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
- ## πŸ”’ Security Considerations
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
- - **Cross-Origin Communication**: All PostMessage communication includes origin validation
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
- ## πŸ› Troubleshooting
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
- ### Common Issues
111
+ ## How communication works
161
112
 
162
- **PostMessage not working**
163
- - Ensure both applications are served over HTTPS (or both over HTTP in development)
164
- - Check that iframe src and parent window origins are correctly configured
165
- - Verify that Weaver script is loaded before Loomer attempts communication
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
- **CSS Variables not detected**
168
- - Confirm variables are defined in `:root` or explicitly exposed via Weaver options
169
- - Check browser DevTools for CSS custom property support
170
- - Ensure variables use the `--` prefix
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
- **Cross-origin errors**
173
- - Verify CORS headers are properly configured on target application
174
- - Use appropriate iframe sandbox attributes
175
- - Consider using a proxy for local development
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
- ## 🀝 Contributing
130
+ I-->>L: iframe "load" event
131
+ L->>W: flush queued messages
178
132
 
179
- See the main [repository README](../../README.md) for contribution guidelines.
133
+ W-->>L: scraped CSS variables
134
+ L->>App: cb(variables)
180
135
 
181
- ## πŸ“„ License
136
+ App->>L: applyCssVariables(overrides)
137
+ L->>W: applyCssVariables
138
+ W-->>L: applied βœ“
139
+ ```
182
140
 
183
- ISC - See [LICENSE](../../LICENSE) for details.
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
- **Made with ❀️ by the Neomorph team**
147
+ ## License
188
148
 
189
- For more information, visit the [main repository](https://github.com/sinha-sahil/neomorph).
149
+ ISC β€” see [LICENSE](../../LICENSE).
package/dist/cdn.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- function e(e){return"string"==typeof e?e:null}function n(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}const t=function(t){if(n(t)){const i=e(t.requestId),r=e(t.service),a=function(e){if(n(e))return{...e};return null}(t.payload);if(null!==i&&null!==r&&null!==a)return{requestId:i,service:r,payload:a}}return null};class i{applicationFrame;constructor(){this.applicationFrame=null}loadApplication(e,n){if("undefined"!=typeof document&&"undefined"!=typeof window)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",document.body.appendChild(this.applicationFrame),window.addEventListener("message",e=>{const i=t(e.data);n(i)})}catch(e){console.error("πŸ•ΈοΈ Weaver: Failed to load application:",e)}else console.warn("πŸ•ΈοΈ Weaver: loadApplication() called in non-browser environment")}listenCssVariables(){if("undefined"!=typeof window)try{const e=this.applicationFrame;if(!e?.contentWindow)return void console.error("πŸ•ΈοΈ Weaver: No iframe loaded or iframe not ready");e.contentWindow.postMessage(JSON.stringify({source:"skinweaver",payload:JSON.stringify({requestId:"randomId",service:"skinweaver",payload:{action:"listenCssVariables"}})}),"*")}catch(e){console.error("πŸ•ΈοΈ Weaver: Failed to send message to iframe:",e)}else console.warn("πŸ•ΈοΈ Weaver: listenCssVariables() called in non-browser environment")}}class r{static inject(e="1.0.0"){if("undefined"!=typeof document)try{const n=document.createElement("script");n.src=`https://cdn.jsdelivr.net/gh/sinha-sahil/neomorph/build/weaver/${e}/index.js`,n.type="text/javascript",document.head.appendChild(n)}catch(e){console.error("πŸ•ΈοΈ Weaver: Failed to inject weaver script:",e)}else console.warn("πŸ•ΈοΈ Weaver: inject() called in non-browser environment")}}export{i as Loomer,r as Weaver};
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
@@ -1,7 +1,17 @@
1
- import { SDKResponse } from "./types";
1
+ import { StylesCallback } from "./types";
2
2
  export declare class Loomer {
3
3
  private applicationFrame;
4
+ private callbacks;
5
+ private ready;
6
+ private pendingMessages;
4
7
  constructor();
5
- loadApplication(url: string, callback: (response: SDKResponse | null) => void): void;
6
- listenCssVariables(): void;
8
+ loadApplication(url: string, container?: HTMLElement): void;
9
+ private flushPendingMessages;
10
+ private postMessage;
11
+ private sendMessage;
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;
7
17
  }
package/dist/types.d.ts CHANGED
@@ -15,6 +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: SDKPayloadPayload) => void;
18
19
  export declare function decodeCssProperty(rawInput: unknown): CSSProperty | null;
19
20
  export declare function decodeSDKPayload(rawInput: unknown): SDKPayload | null;
20
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.0.1",
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,13 +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
- "format": "npx prettier --write .",
22
- "lint": "prettier --plugin-search-dir . --check . && eslint .",
23
- "clean": "rm -rf dist"
24
- },
25
18
  "keywords": [
26
19
  "neomorph",
27
20
  "theme",
@@ -46,9 +39,8 @@
46
39
  "publishConfig": {
47
40
  "access": "public"
48
41
  },
49
- "packageManager": "pnpm@10.18.0",
50
42
  "dependencies": {
51
- "type-decoder": "^2.1.0"
43
+ "type-decoder": "^2.3.1"
52
44
  },
53
45
  "devDependencies": {
54
46
  "@eslint/eslintrc": "^3.3.1",
@@ -64,7 +56,17 @@
64
56
  "eslint-config-prettier": "^10.1.8",
65
57
  "prettier": "^3.6.2",
66
58
  "rollup": "^4.52.0",
59
+ "rollup-plugin-serve": "^3.0.0",
67
60
  "tslib": "^2.8.1",
68
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"
69
71
  }
70
- }
72
+ }