@vitus-labs/unistyle 2.0.0-beta.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -79,6 +79,127 @@ const normalizeTheme = ({ theme, breakpoints }) => {
79
79
  return result;
80
80
  };
81
81
 
82
+ //#endregion
83
+ //#region src/responsive/optimizeBreakpointDeltas.ts
84
+ /** Parse a CSS string into top-level declarations and opaque blocks. */
85
+ const parse = (css) => {
86
+ const entries = [];
87
+ const len = css.length;
88
+ let depth = 0;
89
+ let parenDepth = 0;
90
+ let quote = 0;
91
+ let segmentStart = 0;
92
+ const pushSegment = (rawSegment) => {
93
+ const trimmed = rawSegment.trim();
94
+ if (!trimmed) return;
95
+ const text = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
96
+ const colonIdx = text.indexOf(":");
97
+ if (colonIdx <= 0) {
98
+ entries.push({
99
+ kind: "block",
100
+ raw: `${text};`
101
+ });
102
+ return;
103
+ }
104
+ const prop = text.slice(0, colonIdx).trim();
105
+ const value = text.slice(colonIdx + 1).trim();
106
+ if (!prop || !value) {
107
+ entries.push({
108
+ kind: "block",
109
+ raw: `${text};`
110
+ });
111
+ return;
112
+ }
113
+ entries.push({
114
+ kind: "decl",
115
+ prop,
116
+ value,
117
+ raw: `${prop}: ${value};`
118
+ });
119
+ };
120
+ for (let i = 0; i < len; i++) {
121
+ const code = css.charCodeAt(i);
122
+ if (quote !== 0) {
123
+ if (code === 92) i++;
124
+ else if (code === quote) quote = 0;
125
+ continue;
126
+ }
127
+ if (code === 34 || code === 39) {
128
+ quote = code;
129
+ continue;
130
+ }
131
+ if (code === 40) {
132
+ parenDepth++;
133
+ continue;
134
+ }
135
+ if (code === 41) {
136
+ if (parenDepth > 0) parenDepth--;
137
+ continue;
138
+ }
139
+ if (parenDepth > 0) continue;
140
+ if (code === 123) {
141
+ depth++;
142
+ continue;
143
+ }
144
+ if (code === 125) {
145
+ depth--;
146
+ if (depth === 0) {
147
+ const raw = css.slice(segmentStart, i + 1).trim();
148
+ if (raw) entries.push({
149
+ kind: "block",
150
+ raw
151
+ });
152
+ segmentStart = i + 1;
153
+ }
154
+ continue;
155
+ }
156
+ if (depth === 0 && code === 59) {
157
+ pushSegment(css.slice(segmentStart, i));
158
+ segmentStart = i + 1;
159
+ }
160
+ }
161
+ if (segmentStart < len) {
162
+ const trailing = css.slice(segmentStart).trim();
163
+ if (trailing) if (depth > 0) entries.push({
164
+ kind: "block",
165
+ raw: trailing
166
+ });
167
+ else pushSegment(trailing);
168
+ }
169
+ return entries;
170
+ };
171
+ /**
172
+ * Apply the mobile-first cascade diff. The first entry passes through
173
+ * unchanged; subsequent entries are pruned to the delta vs. the running
174
+ * cascade (declarations by prop, blocks by exact text match).
175
+ */
176
+ const optimizeBreakpointDeltas = (cssStrings) => {
177
+ if (cssStrings.length <= 1) return cssStrings;
178
+ const cascadeDecl = /* @__PURE__ */ new Map();
179
+ const cascadeBlocks = /* @__PURE__ */ new Set();
180
+ const out = new Array(cssStrings.length);
181
+ for (let i = 0; i < cssStrings.length; i++) {
182
+ const css = cssStrings[i];
183
+ if (!css) {
184
+ out[i] = "";
185
+ continue;
186
+ }
187
+ const entries = parse(css);
188
+ const kept = [];
189
+ for (const e of entries) if (e.kind === "decl") {
190
+ if (cascadeDecl.get(e.prop) !== e.value) {
191
+ kept.push(e.raw);
192
+ cascadeDecl.set(e.prop, e.value);
193
+ }
194
+ } else if (!cascadeBlocks.has(e.raw)) {
195
+ kept.push(e.raw);
196
+ cascadeBlocks.add(e.raw);
197
+ }
198
+ out[i] = kept.join(" ");
199
+ }
200
+ return out;
201
+ };
202
+
82
203
  //#endregion
83
204
  //#region src/responsive/optimizeTheme.ts
