grailwind 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-11-06
10
+
11
+ ### Added
12
+
13
+ - Crosses and the shakefork as charges: `cross-couped`, `cross-potent`,
14
+ `cross-pattee`, `maltese-cross`, `cross-crosslet`, `cross-moline`,
15
+ `cross-fleury` and `shakefork`.
16
+ - `semy`: a semé, one built-in charge strewn across the field, e.g. bezanty or
17
+ crusilly.
18
+ - Voided ordinaries, e.g. `cross-voided-argent`: the field shows through.
19
+ `cross-voided-counterchanged` and the like counterchange the outline.
20
+ - Reversed ordinaries: `pile-reversed-*`, `chevron-reversed-*` and
21
+ `pall-reversed-*`, upside down.
22
+
23
+ ### Changed
24
+
25
+ - `@grailwind/eslint-plugin` 0.1.0-beta.2 needs `grailwind` 0.6.x, for voided
26
+ ordinaries.
27
+
28
+ ### Fixed
29
+
30
+ - Charges, a canton or an inescutcheon in a quarter (or a half of `per-pale` or
31
+ `per-fess`) are placed and counterchanged within it, under the shield's own
32
+ ordinary, rather than against the whole shield.
33
+
9
34
  ## [0.5.0] - 2026-11-04
10
35
 
11
36
  ### Added
@@ -113,7 +138,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
113
138
  Initial release: tinctures, divisions, ordinaries, charges, strict mode and
114
139
  custom SVG charges.
115
140
 
