grailwind 0.8.0 → 0.8.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.1] - 2026-11-13
10
+
11
+ ### Fixed
12
+
13
+ - Counterchanged charges `on` a counterchanged ordinary showed the ordinary's
14
+ swapped field and vanished. They now show the field's own tinctures.
15
+ - `chevronny-*` in a part of a split shield (a `per-pale` half, a `per-fess`
16
+ chief) broke at the centre line. It now follows the part, as `per-chevron`
17
+ does, and the arms meet.
18
+
9
19
  ## [0.8.0] - 2026-11-12
10
20
 
11
21
  ### Added
@@ -183,7 +193,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
183
193
  Initial release: tinctures, divisions, ordinaries, charges, strict mode and
184
194
  custom SVG charges.
185
195
 
186
- [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.8.0...HEAD
196
+ [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.8.1...HEAD
197
+ [0.8.1]: https://github.com/pafirmin/grailwind/compare/v0.8.0...v0.8.1
187
198
  [0.8.0]: https://github.com/pafirmin/grailwind/compare/v0.7.0...v0.8.0
188
199
  [0.7.0]: https://github.com/pafirmin/grailwind/compare/v0.6.0...v0.7.0
189
200
  [0.6.0]: https://github.com/pafirmin/grailwind/compare/v0.5.0...v0.6.0
package/charges.js CHANGED
@@ -149,17 +149,28 @@ const inheritCounterchange = Object.fromEntries(
149
149
  COUNTERCHANGE.map((n) => [`--counterchange-${n}`, "inherit"]),
150
150
  );
151
151
 
152
+ // What a charge on a counterchanged ordinary is counterchanged against: the field's own
153
+ // tinctures, handed down by the ordinary to `on` containers only.
154
+ const inheritOnCounterchange = Object.fromEntries(
155
+ COUNTERCHANGE.map((n) => [`--on-counterchange-${n}`, "inherit"]),
156
+ );
157
+
152
158
  // A layer the size of the shield, as the escutcheon is the containing block
153
159
  // for fixed positioning. The charge's mask cuts it to shape.
154
160
  const counterchanged = {
155
161
  ...inheritCounterchange,
162
+ ...inheritOnCounterchange,
156
163
  "&::before": {
157
164
  content: '""',
158
165
  position: "fixed",
159
166
  inset: "0",
160
167
  ...inheritCounterchange,
168
+ ...inheritOnCounterchange,
161
169
  ...Object.fromEntries(
162
- COUNTERCHANGE.map((n) => [`background-${n}`, `var(--counterchange-${n})`]),
170
+ COUNTERCHANGE.map((n) => [
171
+ `background-${n}`,
172
+ `var(--on-counterchange-${n}, var(--counterchange-${n}))`,
173
+ ]),
163
174
  ),
164
175
  },
165
176
  };
@@ -279,7 +290,8 @@ export default plugin(({ addUtilities, matchUtilities }) => {
279
290
  };
280
291
  }
281
292
  }
282
- utilities[".on"] = on;
293
+ // Nested, so Tailwind keeps these apart from the per-count rules above.
294
+ utilities[".on"] = { "&": inheritOnCounterchange, ...on };
283
295
 
284
296
  for (const name of Object.keys(POINTS)) {
285
297
  utilities[`.in-${name}`] = { ".charges > &": atPoint(name) };
package/divisions.js CHANGED
@@ -50,31 +50,36 @@ const bendy = (to, pieces) => (a, b) => {
50
50
  return layer(`linear-gradient(to ${to}, ${stops.join(", ")})`);
51
51
  };
52
52
 
53
- // The direction of `to bottom right` on the shield (aspect-ratio 5 / 6 in grailwind.css), so
54
- // chevronny's lines run parallel to per chevron's, the field's diagonals.
55
- const CHEVRON_ANGLE = 180 - (Math.atan(6 / 5) * 180) / Math.PI;
56
-
57
- // A half box is 75% as long as the whole along this angle (45/sqrt(61) over 60/sqrt(61)).
58
- // Plain, the sinister half sits over a full-width dexter layer, which hides the centre seam.
59
- // With a fur the layers can't overlap, as the transparent stripes would show the layer below.
53
+ // Four tiles, each half the box, so the lines follow the box's own proportions: a part of the
54
+ // shield is the whole one squeezed to fit, as per chevron's are. A tile's lines run parallel to its
55
+ // diagonal, dexter tiles `to bottom right` and sinister ones `to bottom left`.
56
+ // The period is 300/n of a tile's gradient line, which keeps a stripe's width on the whole shield.
57
+ // A lower tile starts half a gradient line further on, so its lines continue the upper tile's.
58
+ // Plain, the dexter and lower tiles overlap the others by 1px, hiding the seams. With a fur the
59
+ // transparent stripes would show the tile beneath, so they butt.
60
60
  const chevronny = (pieces) => (a, b, fur) => {
61
- const stripe = (angle, length) =>
62
- `repeating-linear-gradient(${angle}deg, ${a} 0 calc(${length}% / ${pieces}), ${b} 0 calc(${2 * length}% / ${pieces}))`;
63
- const sinister = stripe(-CHEVRON_ANGLE, 100);
64
-
65
- return fur
66
- ? {
67
- "background-image": [stripe(CHEVRON_ANGLE, 100), sinister].join(", "),
68
- "background-size": "50% 100%",
69
- "background-position": "left, right",
70
- "background-repeat": "no-repeat",
71
- }
72
- : {
73
- "background-image": [sinister, stripe(CHEVRON_ANGLE, 75)].join(", "),
74
- "background-size": "50% 100%, 100% 100%",
75
- "background-position": "right, left",
76
- "background-repeat": "no-repeat",
77
- };
61
+ const period = 300 / pieces;
62
+ const lowerStart = (period - (50 % period)) % period;
63
+ const stripe = (to, start) =>
64
+ `repeating-linear-gradient(to bottom ${to}, ${a} ${start}% ${start + period / 2}%, ${b} 0 ${start + period}%)`;
65
+ const overlap = fur ? "50%" : "calc(50% + 1px)";
66
+
67
+ return {
68
+ "background-image": [
69
+ stripe("left", 0),
70
+ stripe("left", lowerStart),
71
+ stripe("right", 0),
72
+ stripe("right", lowerStart),
73
+ ].join(", "),
74
+ "background-size": [
75
+ `50% 50%`,
76
+ `50% ${overlap}`,
77
+ `${overlap} 50%`,
78
+ `${overlap} ${overlap}`,
79
+ ].join(", "),
80
+ "background-position": "right top, right bottom, left top, left bottom",
81
+ "background-repeat": "no-repeat",
82
+ };
78
83
  };
79
84
 
80
85
  const DIVISIONS = {
@@ -225,21 +230,70 @@ const toLayers = (rules) => {
225
230
  });
226
231
  };
227
232
 
228
- // A division with one fur: the fur's part is left transparent, over the fur's own layers.
229
- const withFur = (division, first, second, theme) => {
233
+ /**
234
+ * A division with one fur: the fur's part is left transparent, over the fur's own layers.
235
+ * @param {string} division The division's name.
236
+ * @param {string} first The first tincture or fur.
237
+ * @param {string} second The second tincture or fur; one of the two is a fur.
238
+ * @param {Function} theme Tailwind's theme function, for the spots' colour.
239
+ * @returns {{image: string, size: string, position: string, repeat: string}[]} The layers, top first.
240
+ */
241
+ const furredLayers = (division, first, second, theme) => {
230
242
  const fur = FURS[first] ? first : second;
231
243
  const paint = (t) => (t === fur ? "transparent" : `var(--color-${t})`);
232
- const all = [
244
+ return [
233
245
  ...toLayers(DIVISIONS[division](paint(first), paint(second), true)),
234
246
  ...furLayers(fur, theme),
235
247
  ];
236
- return Object.fromEntries(
248
+ };
249
+
250
+ /**
251
+ * Layers joined into one comma-separated value per property.
252
+ * @param {{image: string, size: string, position: string, repeat: string}[]} layers
253
+ * @param {string} prefix The property prefix, such as "background" or "--field".
254
+ * @returns {Object<string, string>} `<prefix>-image`, `-size`, `-position` and `-repeat`.
255
+ */
256
+ const joinLayers = (layers, prefix) =>
257
+ Object.fromEntries(
237
258
  ["image", "size", "position", "repeat"].map((p) => [
238
- `background-${p}`,
239
- all.map((layer) => layer[p]).join(", "),
259
+ `${prefix}-${p}`,
260
+ layers.map((layer) => layer[p]).join(", "),
240
261
  ]),
241
262
  );
242
- };
263
+
264
+ /**
265
+ * A division with one fur, as background rules.
266
+ * @param {string} division The division's name.
267
+ * @param {string} first The first tincture or fur.
268
+ * @param {string} second The second tincture or fur; one of the two is a fur.
269
+ * @param {Function} theme Tailwind's theme function, for the spots' colour.
270
+ * @returns {Object<string, string>} The four background-* properties.
271
+ */
272
+ const withFur = (division, first, second, theme) =>
273
+ joinLayers(furredLayers(division, first, second, theme), "background");
274
+
275
+ /**
276
+ * The field's own layers as --field-* variables, for charges on a counterchanged ordinary.
277
+ * A background colour becomes a last opaque layer, as the variables carry no colour.
278
+ * @param {{image: string, size: string, position: string, repeat: string}[]} layers
279
+ * @param {string} [color] The division's background-color, if it has one.
280
+ * @returns {Object<string, string>} The four --field-* variables.
281
+ */
282
+ const field = (layers, color) =>
283
+ joinLayers(
284
+ color
285
+ ? [
286
+ ...layers,
287
+ {
288
+ image: `linear-gradient(${color}, ${color})`,
289
+ size: "100% 100%",
290
+ position: "0 0",
291
+ repeat: "no-repeat",
292
+ },
293
+ ]
294
+ : layers,
295
+ "--field",
296
+ );
243
297
 
244
298
  // The field with its tinctures swapped, for counterchanged ordinaries to paint.
245
299
  const counterchange = (rules) =>
@@ -262,8 +316,10 @@ export default plugin(({ addUtilities, theme }) => {
262
316
 
263
317
  const a = `var(--color-${first})`;
264
318
  const b = `var(--color-${second})`;
319
+ const unswapped = background(a, b);
265
320
  utilities[`.${division}-${first}-${second}`] = {
266
- ...background(a, b),
321
+ ...unswapped,
322
+ ...field(toLayers(unswapped), unswapped["background-color"]),
267
323
  ...counterchange(background(b, a)),
268
324
  "--field-first": a,
269
325
  };
@@ -279,9 +335,11 @@ export default plugin(({ addUtilities, theme }) => {
279
335
  [fur, tincture],
280
336
  [tincture, fur],
281
337
  ]) {
338
+ const layers = furredLayers(division, first, second, theme);
282
339
  // Only a tincture can be "of the first"; a fur first leaves it unset.
283
340
  utilities[`.${division}-${first}-${second}`] = {
284
- ...withFur(division, first, second, theme),
341
+ ...joinLayers(layers, "background"),
342
+ ...field(layers),
285
343
  ...counterchange(withFur(division, second, first, theme)),
286
344
  ...(!FURS[first] && { "--field-first": `var(--color-${first})` }),
287
345
  };
package/grailwind.css CHANGED
@@ -42,6 +42,42 @@
42
42
  syntax: "*";
43
43
  inherits: false;
44
44
  }
45
+ @property --field-image {
46
+ syntax: "*";
47
+ inherits: false;
48
+ }
49
+ @property --field-size {
50
+ syntax: "*";
51
+ inherits: false;
52
+ }
53
+ @property --field-position {
54
+ syntax: "*";
55
+ inherits: false;
56
+ }
57
+ @property --field-repeat {
58
+ syntax: "*";
59
+ inherits: false;
60
+ }
61
+ @property --on-counterchange-color {
62
+ syntax: "*";
63
+ inherits: false;
64
+ }
65
+ @property --on-counterchange-image {
66
+ syntax: "*";
67
+ inherits: false;
68
+ }
69
+ @property --on-counterchange-size {
70
+ syntax: "*";
71
+ inherits: false;
72
+ }
73
+ @property --on-counterchange-position {
74
+ syntax: "*";
75
+ inherits: false;
76
+ }
77
+ @property --on-counterchange-repeat {
78
+ syntax: "*";
79
+ inherits: false;
80
+ }
45
81
  /* A field's first tincture. The escutcheon copies it to --the-first, which inherits,
46
82
  * so a canton's own field can't change it. */
47
83
  @property --field-first {
package/ordinaries.js CHANGED
@@ -780,10 +780,20 @@ export default plugin(({ addBase, addUtilities, theme }) => {
780
780
  // The swapped field's tiles would wrap into the bordure's strips, so the bordure shows there.
781
781
  ...(chief && { "background-clip": "content-box" }),
782
782
  };
783
- utilities[`.${ordinary}-counterchanged`] = ordinaryRules(ordinary, {
784
- ...counterchanged,
785
- "clip-path": chief ? plain : `polygon(${vertices.map(pointBelowChief).join(", ")})`,
786
- });
783
+ // Charges on it are counterchanged against it, so they get the field's own tinctures.
784
+ utilities[`.${ordinary}-counterchanged`] = {
785
+ ...ordinaryRules(ordinary, {
786
+ ...counterchanged,
787
+ "clip-path": chief ? plain : `polygon(${vertices.map(pointBelowChief).join(", ")})`,
788
+ }),
789
+ "--on-counterchange-color": "transparent",
790
+ ...Object.fromEntries(
791
+ ["image", "size", "position", "repeat"].map((n) => [
792
+ `--on-counterchange-${n}`,
793
+ `var(--field-${n})`,
794
+ ]),
795
+ ),
796
+ };
787
797
 
788
798
  for (const [tincture, colour] of PAINTS) {
789
799
  utilities[`.${ordinary}-${tincture}`] = ordinaryRules(ordinary, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grailwind",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "scripts": {
5
5
  "build": "tailwindcss -i docs/app.css -o docs/site.css",
6
6
  "dev": "tailwindcss -i docs/app.css -o docs/site.css --watch",