@zseven-w/pen-core 0.7.2 → 0.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zseven-w/pen-core",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Core document operations, tree utils, variables, layout engine for OpenPencil",
5
5
  "homepage": "https://github.com/ZSeven-W/openpencil/tree/main/packages/pen-core",
6
6
  "bugs": {
@@ -30,7 +30,7 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zseven-w/pen-types": "0.7.2",
33
+ "@zseven-w/pen-types": "0.7.3",
34
34
  "nanoid": "^5.1.6",
35
35
  "paper": "^0.12.18"
36
36
  },
@@ -235,6 +235,58 @@ describe('stripRedundantSectionFills', () => {
235
235
  expect((deepInner as PenNode & { fill?: unknown }).fill).toEqual(solidFill('#0A0A0A'));
236
236
  });
237
237
 
238
+ it('strips stale #FFFFFF section fills on a dark root (legacy alternation residue)', () => {
239
+ // Regression guard for 2026-04-15: the legacy fixSectionAlternation
240
+ // painted #FFFFFF / #F8FAFC on unfilled section runs regardless of
241
+ // page theme. After the alternation skip for dark parents landed,
242
+ // stale docs (and weak-model hedges) still carry those whites.
243
+ // stripRedundantSectionFills must now clean them up.
244
+ const section1 = frame({
245
+ id: 's1',
246
+ name: 'Hero',
247
+ role: 'hero',
248
+ fill: solidFill('#FFFFFF'),
249
+ });
250
+ const section2 = frame({
251
+ id: 's2',
252
+ name: 'Stats',
253
+ role: 'stats-section',
254
+ fill: solidFill('#F8FAFC'),
255
+ });
256
+ const section3 = frame({
257
+ id: 's3',
258
+ name: 'CTA',
259
+ role: 'cta-section',
260
+ fill: solidFill('#FFFFFF'),
261
+ });
262
+ const root = frame({
263
+ id: 'root',
264
+ fill: solidFill('#111111'),
265
+ children: [section1, section2, section3],
266
+ });
267
+ const changed = stripRedundantSectionFills(root);
268
+ expect(changed).toBe(true);
269
+ expect((section1 as PenNode & { fill?: unknown }).fill).toBeUndefined();
270
+ expect((section2 as PenNode & { fill?: unknown }).fill).toBeUndefined();
271
+ expect((section3 as PenNode & { fill?: unknown }).fill).toBeUndefined();
272
+ });
273
+
274
+ it('strips a safe-light hedge even when the root has no fill', () => {
275
+ // Mirror of the existing "root frame without a fill" dark case: a
276
+ // bare #FFFFFF on a section root is almost certainly the sub-agent
277
+ // hedging against a missing background spec, not a deliberate choice.
278
+ const section = frame({
279
+ id: 'sec',
280
+ fill: solidFill('#FAFAFA'),
281
+ });
282
+ const root = frame({
283
+ id: 'root',
284
+ children: [section],
285
+ });
286
+ stripRedundantSectionFills(root);
287
+ expect((section as PenNode & { fill?: unknown }).fill).toBeUndefined();
288
+ });
289
+
238
290
  it('reproduces the M2.7 health-tracker case', () => {
239
291
  // Direct repro of the actual failure: root #1a1a2e, six section roots
240
292
  // all hardcoded #0A0A0A, including one real card. The six section
@@ -126,13 +126,41 @@ const SAFE_DARK_HEXES = new Set([
126
126
  '#202020',
127
127
  ]);
128
128
 
129
+ /**
130
+ * Light-mode counterparts of SAFE_DARK_HEXES. Two sources produce these
131
+ * on section roots:
132
+ * 1. Weaker sub-agents that hedge with pure white / near-white instead
133
+ * of the role-correct transparent fill.
134
+ * 2. The legacy `fixSectionAlternation` post-pass that painted an
135
+ * #FFFFFF / #F8FAFC ladder on every run of ≥3 unfilled sections.
136
+ * On dark pages that ladder leaves visible white strips; if a doc
137
+ * with those stale fills is re-opened or re-rendered after the
138
+ * alternation skip landed, the fills are still there and the skip
139
+ * alone won't remove them.
140
+ * Treat these the same as safe-dark hedges: strip on any section root.
141
+ */
142
+ const SAFE_LIGHT_HEXES = new Set([
143
+ '#ffffff',
144
+ '#fff',
145
+ '#fefefe',
146
+ '#fdfdfd',
147
+ '#fcfcfc',
148
+ '#fafafa',
149
+ '#f9fafb',
150
+ '#f8f8f8',
151
+ '#f8fafc',
152
+ '#f5f5f5',
153
+ '#f4f4f5',
154
+ '#f3f4f6',
155
+ ]);
156
+
129
157
  function shouldStripFill(childFill: string, rootFill: string | null): boolean {
130
158
  const childKey = normalizeHex(childFill);
131
159
  if (rootFill) {
132
160
  const rootKey = normalizeHex(rootFill);
133
161
  if (childKey === rootKey) return true;
134
162
  }
135
- return SAFE_DARK_HEXES.has(childKey);
163
+ return SAFE_DARK_HEXES.has(childKey) || SAFE_LIGHT_HEXES.has(childKey);
136
164
  }
137
165
 
138
166
  function normalizeHex(color: string): string {