116
- [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.5.0...HEAD
141
+ [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.6.0...HEAD
142
+ [0.6.0]: https://github.com/pafirmin/grailwind/compare/v0.5.0...v0.6.0
117
143
  [0.5.0]: https://github.com/pafirmin/grailwind/compare/v0.4.0...v0.5.0
118
144
  [0.4.0]: https://github.com/pafirmin/grailwind/compare/v0.3.0...v0.4.0
119
145
  [0.3.0]: https://github.com/pafirmin/grailwind/compare/v0.2.0...v0.3.0
package/README.md CHANGED
@@ -62,12 +62,15 @@ tinctures replace Tailwind's colours entirely.
62
62
  - **Ordinaries:** `fess-or`, `chevron-gules`, `cross-argent`, `chief-azure`,
63
63
  `two-bars-or` and more, with `fimbriated-*` for a narrow border,
64
64
  `fess-engrailed-or` and the like for an engrailed or invected edge,
65
+ `cross-voided-argent` and the like to show the field through it,
66
+ `pile-reversed-or` (also the chevron and pall) to turn one upside down,
65
67
  `chief-ermine` and the like in a fur, or `chevron-counterchanged` and the like
66
68
  on a divided field.
67
- - **Charges:** `mullet`, `fleur-de-lis`, `bezant` and more, arranged by
69
+ - **Charges:** `mullet`, `fleur-de-lis`, `bezant`, crosses and more, arranged by
68
70
  number, `in-pale`, `in-fess`, `in-bend`, `in-bend-sinister`, `on` or `between`
69
- the ordinary, or placed at a named point such as `in-dexter-chief`, sized
70
- with `charge-size-*`, and `counterchanged` on a divided field.
71
+ the ordinary, or placed at a named point such as `in-dexter-chief`, strewn
72
+ `semy` across the field, sized with `charge-size-*`, and `counterchanged` on
73
+ a divided field.
71
74
  - **Your own charges:** any SVG, via `charge-[url(…)]`.
72
75
 
73
76
  ## Linting
package/charges.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import plugin from "tailwindcss/plugin";
5
5
  import { POINTS, ROUNDELS } from "./data.js";
6
+ import { AT } from "./furs.js";
6
7
 
7
8
  const star = (cx, cy, r, points = 5) => {
8
9
  const coords = [];
@@ -14,6 +15,20 @@ const star = (cx, cy, r, points = 5) => {
14
15
  return `<polygon points="${coords.join(" ")}"/>`;
15
16
  };
16
17
 
18
+ // One arm drawn upward from the centre, repeated x times evenly about (50, 50).
19
+ const rotateX = (d, x) => {
20
+ const path = (d, deg) => `<path d="${d}" transform="rotate(${deg} 50 50)" />`;
21
+
22
+ const paths = [];
23
+ const rotation = 360 / x;
24
+ let curr = 0;
25
+ for (let i = 0; i < x; i++) {
26
+ paths.push(path(d, curr));
27
+ curr += rotation;
28
+ }
29
+ return paths.join("");
30
+ };
31
+
17
32
  const SHAPES = {
18
33
  roundel: `<circle cx="50" cy="50" r="46"/>`,
19
34
  annulet: `<circle cx="50" cy="50" r="39" fill="none" stroke="#000" stroke-width="14"/>`,
@@ -29,6 +44,20 @@ const SHAPES = {
29
44
  <path d="M45 65H55C55 74 53 82 50 90C47 82 45 74 45 65Z"/>
30
45
  <path d="M58 65C62 74 72 80 84 78C80 88 64 90 56 74Z"/>
31
46
  <path d="M42 65C38 74 28 80 16 78C20 88 36 90 44 74Z"/>`,
47
+ "cross-couped": rotateX("M39 50V2H61V50Z", 4),
48
+ "cross-potent": rotateX("M43 50V16H31V2H69V16H57V50Z", 4),
49
+ "cross-pattee": rotateX("M40 50V40Q36 20 22 2H78Q64 20 60 40V50Z", 4),
50
+ "maltese-cross": rotateX("M50 50L30 2L50 18L70 2Z", 4),
51
+ "cross-crosslet": rotateX("M43 50V25H32V13H43V2H57V13H68V25H57V50Z", 4),
52
+ "cross-moline": rotateX(
53
+ "M43 50V18C42 12 38 11 33 16C31 6 40 -1 50 7C60 -1 69 6 67 16C62 11 58 12 57 18V50Z",
54
+ 4,
55
+ ),
56
+ "cross-fleury": rotateX(
57
+ "M43 50V21C41 17 37 16 33 20C30 12 37 8 45 13C45 8 47 5 50 2C53 5 55 8 55 13C63 8 70 12 67 20C63 16 59 17 57 21V50Z",
58
+ 4,
59
+ ),
60
+ shakefork: `<polygon points="50,96 39,85 39,43 12.6,20.9 11.3,5.4 26.8,4 50,23.5 73.2,4 88.7,5.4 87.4,20.9 61,43 61,85"/>`,
32
61
  };
33
62
 
34
63
  // Points as [left%, top%] of the shield, plus a charge size as % of its width.
@@ -88,6 +117,19 @@ const svgUrl = (shape) =>
88
117
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shape}</svg>`,
89
118
  )}")`;
90
119
 
120
+ // Semé: whole charges across the field, staggered on the furs' tile (see furs.js), cut at the
121
+ // edges. The shape is defined once and reused, so a shape with its own ids stays valid.
122
+ const SEMY_ACROSS = 4;
123
+ const SEMY_SCALE = 0.25;
124
+ const SEMY_SHIFT = `${-100 / (4 * (SEMY_ACROSS - 1))}%`;
125
+ const semyTile = (shape) => {
126
+ const at = (x, y) => `translate(${x - 50 * SEMY_SCALE} ${y - 50 * SEMY_SCALE})`;
127
+ const uses = AT.map(([x, y]) => `<use href="#s" transform="${at(x, y)} scale(${SEMY_SCALE})"/>`);
128
+ return `url("data:image/svg+xml,${encodeURIComponent(
129
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 60"><defs><g id="s">${shape}</g></defs>${uses.join("")}</svg>`,
130
+ )}")`;
131
+ };
132
+
91
133
  const maskRules = (image) => ({
92
134
  "mask-image": image,
93
135
  "mask-size": "contain",
@@ -143,12 +185,13 @@ export default plugin(({ addUtilities, matchUtilities }) => {
143
185
  const utilities = {};
144
186
 
145
187
  for (const [name, shape] of Object.entries(SHAPES)) {
146
- utilities[`.${name}`] = maskRules(svgUrl(shape));
188
+ utilities[`.${name}`] = { ...maskRules(svgUrl(shape)), "--semy-image": semyTile(shape) };
147
189
  }
148
190
 
149
191
  for (const [name, tincture] of Object.entries(ROUNDELS)) {
150
192
  utilities[`.${name}`] = {
151
193
  ...maskRules(svgUrl(SHAPES.roundel)),
194
+ "--semy-image": semyTile(SHAPES.roundel),
152
195
  "background-color": `var(--color-${tincture})`,
153
196
  };
154
197
  }
@@ -242,6 +285,22 @@ export default plugin(({ addUtilities, matchUtilities }) => {
242
285
  utilities[`.in-${name}`] = { ".charges > &": atPoint(name) };
243
286
  }
244
287
 
288
+ // The whole field, under the ordinaries and the chief. A custom charge has no tile, so it
289
+ // draws nothing: `none` would mean no mask, and flood the field.
290
+ utilities[".semy"] = {
291
+ "&.charges": { inset: "0", "z-index": "0" },
292
+ "&.charges > *": {
293
+ inset: "0",
294
+ width: "auto",
295
+ margin: "0",
296
+ "aspect-ratio": "auto",
297
+ "mask-image": "var(--semy-image, linear-gradient(transparent, transparent))",
298
+ "mask-size": `calc(100% / ${SEMY_ACROSS}) auto`,
299
+ "mask-repeat": "repeat",
300
+ "mask-position": `${SEMY_SHIFT} ${SEMY_SHIFT}`,
301
+ },
302
+ };
303
+
245
304
  // On a charge, or on `charges` for all of them.
246
305
  utilities[".counterchanged"] = {
247
306
  ".charges > &": counterchanged,
package/data.js CHANGED
@@ -23,6 +23,10 @@ export const DIVISIONS = [
23
23
  "paly-of-eight",
24
24
  ];
25
25
 
26
+ // Ordinaries that can be reversed, e.g. `pile-reversed-or`: the same shape upside down.
27
+ export const REVERSIBLE = ["chevron", "pall", "pile"];
28
+ const reversed = (ordinary) => `${ordinary}-reversed`;
29
+
26
30
  export const ORDINARIES = [
27
31
  "pale",
28
32
  "fess",
@@ -37,6 +41,7 @@ export const ORDINARIES = [
37
41
  "pall",
38
42
  "chief",
39
43
  "pile",
44
+ ...REVERSIBLE.map(reversed),
40
45
  ];
41
46
 
42
47
  export const CHARGES = [
@@ -47,6 +52,14 @@ export const CHARGES = [
47
52
  "billet",
48
53
  "crescent",
49
54
  "fleur-de-lis",
55
+ "cross-couped",
56
+ "cross-potent",
57
+ "cross-pattee",
58
+ "maltese-cross",
59
+ "cross-crosslet",
60
+ "cross-moline",
61
+ "cross-fleury",
62
+ "shakefork",
50
63
  ];
51
64
 
52
65
  // Roundels have their own names by tincture.
@@ -61,6 +74,21 @@ export const ROUNDELS = {
61
74
  // Lines of partition for an ordinary's edge, e.g. `fess-engrailed-or`.
62
75
  export const LINES = ["engrailed", "invected"];
63
76
 
77
+ // Ordinaries that can be voided, e.g. `cross-voided-argent`.
78
+ export const VOIDABLE = [
79
+ "pale",
80
+ "fess",
81
+ "bar",
82
+ "bend",
83
+ "bend-sinister",
84
+ "chevron",
85
+ "cross",
86
+ "saltire",
87
+ "pall",
88
+ "pile",
89
+ ...REVERSIBLE.map(reversed),
90
+ ];
91
+
64
92
  // Each fur's [ground, spot] tinctures.
65
93
  export const FURS = {
66
94
  ermine: ["argent", "sable"],
@@ -90,6 +118,7 @@ export const ARRANGEMENTS = [
90
118
  "in-bend-sinister",
91
119
  "in-chief",
92
120
  "in-base",
121
+ "semy",
93
122
  ];
94
123
 
95
124
  // Where charges go for "between", as [left%, top%], plus their size as % of width.
@@ -128,3 +157,28 @@ export const ON = {
128
157
  pall: { size: 14, counts: { 1: [[50, 50]], 3: [[22, 22], [78, 22], [50, 75]] } },
129
158
  pile: { size: 14, counts: { 1: [[50, 25]], 3: [[50, 14], [50, 34], [50, 54]] } },
130
159
  };
160
+
161
+ const mirror = (points) => points.map(([x, y]) => [x, 100 - y]);
162
+ const mapValues = (object, fn) =>
163
+ Object.fromEntries(Object.entries(object).map(([key, value]) => [key, fn(value)]));
164
+
165
+ for (const ordinary of REVERSIBLE) {
166
+ const { size, points, counts = {} } = BETWEEN[ordinary];
167
+ BETWEEN[reversed(ordinary)] = {
168
+ size,
169
+ points: mirror(points),
170
+ counts: mapValues(counts, (count) => ({ ...count, points: mirror(count.points) })),
171
+ };
172
+ ON[reversed(ordinary)] = {
173
+ size: ON[ordinary].size,
174
+ counts: mapValues(ON[ordinary].counts, mirror),
175
+ };
176
+ }
177
+
178
+ // Mirrored, the lower charges would clip on the heater's curved flanks.
179
+ // prettier-ignore
180
+ BETWEEN["chevron-reversed"] = {
181
+ ...BETWEEN["chevron-reversed"],
182
+ points: [[28, 74], [72, 74], [50, 30]],
183
+ counts: { 6: { size: 12, points: [[26, 78], [50, 89], [74, 78], [38, 34], [62, 34], [50, 16]] } },
184
+ };
package/furs.js CHANGED
@@ -11,7 +11,7 @@ const SPOT = [
11
11
 
12
12
  // A 5:6 tile, the shield's own shape, so whole tiles fill it: a spot at its centre, and quarters of one at each
13
13
  // corner that join up with the neighbouring tiles.
14
- const AT = [
14
+ export const AT = [
15
15
  [25, 30],
16
16
  [0, 0],
17
17
  [50, 0],
package/grailwind.css CHANGED
@@ -120,6 +120,13 @@
120
120
  display: grid;
121
121
  grid-auto-flow: column;
122
122
  grid-auto-columns: 1fr;
123
+
124
+ /* On a shield, each part is a coat of its own: what's in it is placed, stacked and
125
+ * counterchanged within it. */
126
+ &:is(.escutcheon, .escutcheon *) > * {
127
+ position: relative;
128
+ contain: layout;
129
+ }
123
130
  }
124
131
 
125
132
  /* Per fess: split horizontally into chief / base. */
@@ -127,6 +134,12 @@
127
134
  display: grid;
128
135
  grid-auto-flow: row;
129
136
  grid-auto-rows: 1fr;
137
+
138
+ /* See per-pale. */
139
+ &:is(.escutcheon, .escutcheon *) > * {
140
+ position: relative;
141
+ contain: layout;
142
+ }
130
143
  }
131
144
 
132
145
  /* Quarterly: four quarters, numbered 1–4 across then down.
@@ -141,6 +154,12 @@
141
154
  --the-first: var(--field-first);
142
155
  --outer-inset: 0px;
143
156
  }
157
+
158
+ /* See per-pale. */
159
+ &:is(.escutcheon, .escutcheon *) > * {
160
+ position: relative;
161
+ contain: layout;
162
+ }
144
163
  }
145
164
 
146
165
  /* ---------------------------------------------------------------------------
package/ordinaries.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Grailwind CSS: ordinaries, e.g. `fess-or`.
3
3
  */
4
4
  import plugin from "tailwindcss/plugin";
5
- import { BETWEEN, FURS, OF_THE_FIRST, ON, TINCTURES } from "./data.js";
5
+ import { BETWEEN, FURS, OF_THE_FIRST, ON, REVERSIBLE, TINCTURES, VOIDABLE } from "./data.js";
6
6
  import { furBackground } from "./furs.js";
7
7
 
8
8
  // prettier-ignore
@@ -34,6 +34,10 @@ const ORDINARIES = {
34
34
  chief: [[0, 0], [100, 0], [100, 30], [0, 30]],
35
35
  pile: [[20, 0], [80, 0], [50, 85]],
36
36
  };
37
+ // signedArea() follows the flip's winding, so the order stays.
38
+ for (const ordinary of REVERSIBLE) {
39
+ ORDINARIES[`${ordinary}-reversed`] = ORDINARIES[ordinary].map(([x, y]) => [x, 100 - y]);
40
+ }
37
41
 
38
42
  const num = (v) => +v.toFixed(3);
39
43
  const pct = (v) => (num(v) === 0 ? "0" : `${num(v)}%`);
@@ -59,6 +63,7 @@ const outer = ([x, y]) => {
59
63
  // A y in % of the field, as % of the whole shield when a chief takes the top of it.
60
64
  const belowChief = (y) =>
61
65
  y === 100 ? "100%" : `calc(${num(y)}% + var(--field-top, 0%) * ${num(1 - y / 100)})`;
66
+ const pointBelowChief = ([x, y]) => `${pct(x)} ${belowChief(y)}`;
62
67
 
63
68
  // Fimbriation: the ordinary's clip is widened and filled with the fimbriation
64
69
  // tincture, and the ordinary's own tincture is painted over it as gradient
@@ -86,15 +91,15 @@ const offsetLine = (a, b, outward, distance) => {
86
91
  return { n, c: n[0] * ax + n[1] * ay + distance };
87
92
  };
88
93
 
89
- // How far each vertex moves, in %, when fimbriated. Sides on the shield's edge
90
- // don't move, so their vertices slide along it.
91
- const widen = (vertices) => {
94
+ // How far each vertex moves, in %, when the sides move out by `distance` (in by a negative one).
95
+ // Sides on the shield's edge don't move, so their vertices slide along it.
96
+ const widen = (vertices, distance = FIMBRIATION) => {
92
97
  const outward = Math.sign(signedArea(vertices));
93
98
  const count = vertices.length;
94
99
  const line = (i) => {
95
100
  const a = vertices[i];
96
101
  const b = vertices[(i + 1) % count];
97
- return offsetLine(a, b, outward, onShieldEdge(a, b) ? 0 : FIMBRIATION);
102
+ return offsetLine(a, b, outward, onShieldEdge(a, b) ? 0 : distance);
98
103
  };
99
104
  return vertices.map((v, i) => {
100
105
  const p = line((i - 1 + count) % count);
@@ -106,6 +111,23 @@ const widen = (vertices) => {
106
111
  });
107
112
  };
108
113
 
114
+ const rotate = (list, start) => [...list.slice(start), ...list.slice(0, start)];
115
+
116
+ // The ordinary with a hole the shape of it shrunk by `distance`, cut with evenodd.
117
+ const voidedPolygon = (vertices, distance, format = point) => {
118
+ const offsets = widen(vertices, -distance);
119
+ const inner = vertices.map(([x, y], i) => [x + offsets[i][0], y + offsets[i][1]]);
120
+ // Start and end at the shield's edge so we don't draw through it.
121
+ const edge = vertices.findIndex((v, i) => onShieldEdge(v, vertices[(i + 1) % vertices.length]));
122
+ if (edge === -1) {
123
+ throw new Error("a voided ordinary needs a side on the shield's edge");
124
+ }
125
+ const outerStr = [...rotate(vertices, edge), vertices[edge]].map(format).join(", ");
126
+ const innerStr = [...rotate(inner, edge), inner[edge]].map(format).join(", ");
127
+
128
+ return `polygon(evenodd, ${outerStr}, ${innerStr})`;
129
+ };
130
+
109
131
  const shift = (v, d) =>
110
132
  num(d) === 0
111
133
  ? pct(v)
@@ -162,6 +184,28 @@ const BANDS = {
162
184
  pile: [half("u-v", "end", [20, 0, 50, 85]), half("u+v", "start", [50, 0, 80, 85]), seam(83)],
163
185
  };
164
186
 
187
+ // The bands upside down. A diagonal swaps kind, as x − y becomes x + (100 − y) − 100. A half's
188
+ // stops stay: its 50% line is the flipped tile's other diagonal, which is the flipped side.
189
+ const reverseBand = ({ kind, from, to, tile, ...rest }) => {
190
+ const layer = { ...rest, tile: tile && [tile[0], 100 - tile[3], tile[2], 100 - tile[1]] };
191
+ if (from === undefined) {
192
+ return { ...layer, kind: { "u-v": "u+v", "u+v": "u-v" }[kind] ?? kind };
193
+ }
194
+ switch (kind) {
195
+ case "x":
196
+ return { ...layer, kind, from, to };
197
+ case "y":
198
+ return { ...layer, kind, from: 100 - to, to: 100 - from };
199
+ case "u-v":
200
+ return { ...layer, kind: "u+v", from: from + 100, to: to + 100 };
201
+ case "u+v":
202
+ return { ...layer, kind: "u-v", from: from - 100, to: to - 100 };
203
+ }
204
+ };
205
+ for (const ordinary of REVERSIBLE) {
206
+ BANDS[`${ordinary}-reversed`] = BANDS[ordinary].map(reverseBand);
207
+ }
208
+
165
209
  const DIRECTION = {
166
210
  x: "to right",
167
211
  y: "to bottom",
@@ -223,6 +267,19 @@ const bands = (layers, colour) => ({
223
267
  // box is shorter, so they are slightly squashed.
224
268
  const SCALLOP = 14;
225
269
  const SCALLOPS = { chevron: 10 };
270
+
271
+ // A quarter of the ordinary's width, in real units. The pile tapers, so its is set by eye.
272
+ // prettier-ignore
273
+ const VOIDS = {
274
+ pale: 7.5, fess: 9, bar: 4.2, bend: 7.7, "bend-sinister": 7.7,
275
+ chevron: 3.8, cross: 5, saltire: 5.8, pall: 5, pile: 8,
276
+ };
277
+ for (const ordinary of REVERSIBLE) {
278
+ if (SCALLOPS[ordinary]) {
279
+ SCALLOPS[`${ordinary}-reversed`] = SCALLOPS[ordinary];
280
+ }
281
+ VOIDS[`${ordinary}-reversed`] = VOIDS[ordinary];
282
+ }
226
283
  const DEPTH = 0.35;
227
284
 
228
285
  const at = ([x, y]) => point([(x * 100) / W, (y * 100) / H]);
@@ -376,10 +433,11 @@ const PAINTS = [
376
433
  ];
377
434
 
378
435
  export default plugin(({ addBase, addUtilities, theme }) => {
379
- // Unset on an inescutcheon or canton, so it can't inherit its shield's layout. In the base
380
- // layer, so an ordinary on it still sets its own.
436
+ // Unset on an inescutcheon, a canton or a part of a divided shield, so it can't inherit its
437
+ // shield's layout. In the base layer, so an ordinary on it still sets its own.
381
438
  addBase({
382
- ".inescutcheon, .canton": Object.fromEntries(layoutVars().map((name) => [name, "initial"])),
439
+ ".inescutcheon, .canton, :is(.escutcheon, .escutcheon *):is(.quarterly, .per-pale, .per-fess) > *":
440
+ Object.fromEntries(layoutVars().map((name) => [name, "initial"])),
383
441
  });
384
442
 
385
443
  const utilities = {};
@@ -429,7 +487,7 @@ export default plugin(({ addBase, addUtilities, theme }) => {
429
487
 
430
488
  // Covers the whole shield, so the swapped field lines up with the real one,
431
489
  // and the clip moves the ordinary below any chief instead.
432
- utilities[`.${ordinary}-counterchanged`] = ordinaryRules(ordinary, {
490
+ const counterchanged = {
433
491
  ...(chief ? OVER_BORDURE : { inset: "0" }),
434
492
  // Registered not to inherit, so a shield inside another can't pick up its swap.
435
493
  ...Object.fromEntries(
@@ -445,9 +503,10 @@ export default plugin(({ addBase, addUtilities, theme }) => {
445
503
  "background-repeat": "var(--counterchange-repeat, repeat)",
446
504
  // The swapped field's tiles would wrap into the bordure's strips, so the bordure shows there.
447
505
  ...(chief && { "background-clip": "content-box" }),
448
- "clip-path": chief
449
- ? plain
450
- : `polygon(${vertices.map(([x, y]) => `${pct(x)} ${belowChief(y)}`).join(", ")})`,
506
+ };
507
+ utilities[`.${ordinary}-counterchanged`] = ordinaryRules(ordinary, {
508
+ ...counterchanged,
509
+ "clip-path": chief ? plain : `polygon(${vertices.map(pointBelowChief).join(", ")})`,
451
510
  });
452
511
 
453
512
  for (const [tincture, colour] of PAINTS) {
@@ -490,6 +549,29 @@ export default plugin(({ addBase, addUtilities, theme }) => {
490
549
  });
491
550
  }
492
551
  }
552
+
553
+ // Not fimbriated.
554
+ if (VOIDABLE.includes(ordinary)) {
555
+ const clip = voidedPolygon(vertices, VOIDS[ordinary]);
556
+ for (const [tincture, colour] of PAINTS) {
557
+ utilities[`.${ordinary}-voided-${tincture}`] = ordinaryRules(ordinary, {
558
+ ...placed,
559
+ "background-color": colour,
560
+ "clip-path": clip,
561
+ });
562
+ }
563
+ for (const fur of Object.keys(FURS)) {
564
+ utilities[`.${ordinary}-voided-${fur}`] = ordinaryRules(ordinary, {
565
+ ...placed,
566
+ ...furBackground(fur, theme),
567
+ "clip-path": clip,
568
+ });
569
+ }
570
+ utilities[`.${ordinary}-voided-counterchanged`] = ordinaryRules(ordinary, {
571
+ ...counterchanged,
572
+ "clip-path": voidedPolygon(vertices, VOIDS[ordinary], pointBelowChief),
573
+ });
574
+ }
493
575
  }
494
576
 
495
577
  addUtilities(utilities);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grailwind",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",
@@ -26,7 +26,7 @@
26
26
  "license": "MIT",
27
27
  "description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
28
28
  "devDependencies": {
29
- "@grailwind/eslint-plugin": "^0.1.0-beta.1",
29
+ "@grailwind/eslint-plugin": "^0.1.0-beta.2",
30
30
  "@html-eslint/parser": "^0.67.0",
31
31
  "@tailwindcss/cli": "^4.3.3",
32
32
  "eslint": "^10.14.0",