grailwind 0.6.0 → 0.7.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,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-11-09
10
+
11
+ ### Added
12
+
13
+ - `per-chevron-*`: a field divided along a chevron's centre line, the first
14
+ tincture in chief.
15
+ - `bendy-*`, `bendy-sinister-*`, `chevronny-*` (each with `-of-eight-*`) and
16
+ `chequy-*`: six or eight pieces, or five squares by six.
17
+
18
+ ### Changed
19
+
20
+ - `@grailwind/eslint-plugin` 0.1.0-beta.3 needs `grailwind` 0.7.x.
21
+
22
+ ### Fixed
23
+
24
+ - On a shield, `quarterly` and two-part `per-fess` split at the fess point, as
25
+ the divisions do, not halfway down.
26
+ - No seam down the centre of `per-saltire-*` at odd widths.
27
+
9
28
  ## [0.6.0] - 2026-11-06
10
29
 
11
30
  ### Added
@@ -138,7 +157,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
138
157
  Initial release: tinctures, divisions, ordinaries, charges, strict mode and
139
158
  custom SVG charges.
140
159
 
141
- [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.6.0...HEAD
160
+ [Unreleased]: https://github.com/pafirmin/grailwind/compare/v0.7.0...HEAD
161
+ [0.7.0]: https://github.com/pafirmin/grailwind/compare/v0.6.0...v0.7.0
142
162
  [0.6.0]: https://github.com/pafirmin/grailwind/compare/v0.5.0...v0.6.0
143
163
  [0.5.0]: https://github.com/pafirmin/grailwind/compare/v0.4.0...v0.5.0
144
164
  [0.4.0]: https://github.com/pafirmin/grailwind/compare/v0.3.0...v0.4.0
package/README.md CHANGED
@@ -57,8 +57,8 @@ tinctures replace Tailwind's colours entirely.
57
57
  - **The shield:** `escutcheon`, plus `bordure-*`, `inescutcheon` (a small shield at the centre)
58
58
  and `canton` (a ninth of the shield in dexter chief), each with its own field and charges.
59
59
  - **Divided fields:** `per-pale-or-gules`, `quarterly-azure-argent`,
60
- `gyronny-or-sable`, `barry-argent-azure` and more, or `per-pale`, `per-fess` and `quarterly` for
61
- layout.
60
+ `gyronny-or-sable`, `barry-argent-azure`, `bendy-argent-gules`, `chequy-or-azure` and more, or
61
+ `per-pale`, `per-fess` and `quarterly` for layout.
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,
package/data.js CHANGED
@@ -13,6 +13,7 @@ export const DIVISIONS = [
13
13
  "per-fess",
14
14
  "per-bend",
15
15
  "per-bend-sinister",
16
+ "per-chevron",
16
17
  "per-saltire",
17
18
  "quarterly",
18
19
  "gyronny",
@@ -21,6 +22,13 @@ export const DIVISIONS = [
21
22
  "barry-of-ten",
22
23
  "paly",
23
24
  "paly-of-eight",
25
+ "bendy",
26
+ "bendy-of-eight",
27
+ "bendy-sinister",
28
+ "bendy-sinister-of-eight",
29
+ "chevronny",
30
+ "chevronny-of-eight",
31
+ "chequy",
24
32
  ];
25
33
 
26
34
  // Ordinaries that can be reversed, e.g. `pile-reversed-or`: the same shape upside down.
package/divisions.js CHANGED
@@ -22,6 +22,57 @@ const stripes = (to, pieces) => (a, b) => {
22
22
  };
23
23
  };
24
24
 
25
+ // How much of the corner-to-corner extent the shield covers: the base corner is cut away by the
26
+ // border-radius of 0 0 50% 50% / 0 0 45% 45% and the 5:6 aspect ratio in grailwind.css. It runs
27
+ // from the chief corner the bands start in to the tangent on the base curve.
28
+ const BENDY_VISIBLE = (() => {
29
+ const [width, height] = [5, 6];
30
+ const [radiusX, radiusY] = [width / 2, 0.45 * height];
31
+ const [centreX, centreY] = [width / 2, height - radiusY];
32
+ const tangent = Math.sqrt((height * radiusX) ** 2 + (width * radiusY) ** 2);
33
+ return (width * height - height * centreX + width * centreY + tangent) / (2 * width * height);
34
+ })();
35
+
36
+ // Diagonal bands parallel to per bend, `pieces` equal ones across the visible part of the shield,
37
+ // the last running to the edge. Explicit stops, not a repeating gradient, so a canton's square
38
+ // corners don't show the first tincture again.
39
+ const bendy = (to, pieces) => (a, b) => {
40
+ const stops = [];
41
+ for (let i = 0; i < pieces; i++) {
42
+ const start = i === 0 ? 0 : ((i * BENDY_VISIBLE * 100) / pieces).toFixed(2);
43
+ const end = i === pieces - 1 ? 100 : (((i + 1) * BENDY_VISIBLE * 100) / pieces).toFixed(2);
44
+ stops.push(`${i % 2 === 0 ? a : b} ${start}% ${end}%`);
45
+ }
46
+ return layer(`linear-gradient(to ${to}, ${stops.join(", ")})`);
47
+ };
48
+
49
+ // The direction of `to bottom right` on the shield (aspect-ratio 5 / 6 in grailwind.css), so
50
+ // chevronny's lines run parallel to per chevron's, the field's diagonals.
51
+ const CHEVRON_ANGLE = 180 - (Math.atan(6 / 5) * 180) / Math.PI;
52
+
53
+ // A half box is 75% as long as the whole along this angle (45/sqrt(61) over 60/sqrt(61)).
54
+ // Plain, the sinister half sits over a full-width dexter layer, which hides the centre seam.
55
+ // With a fur the layers can't overlap, as the transparent stripes would show the layer below.
56
+ const chevronny = (pieces) => (a, b, fur) => {
57
+ const stripe = (angle, length) =>
58
+ `repeating-linear-gradient(${angle}deg, ${a} 0 calc(${length}% / ${pieces}), ${b} 0 calc(${2 * length}% / ${pieces}))`;
59
+ const sinister = stripe(-CHEVRON_ANGLE, 100);
60
+
61
+ return fur
62
+ ? {
63
+ "background-image": [stripe(CHEVRON_ANGLE, 100), sinister].join(", "),
64
+ "background-size": "50% 100%",
65
+ "background-position": "left, right",
66
+ "background-repeat": "no-repeat",
67
+ }
68
+ : {
69
+ "background-image": [sinister, stripe(CHEVRON_ANGLE, 75)].join(", "),
70
+ "background-size": "50% 100%, 100% 100%",
71
+ "background-position": "right, left",
72
+ "background-repeat": "no-repeat",
73
+ };
74
+ };
75
+
25
76
  const DIVISIONS = {
26
77
  // Vertical split: first on the dexter (viewer's left).
27
78
  "per-pale": (a, b) => layer(`linear-gradient(to right, ${a} 50%, ${b} 50%)`),
@@ -36,18 +87,39 @@ const DIVISIONS = {
36
87
  // Line from sinister chief to dexter base: first in the upper half.
37
88
  "per-bend-sinister": (a, b) => layer(`linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`),
38
89
 
90
+ "per-chevron": (a, b) =>
91
+ a === "transparent"
92
+ ? {
93
+ "background-image": [
94
+ `linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`,
95
+ `linear-gradient(to bottom left, ${a} 50%, ${b} 50%)`,
96
+ `linear-gradient(${b}, ${b})`,
97
+ ].join(", "),
98
+ "background-size": "50% 50%, 50% 50%, 100% 15%",
99
+ "background-position": "left 70%, right 70%, bottom",
100
+ "background-repeat": "no-repeat",
101
+ }
102
+ : {
103
+ "background-image": [
104
+ `linear-gradient(to bottom right, ${a} 42.5%, transparent 42.5%)`,
105
+ `linear-gradient(to bottom left, ${a} 42.5%, ${b} 42.5%)`,
106
+ ].join(", "),
107
+ },
108
+
39
109
  // Both diagonals, crossing at the centre like the saltire: first in chief and base.
40
- // Gyronny's four tiles, with the top-right and bottom-left swapped.
110
+ // Gyronny's four tiles, with the top-right and bottom-left swapped. Two strips under them fill
111
+ // the sub-pixel seam where the tiles meet: the first tincture down, the second across.
41
112
  "per-saltire": (a, b) => ({
42
- "background-color": b,
43
113
  "background-image": [
44
114
  `linear-gradient(to bottom left, ${a} 50%, ${b} 50%)`,
45
115
  `linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`,
46
116
  `linear-gradient(to bottom left, ${b} 50%, ${a} 50%)`,
47
117
  `linear-gradient(to bottom right, ${b} 50%, ${a} 50%)`,
118
+ `linear-gradient(${a}, ${a})`,
119
+ `linear-gradient(${b}, ${b})`,
48
120
  ].join(", "),
49
- "background-size": "50% 50%",
50
- "background-position": "left top, right top, right bottom, left bottom",
121
+ "background-size": "50% 50%, 50% 50%, 50% 50%, 50% 50%, 2px 100%, 100% 2px",
122
+ "background-position": "left top, right top, right bottom, left bottom, center, center",
51
123
  "background-repeat": "no-repeat",
52
124
  }),
53
125
 
@@ -83,22 +155,42 @@ const DIVISIONS = {
83
155
  "barry-of-ten": stripes("bottom", 10),
84
156
  paly: stripes("right", 6),
85
157
  "paly-of-eight": stripes("right", 8),
158
+
159
+ // Bendy has the first tincture in the sinister chief corner, bendy sinister in the dexter.
160
+ bendy: bendy("bottom left", 6),
161
+ "bendy-of-eight": bendy("bottom left", 8),
162
+ "bendy-sinister": bendy("bottom right", 6),
163
+ "bendy-sinister-of-eight": bendy("bottom right", 8),
164
+
165
+ // The first tincture in the chief corners.
166
+ chevronny: chevronny(6),
167
+ "chevronny-of-eight": chevronny(8),
168
+
169
+ // 5 across and 6 down, the first tincture in the dexter chief square. Conic starts at the top.
170
+ // The repeat must be explicit, as with stripes.
171
+ chequy: (a, b) => ({
172
+ "background-image": `conic-gradient(${b} 0 25%, ${a} 0 50%, ${b} 0 75%, ${a} 0)`,
173
+ "background-size": "40% calc(100% / 3)",
174
+ "background-repeat": "repeat",
175
+ }),
86
176
  };
87
177
 
88
178
  // A division's background rules as one { image, size, position, repeat } per layer.
89
179
  const toLayers = (rules) => {
90
- return rules["background-image"].split(/, (?=linear-gradient)/).map((gradient, i) => {
91
- const size = (rules["background-size"] ?? "100% 100%").split(", ");
92
- const position = (rules["background-position"] ?? "0 0").split(", ");
93
- const repeat = (rules["background-repeat"] ?? "no-repeat").split(", ");
94
-
95
- return {
96
- image: gradient,
97
- size: size[i % size.length],
98
- position: position[i % position.length],
99
- repeat: repeat[i % repeat.length],
100
- };
101
- });
180
+ return rules["background-image"]
181
+ .split(/, (?=(?:repeating-)?(?:linear|conic)-gradient)/)
182
+ .map((gradient, i) => {
183
+ const size = (rules["background-size"] ?? "100% 100%").split(", ");
184
+ const position = (rules["background-position"] ?? "0 0").split(", ");
185
+ const repeat = (rules["background-repeat"] ?? "no-repeat").split(", ");
186
+
187
+ return {
188
+ image: gradient,
189
+ size: size[i % size.length],
190
+ position: position[i % position.length],
191
+ repeat: repeat[i % repeat.length],
192
+ };
193
+ });
102
194
  };
103
195
 
104
196
  // A division with one fur: the fur's part is left transparent, over the fur's own layers.
package/grailwind.css CHANGED
@@ -135,10 +135,18 @@
135
135
  grid-auto-flow: row;
136
136
  grid-auto-rows: 1fr;
137
137
 
138
- /* See per-pale. */
139
- &:is(.escutcheon, .escutcheon *) > * {
140
- position: relative;
141
- contain: layout;
138
+ /* On a shield, two parts split at the fess point, FESS_POINT in divisions.js.
139
+ * More parts stay equal. */
140
+ &:is(.escutcheon, .escutcheon *) {
141
+ &:has(> :last-child:nth-child(2)) {
142
+ grid-template-rows: 45fr 55fr;
143
+ }
144
+
145
+ /* See per-pale. */
146
+ & > * {
147
+ position: relative;
148
+ contain: layout;
149
+ }
142
150
  }
143
151
  }
144
152
 
@@ -155,10 +163,15 @@
155
163
  --outer-inset: 0px;
156
164
  }
157
165
 
158
- /* See per-pale. */
159
- &:is(.escutcheon, .escutcheon *) > * {
160
- position: relative;
161
- contain: layout;
166
+ /* On a shield, split at the fess point, as per-fess. */
167
+ &:is(.escutcheon, .escutcheon *) {
168
+ grid-template-rows: 45fr 55fr;
169
+
170
+ /* See per-pale. */
171
+ & > * {
172
+ position: relative;
173
+ contain: layout;
174
+ }
162
175
  }
163
176
  }
164
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grailwind",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
@@ -9,7 +9,8 @@
9
9
  "dev:watch": "tailwindcss -i docs/app.css -o docs/site.css --watch=always & python3 -m http.server ${PORT:-8000} -d docs",
10
10
  "lint": "eslint .",
11
11
  "format": "prettier --write .",
12
- "format:check": "prettier --check ."
12
+ "format:check": "prettier --check .",
13
+ "prepare": "husky"
13
14
  },
14
15
  "keywords": [
15
16
  "tailwindcss",
@@ -26,10 +27,12 @@
26
27
  "license": "MIT",
27
28
  "description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
28
29
  "devDependencies": {
29
- "@grailwind/eslint-plugin": "^0.1.0-beta.2",
30
+ "@grailwind/eslint-plugin": "^0.1.0-beta.3",
30
31
  "@html-eslint/parser": "^0.67.0",
31
32
  "@tailwindcss/cli": "^4.3.3",
32
33
  "eslint": "^10.14.0",
34
+ "husky": "^9.1.7",
35
+ "lint-staged": "^17.6.0",
33
36
  "playwright": "~1.64.1",
34
37
  "prettier": "^3.9.10",
35
38
  "tailwindcss": "^4.3.3"
@@ -71,5 +74,8 @@
71
74
  },
72
75
  "bugs": {
73
76
  "url": "https://github.com/pafirmin/grailwind/issues"
77
+ },
78
+ "lint-staged": {
79
+ "*": "prettier --write --ignore-unknown"
74
80
  }
75
81
  }