84
205
  const shallowEqual = (a, b) => {
@@ -145,6 +266,23 @@ const transformTheme = ({ theme, breakpoints }) => {
145
266
  //#endregion
146
267
  //#region src/responsive/makeItResponsive.ts
147
268
  /**
269
+ * Coerce a styles-callback result to a CSS string for delta optimization.
270
+ * Returns null when the engine's result type can't be stringified cleanly
271
+ * (e.g. Emotion / styled-components objects whose default toString() yields
272
+ * "[object Object]") — caller falls back to the unoptimized path.
273
+ *
274
+ * Styler's CSSResult provides toString() that resolves with empty props,
275
+ * so any function interpolation that needs render-time props must come from
276
+ * the styles-callback closure (theme is destructured at call time, not
277
+ * resolved later). Verified across the project's styles callbacks.
278
+ */
279
+ const stringifyResult = (result) => {
280
+ if (result == null) return "";
281
+ if (typeof result === "string") return result;
282
+ const text = String(result);
283
+ return text.includes("[object ") ? null : text;
284
+ };
285
+ /**
148
286
  * Core responsive engine used by every styled component in the system.
149
287
  *
150
288
  * Returns a styled-components interpolation function that:
@@ -190,6 +328,19 @@ const makeItResponsive = ({ theme: customTheme, key = "", css, styles, normalize
190
328
  optimized: optimizedTheme
191
329
  });
192
330
  }
331
+ const renderedTexts = sortedBreakpoints.map((item) => {
332
+ const breakpointTheme = optimizedTheme[item];
333
+ if (!breakpointTheme || !media) return "";
334
+ return stringifyResult(renderStyles(breakpointTheme));
335
+ });
336
+ if (renderedTexts.every((t) => t !== null)) {
337
+ const deltas = optimizeBreakpointDeltas(renderedTexts);
338
+ return sortedBreakpoints.map((item, i) => {
339
+ const css = deltas[i];
340
+ if (!css || !media) return "";
341
+ return media[item]`${css}`;
342
+ });
343
+ }
193
344
  return sortedBreakpoints.map((item) => {
194
345
  const breakpointTheme = optimizedTheme[item];
195
346
  if (!breakpointTheme || !media) return "";
@@ -79,6 +79,127 @@ const normalizeTheme = ({ theme, breakpoints }) => {
79
79
  return result;
80
80
  };
81
81
 
82
+ //#endregion
83
+ //#region src/responsive/optimizeBreakpointDeltas.ts
84
+ /** Parse a CSS string into top-level declarations and opaque blocks. */
85
+ const parse = (css) => {
86
+ const entries = [];
87
+ const len = css.length;
88
+ let depth = 0;
89
+ let parenDepth = 0;
90
+ let quote = 0;
91
+ let segmentStart = 0;
92
+ const pushSegment = (rawSegment) => {
93
+ const trimmed = rawSegment.trim();
94
+ if (!trimmed) return;
95
+ const text = trimmed.endsWith(";") ? trimmed.slice(0, -1) : trimmed;
96
+ const colonIdx = text.indexOf(":");
97
+ if (colonIdx <= 0) {
98
+ entries.push({
99
+ kind: "block",
100
+ raw: `${text};`
101
+ });
102
+ return;
103
+ }
104
+ const prop = text.slice(0, colonIdx).trim();
105
+ const value = text.slice(colonIdx + 1).trim();
106
+ if (!prop || !value) {
107
+ entries.push({
108
+ kind: "block",
109
+ raw: `${text};`
110
+ });
111
+ return;
112
+ }
113
+ entries.push({
114
+ kind: "decl",
115
+ prop,
116
+ value,
117
+ raw: `${prop}: ${value};`
118
+ });
119
+ };
120
+ for (let i = 0; i < len; i++) {
121
+ const code = css.charCodeAt(i);
122
+ if (quote !== 0) {
123
+ if (code === 92) i++;
124
+ else if (code === quote) quote = 0;
125
+ continue;
126
+ }
127
+ if (code === 34 || code === 39) {
128
+ quote = code;
129
+ continue;
130
+ }
131
+ if (code === 40) {
132
+ parenDepth++;
133
+ continue;
134
+ }
135
+ if (code === 41) {
136
+ if (parenDepth > 0) parenDepth--;
137
+ continue;
138
+ }
139
+ if (parenDepth > 0) continue;
140
+ if (code === 123) {
141
+ depth++;
142
+ continue;
143
+ }
144
+ if (code === 125) {
145
+ depth--;
146
+ if (depth === 0) {
147
+ const raw = css.slice(segmentStart, i + 1).trim();
148
+ if (raw) entries.push({
149
+ kind: "block",
150
+ raw
151
+ });
152
+ segmentStart = i + 1;
153
+ }
154
+ continue;
155
+ }
156
+ if (depth === 0 && code === 59) {
157
+ pushSegment(css.slice(segmentStart, i));
158
+ segmentStart = i + 1;
159
+ }
160
+ }
161
+ if (segmentStart < len) {
162
+ const trailing = css.slice(segmentStart).trim();
163
+ if (trailing) if (depth > 0) entries.push({
164
+ kind: "block",
165
+ raw: trailing
166
+ });
167
+ else pushSegment(trailing);
168
+ }
169
+ return entries;
170
+ };
171
+ /**
172
+ * Apply the mobile-first cascade diff. The first entry passes through
173
+ * unchanged; subsequent entries are pruned to the delta vs. the running
174
+ * cascade (declarations by prop, blocks by exact text match).
175
+ */
176
+ const optimizeBreakpointDeltas = (cssStrings) => {
177
+ if (cssStrings.length <= 1) return cssStrings;
178
+ const cascadeDecl = /* @__PURE__ */ new Map();
179
+ const cascadeBlocks = /* @__PURE__ */ new Set();
180
+ const out = new Array(cssStrings.length);
181
+ for (let i = 0; i < cssStrings.length; i++) {
182
+ const css = cssStrings[i];
183
+ if (!css) {
184
+ out[i] = "";
185
+ continue;
186
+ }
187
+ const entries = parse(css);
188
+ const kept = [];
189
+ for (const e of entries) if (e.kind === "decl") {
190
+ if (cascadeDecl.get(e.prop) !== e.value) {
191
+ kept.push(e.raw);
192
+ cascadeDecl.set(e.prop, e.value);
193
+ }
194
+ } else if (!cascadeBlocks.has(e.raw)) {
195
+ kept.push(e.raw);
196
+ cascadeBlocks.add(e.raw);
197
+ }
198
+ out[i] = kept.join(" ");
199
+ }
200
+ return out;
201
+ };
202
+
82
203
  //#endregion
83
204
  //#region src/responsive/optimizeTheme.ts
84
205
  const shallowEqual = (a, b) => {
@@ -145,6 +266,23 @@ const transformTheme = ({ theme, breakpoints }) => {
145
266
  //#endregion
146
267
  //#region src/responsive/makeItResponsive.ts
147
268
  /**
269
+ * Coerce a styles-callback result to a CSS string for delta optimization.
270
+ * Returns null when the engine's result type can't be stringified cleanly
271
+ * (e.g. Emotion / styled-components objects whose default toString() yields
272
+ * "[object Object]") — caller falls back to the unoptimized path.
273
+ *
274
+ * Styler's CSSResult provides toString() that resolves with empty props,
275
+ * so any function interpolation that needs render-time props must come from
276
+ * the styles-callback closure (theme is destructured at call time, not
277
+ * resolved later). Verified across the project's styles callbacks.
278
+ */
279
+ const stringifyResult = (result) => {
280
+ if (result == null) return "";
281
+ if (typeof result === "string") return result;
282
+ const text = String(result);
283
+ return text.includes("[object ") ? null : text;
284
+ };
285
+ /**
148
286
  * Core responsive engine used by every styled component in the system.
149
287
  *
150
288
  * Returns a styled-components interpolation function that:
@@ -190,6 +328,19 @@ const makeItResponsive = ({ theme: customTheme, key = "", css, styles, normalize
190
328
  optimized: optimizedTheme
191
329
  });
192
330
  }
331
+ const renderedTexts = sortedBreakpoints.map((item) => {
332
+ const breakpointTheme = optimizedTheme[item];
333
+ if (!breakpointTheme || !media) return "";
334
+ return stringifyResult(renderStyles(breakpointTheme));
335
+ });
336
+ if (renderedTexts.every((t) => t !== null)) {
337
+ const deltas = optimizeBreakpointDeltas(renderedTexts);
338
+ return sortedBreakpoints.map((item, i) => {
339
+ const css = deltas[i];
340
+ if (!css || !media) return "";
341
+ return media[item]`${css}`;
342
+ });
343
+ }
193
344
  return sortedBreakpoints.map((item) => {
194
345
  const breakpointTheme = optimizedTheme[item];
195
346
  if (!breakpointTheme || !media) return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitus-labs/unistyle",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "author": "Vit Bokisch <vit@bokisch.cz>",
6
6
  "maintainers": [
@@ -51,7 +51,7 @@
51
51
  "node": ">= 18"
52
52
  },
53
53
  "peerDependencies": {
54
- "@vitus-labs/core": "2.0.0-beta.3",
54
+ "@vitus-labs/core": "2.0.0",
55
55
  "react": ">= 19",
56
56
  "react-native": ">= 0.76"
57
57
  },