grailwind 0.0.0-stage → 0.2.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 +27 -0
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/charges.js +194 -0
- package/divisions.js +69 -0
- package/grailwind.css +111 -0
- package/ordinaries.js +256 -0
- package/package.json +54 -4
- package/palette-reset.css +4 -0
- package/strict.css +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-11-01
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Charges `on` an ordinary: `charges on` places one, two, three or five charges
|
|
12
|
+
along the pale, fess, bend, bend sinister, chevron, cross, saltire or pall.
|
|
13
|
+
- Fimbriation: `fimbriated-<tincture>` edges an ordinary with a narrow border,
|
|
14
|
+
e.g. `cross-azure fimbriated-argent`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Charges at `in-dexter-chief`, `in-chief` and `in-sinister-chief` now sit on
|
|
19
|
+
the chief when one is present, and charges are drawn above it.
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-10-31
|
|
22
|
+
|
|
23
|
+
Initial release: tinctures, divisions, ordinaries, charges, strict mode and
|
|
24
|
+
custom SVG charges.
|
|
25
|
+
|
|
26
|
+
[0.2.0]: https://github.com/pafirmin/grailwind/compare/v0.1.0...v0.2.0
|
|
27
|
+
[0.1.0]: https://github.com/pafirmin/grailwind/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Firmin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Grailwind CSS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Heraldic blazon for [Tailwind CSS](https://tailwindcss.com). Draw coats of
|
|
6
|
+
arms, or style anything else, in the language heralds have used for 800 years:
|
|
7
|
+
|
|
8
|
+
```html
|
|
9
|
+
<div class="escutcheon azure chevron-or">
|
|
10
|
+
<div class="charges between">
|
|
11
|
+
<i class="mullet argent"></i>
|
|
12
|
+
<i class="mullet argent"></i>
|
|
13
|
+
<i class="mullet argent"></i>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's *Azure, a chevron Or between three mullets Argent*: a blue shield, a
|
|
19
|
+
gold chevron, and three silver stars around it.
|
|
20
|
+
|
|
21
|
+
**[Read the documentation](https://pafirmin.github.io/grailwind/)** for every class, with live examples.
|
|
22
|
+
|
|
23
|
+
> Grailwind is a light-hearted project at version 0.x, so class names may
|
|
24
|
+
> still change.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Grailwind is a plugin for Tailwind CSS v4.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install -D grailwind
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Import it after Tailwind:
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
@import "tailwindcss";
|
|
38
|
+
@import "grailwind";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This adds the heraldic tinctures (`or`, `argent`, `gules`, `azure`, `vert`,
|
|
42
|
+
`purpure`, `sable`) alongside Tailwind's own colours.
|
|
43
|
+
|
|
44
|
+
For a more authentic experience, import `grailwind/strict` instead, and the
|
|
45
|
+
tinctures replace Tailwind's colours entirely.
|
|
46
|
+
|
|
47
|
+
## What's included
|
|
48
|
+
|
|
49
|
+
- **Tinctures:** a tincture on its own sets the background (`azure`), and
|
|
50
|
+
Tailwind's prefixes work as usual (`text-or`).
|
|
51
|
+
- **The shield:** `escutcheon`, plus `bordure-*`.
|
|
52
|
+
- **Divided fields:** `per-pale-or-gules`, `quarterly-azure-argent`,
|
|
53
|
+
`gyronny-or-sable` and more, or `per-pale`, `per-fess` and `quarterly` for
|
|
54
|
+
layout.
|
|
55
|
+
- **Ordinaries:** `fess-or`, `chevron-gules`, `cross-argent`, `chief-azure`
|
|
56
|
+
and more, with `fimbriated-*` for a narrow border.
|
|
57
|
+
- **Charges:** `mullet`, `fleur-de-lis`, `bezant` and more, arranged by
|
|
58
|
+
number, `in-pale`, `in-fess`, `in-bend`, `on` or `between` the ordinary, or
|
|
59
|
+
placed at a named point such as `in-dexter-chief`, and sized with
|
|
60
|
+
`charge-size-*`.
|
|
61
|
+
- **Your own charges:** any SVG, via `charge-[url(…)]`.
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm install
|
|
67
|
+
npm test # checks both the default and strict builds
|
|
68
|
+
npm run dev:watch # rebuilds the docs site on change and serves it at http://localhost:8000
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Licence
|
|
72
|
+
|
|
73
|
+
[MIT](LICENSE)
|
package/charges.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind CSS: charges and their arrangement.
|
|
3
|
+
*/
|
|
4
|
+
import plugin from "tailwindcss/plugin";
|
|
5
|
+
|
|
6
|
+
const star = (cx, cy, r, points = 5) => {
|
|
7
|
+
const coords = [];
|
|
8
|
+
for (let i = 0; i < points * 2; i++) {
|
|
9
|
+
const radius = i % 2 ? r * 0.382 : r;
|
|
10
|
+
const angle = (Math.PI / points) * i - Math.PI / 2;
|
|
11
|
+
coords.push(`${cx + radius * Math.cos(angle)},${cy + radius * Math.sin(angle)}`);
|
|
12
|
+
}
|
|
13
|
+
return `<polygon points="${coords.join(" ")}"/>`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SHAPES = {
|
|
17
|
+
roundel: `<circle cx="50" cy="50" r="46"/>`,
|
|
18
|
+
annulet: `<circle cx="50" cy="50" r="39" fill="none" stroke="#000" stroke-width="14"/>`,
|
|
19
|
+
mullet: star(50, 53, 48),
|
|
20
|
+
lozenge: `<polygon points="50,2 84,50 50,98 16,50"/>`,
|
|
21
|
+
billet: `<rect x="28" y="4" width="44" height="92"/>`,
|
|
22
|
+
crescent: `<mask id="m"><rect width="100" height="100" fill="#fff"/><circle cx="50" cy="35" r="35"/></mask><circle cx="50" cy="54" r="42" mask="url(#m)"/>`,
|
|
23
|
+
"fleur-de-lis": `
|
|
24
|
+
<path d="M50 2C61 16 64 36 55 56H45C36 36 39 16 50 2Z"/>
|
|
25
|
+
<path d="M55 56C57 38 70 24 84 26C97 29 99 46 89 52C92 44 87 37 80 39C71 42 66 50 66 56Z"/>
|
|
26
|
+
<path d="M45 56C43 38 30 24 16 26C3 29 1 46 11 52C8 44 13 37 20 39C29 42 34 50 34 56Z"/>
|
|
27
|
+
<rect x="28" y="56" width="44" height="9" rx="2"/>
|
|
28
|
+
<path d="M45 65H55C55 74 53 82 50 90C47 82 45 74 45 65Z"/>
|
|
29
|
+
<path d="M58 65C62 74 72 80 84 78C80 88 64 90 56 74Z"/>
|
|
30
|
+
<path d="M42 65C38 74 28 80 16 78C20 88 36 90 44 74Z"/>`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Roundels have their own names by tincture.
|
|
34
|
+
const ROUNDELS = {
|
|
35
|
+
bezant: "or",
|
|
36
|
+
plate: "argent",
|
|
37
|
+
torteau: "gules",
|
|
38
|
+
hurt: "azure",
|
|
39
|
+
pellet: "sable",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Points as [left%, top%] of the shield, plus a charge size as % of its width.
|
|
43
|
+
// The default for each number of charges, e.g. three are placed 2 and 1.
|
|
44
|
+
const DEFAULTS = {
|
|
45
|
+
1: { size: 50, points: [[50, 45]] },
|
|
46
|
+
2: { size: 30, points: [[50, 24], [50, 66]] },
|
|
47
|
+
3: { size: 28, points: [[28, 25], [72, 25], [50, 64]] },
|
|
48
|
+
4: { size: 24, points: [[30, 24], [70, 24], [32, 60], [68, 60]] },
|
|
49
|
+
6: { size: 20, points: [[20, 18], [50, 18], [80, 18], [35, 42], [65, 42], [50, 66]] },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// The points of the shield, for placing a single charge, e.g. `in-dexter-chief`.
|
|
53
|
+
const POINTS = {
|
|
54
|
+
"dexter-chief": [25, 17],
|
|
55
|
+
chief: [50, 15],
|
|
56
|
+
"sinister-chief": [75, 17],
|
|
57
|
+
"honour-point": [50, 30],
|
|
58
|
+
"fess-point": [50, 45],
|
|
59
|
+
"nombril-point": [50, 60],
|
|
60
|
+
"dexter-base": [30, 72],
|
|
61
|
+
base: [50, 84],
|
|
62
|
+
"sinister-base": [70, 72],
|
|
63
|
+
};
|
|
64
|
+
const POINT_SIZE = 22;
|
|
65
|
+
|
|
66
|
+
// With a chief on the shield, these points sit on it (see ordinaries.js).
|
|
67
|
+
const CHIEF_POINTS = new Set(["dexter-chief", "chief", "sinister-chief"]);
|
|
68
|
+
|
|
69
|
+
const ARRANGEMENTS = {
|
|
70
|
+
"in-pale": {
|
|
71
|
+
2: { size: 30, points: [[50, 24], [50, 66]] },
|
|
72
|
+
3: { size: 22, points: [[50, 16], [50, 43], [50, 70]] },
|
|
73
|
+
},
|
|
74
|
+
"in-fess": {
|
|
75
|
+
2: { size: 30, points: [[30, 45], [70, 45]] },
|
|
76
|
+
3: { size: 24, points: [[22, 45], [50, 45], [78, 45]] },
|
|
77
|
+
},
|
|
78
|
+
"in-bend": {
|
|
79
|
+
2: { size: 28, points: [[30, 26], [70, 64]] },
|
|
80
|
+
3: { size: 22, points: [[22, 16], [50, 45], [78, 74]] },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const svgUrl = (shape) =>
|
|
85
|
+
`url("data:image/svg+xml,${encodeURIComponent(
|
|
86
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${shape}</svg>`,
|
|
87
|
+
)}")`;
|
|
88
|
+
|
|
89
|
+
const maskRules = (image) => ({
|
|
90
|
+
"mask-image": image,
|
|
91
|
+
"mask-size": "contain",
|
|
92
|
+
"mask-repeat": "no-repeat",
|
|
93
|
+
"mask-position": "center",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const place = ([left, top], size) => ({
|
|
97
|
+
left: `${left}%`,
|
|
98
|
+
top: `${top}%`,
|
|
99
|
+
width: `var(--charge-size, ${size}%)`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const hasCount = (n) => `:has(> :nth-child(${n}):last-child)`;
|
|
103
|
+
|
|
104
|
+
export default plugin(({ addUtilities, matchUtilities }) => {
|
|
105
|
+
// User-defined charges from images
|
|
106
|
+
matchUtilities({ charge: maskRules }, { type: ["url"] });
|
|
107
|
+
|
|
108
|
+
const utilities = {};
|
|
109
|
+
|
|
110
|
+
for (const [name, shape] of Object.entries(SHAPES)) {
|
|
111
|
+
utilities[`.${name}`] = maskRules(svgUrl(shape));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [name, tincture] of Object.entries(ROUNDELS)) {
|
|
115
|
+
utilities[`.${name}`] = {
|
|
116
|
+
...maskRules(svgUrl(SHAPES.roundel)),
|
|
117
|
+
"background-color": `var(--color-${tincture})`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Defaults use :where() so arrangements and `between` always win.
|
|
122
|
+
const charges = {
|
|
123
|
+
position: "absolute",
|
|
124
|
+
inset: "var(--field-top, 0%) 0 0",
|
|
125
|
+
// Above the chief, which is painted last, so charges can sit on it.
|
|
126
|
+
"z-index": "1",
|
|
127
|
+
"& > *": {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
"aspect-ratio": "1",
|
|
130
|
+
translate: "-50% -50%",
|
|
131
|
+
"object-fit": "contain",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
for (const [count, { size, points }] of Object.entries(DEFAULTS)) {
|
|
135
|
+
points.forEach((point, i) => {
|
|
136
|
+
charges[`&:where(${hasCount(count)}) > :where(:nth-child(${i + 1}))`] =
|
|
137
|
+
place(point, size);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
utilities[".charges"] = charges;
|
|
141
|
+
|
|
142
|
+
for (const [name, counts] of Object.entries(ARRANGEMENTS)) {
|
|
143
|
+
const arrangement = {};
|
|
144
|
+
for (const [count, { size, points }] of Object.entries(counts)) {
|
|
145
|
+
points.forEach((point, i) => {
|
|
146
|
+
arrangement[`&${hasCount(count)} > :nth-child(${i + 1})`] = place(point, size);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
utilities[`.${name}`] = arrangement;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Positions come from the ordinary on the shield (see ordinaries.js).
|
|
153
|
+
const between = {};
|
|
154
|
+
for (let i = 1; i <= 4; i++) {
|
|
155
|
+
between[`& > :nth-child(${i})`] = {
|
|
156
|
+
left: `var(--between-${i}-x)`,
|
|
157
|
+
top: `var(--between-${i}-y)`,
|
|
158
|
+
width: "var(--charge-size, var(--between-size))",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
utilities[".between"] = between;
|
|
162
|
+
|
|
163
|
+
// Falls back to the plain-field default when the ordinary has no points for this count.
|
|
164
|
+
// An undefined var() would otherwise unset `left`/`top` instead of deferring to the :where() default.
|
|
165
|
+
const on = {};
|
|
166
|
+
for (const count of [1, 2, 3, 5]) {
|
|
167
|
+
const fallback = DEFAULTS[count];
|
|
168
|
+
for (let i = 1; i <= count; i++) {
|
|
169
|
+
const [left, top] = fallback?.points[i - 1] ?? [];
|
|
170
|
+
const at = (axis, value) =>
|
|
171
|
+
value === undefined ? `var(--on-${count}-${i}-${axis})` : `var(--on-${count}-${i}-${axis}, ${value}%)`;
|
|
172
|
+
on[`&${hasCount(count)} > :nth-child(${i})`] = {
|
|
173
|
+
left: at("x", left),
|
|
174
|
+
top: at("y", top),
|
|
175
|
+
width: `var(--charge-size, var(--on-size, ${fallback?.size ?? 16}%))`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
utilities[".on"] = on;
|
|
180
|
+
|
|
181
|
+
for (const [name, [left, top]] of Object.entries(POINTS)) {
|
|
182
|
+
utilities[`.in-${name}`] = {
|
|
183
|
+
".charges > &": CHIEF_POINTS.has(name)
|
|
184
|
+
? {
|
|
185
|
+
left: `${left}%`,
|
|
186
|
+
top: `var(--chief-points-y, ${top}%)`,
|
|
187
|
+
width: `var(--charge-size, var(--chief-points-size, ${POINT_SIZE}%))`,
|
|
188
|
+
}
|
|
189
|
+
: place([left, top], POINT_SIZE),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
addUtilities(utilities);
|
|
194
|
+
});
|
package/divisions.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind CSS: field divisions as decoration.
|
|
3
|
+
*/
|
|
4
|
+
import plugin from "tailwindcss/plugin";
|
|
5
|
+
|
|
6
|
+
const TINCTURES = ["or", "argent", "gules", "azure", "vert", "purpure", "sable"];
|
|
7
|
+
|
|
8
|
+
// Each division maps (first, second) tincture colours to background rules.
|
|
9
|
+
const layer = (image) => ({ "background-image": image });
|
|
10
|
+
|
|
11
|
+
const DIVISIONS = {
|
|
12
|
+
// Vertical split: first on the dexter (viewer's left).
|
|
13
|
+
"per-pale": (a, b) => layer(`linear-gradient(to right, ${a} 50%, ${b} 50%)`),
|
|
14
|
+
|
|
15
|
+
// Horizontal split: first in chief.
|
|
16
|
+
"per-fess": (a, b) => layer(`linear-gradient(to bottom, ${a} 50%, ${b} 50%)`),
|
|
17
|
+
|
|
18
|
+
// Line from dexter chief to sinister base: first in the upper half.
|
|
19
|
+
// With a corner-to-corner gradient, the 50% line joins the other two corners.
|
|
20
|
+
"per-bend": (a, b) => layer(`linear-gradient(to bottom left, ${a} 50%, ${b} 50%)`),
|
|
21
|
+
|
|
22
|
+
// Line from sinister chief to dexter base: first in the upper half.
|
|
23
|
+
"per-bend-sinister": (a, b) =>
|
|
24
|
+
layer(`linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`),
|
|
25
|
+
|
|
26
|
+
// Quarters 1 and 4 (dexter chief, sinister base) are the first tincture.
|
|
27
|
+
// Linear gradients rather than conic, which render jagged edges in some browsers.
|
|
28
|
+
quarterly: (a, b) => ({
|
|
29
|
+
"background-image": `linear-gradient(to right, ${a} 50%, ${b} 50%), linear-gradient(to right, ${b} 50%, ${a} 50%)`,
|
|
30
|
+
"background-size": "100% 50%, 100% 100%",
|
|
31
|
+
"background-position": "top, top",
|
|
32
|
+
"background-repeat": "no-repeat",
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
// Gyronny of eight: each quarter split corner to corner. The first tincture
|
|
36
|
+
// is in the gyron just dexter of the top line. The background colour fills
|
|
37
|
+
// any sub-pixel seam between quarters.
|
|
38
|
+
gyronny: (a, b) => ({
|
|
39
|
+
"background-color": b,
|
|
40
|
+
"background-image": [
|
|
41
|
+
`linear-gradient(to bottom left, ${a} 50%, ${b} 50%)`,
|
|
42
|
+
`linear-gradient(to bottom right, ${b} 50%, ${a} 50%)`,
|
|
43
|
+
`linear-gradient(to bottom left, ${b} 50%, ${a} 50%)`,
|
|
44
|
+
`linear-gradient(to bottom right, ${a} 50%, ${b} 50%)`,
|
|
45
|
+
].join(", "),
|
|
46
|
+
"background-size": "50% 50%",
|
|
47
|
+
"background-position": "left top, right top, right bottom, left bottom",
|
|
48
|
+
"background-repeat": "no-repeat",
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default plugin(({ addUtilities }) => {
|
|
53
|
+
const utilities = {};
|
|
54
|
+
|
|
55
|
+
for (const [division, background] of Object.entries(DIVISIONS)) {
|
|
56
|
+
for (const first of TINCTURES) {
|
|
57
|
+
for (const second of TINCTURES) {
|
|
58
|
+
if (first === second) continue;
|
|
59
|
+
|
|
60
|
+
utilities[`.${division}-${first}-${second}`] = background(
|
|
61
|
+
`var(--color-${first})`,
|
|
62
|
+
`var(--color-${second})`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addUtilities(utilities);
|
|
69
|
+
});
|
package/grailwind.css
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind CSS: heraldic blazon for Tailwind v4.
|
|
3
|
+
*
|
|
4
|
+
* Import after Tailwind:
|
|
5
|
+
* @import "tailwindcss";
|
|
6
|
+
* @import "grailwind";
|
|
7
|
+
*
|
|
8
|
+
* Adds the tinctures alongside Tailwind's colours. For tinctures only,
|
|
9
|
+
* import "grailwind/strict" instead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@plugin "./divisions.js";
|
|
13
|
+
@plugin "./ordinaries.js";
|
|
14
|
+
@plugin "./charges.js";
|
|
15
|
+
|
|
16
|
+
@property --fimbriated {
|
|
17
|
+
syntax: "*";
|
|
18
|
+
inherits: false;
|
|
19
|
+
}
|
|
20
|
+
@property --fimbriation {
|
|
21
|
+
syntax: "*";
|
|
22
|
+
inherits: false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ---------------------------------------------------------------------------
|
|
26
|
+
* Tinctures
|
|
27
|
+
* ------------------------------------------------------------------------- */
|
|
28
|
+
@theme {
|
|
29
|
+
/* Metals */
|
|
30
|
+
--color-or: #f5c518;
|
|
31
|
+
--color-argent: #ffffff;
|
|
32
|
+
|
|
33
|
+
/* Colours */
|
|
34
|
+
--color-gules: #d21f1f;
|
|
35
|
+
--color-azure: #1f4fb0;
|
|
36
|
+
--color-vert: #1d8a3c;
|
|
37
|
+
--color-purpure: #7e3a8e;
|
|
38
|
+
--color-sable: #141414;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ---------------------------------------------------------------------------
|
|
42
|
+
* Bare tinctures
|
|
43
|
+
* ------------------------------------------------------------------------- */
|
|
44
|
+
@utility or { background-color: var(--color-or); }
|
|
45
|
+
@utility argent { background-color: var(--color-argent); }
|
|
46
|
+
@utility gules { background-color: var(--color-gules); }
|
|
47
|
+
@utility azure { background-color: var(--color-azure); }
|
|
48
|
+
@utility vert { background-color: var(--color-vert); }
|
|
49
|
+
@utility purpure { background-color: var(--color-purpure); }
|
|
50
|
+
@utility sable { background-color: var(--color-sable); }
|
|
51
|
+
|
|
52
|
+
/* ---------------------------------------------------------------------------
|
|
53
|
+
* Field divisions
|
|
54
|
+
* ------------------------------------------------------------------------- */
|
|
55
|
+
|
|
56
|
+
/* Per pale: split vertically into dexter | sinister. */
|
|
57
|
+
@utility per-pale {
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-auto-flow: column;
|
|
60
|
+
grid-auto-columns: 1fr;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Per fess: split horizontally into chief / base. */
|
|
64
|
+
@utility per-fess {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-auto-flow: row;
|
|
67
|
+
grid-auto-rows: 1fr;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Quarterly: four quarters, numbered 1–4 across then down.
|
|
71
|
+
* Quarter 1 is dexter chief, i.e. the viewer's top left. */
|
|
72
|
+
@utility quarterly {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: 1fr 1fr;
|
|
75
|
+
grid-template-rows: 1fr 1fr;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ---------------------------------------------------------------------------
|
|
79
|
+
* Subordinaries
|
|
80
|
+
* ------------------------------------------------------------------------- */
|
|
81
|
+
|
|
82
|
+
/* Bordure: a border in a given tincture, e.g. `bordure-or`. */
|
|
83
|
+
@utility bordure-* {
|
|
84
|
+
border-style: solid;
|
|
85
|
+
border-width: 0.75rem;
|
|
86
|
+
border-color: --value(--color-*);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ---------------------------------------------------------------------------
|
|
90
|
+
* The shield itself
|
|
91
|
+
* ------------------------------------------------------------------------- */
|
|
92
|
+
|
|
93
|
+
/* Escutcheon: a shield shape to draw arms in. */
|
|
94
|
+
@utility escutcheon {
|
|
95
|
+
position: relative;
|
|
96
|
+
aspect-ratio: 5 / 6;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
border-radius: 0 0 50% 50% / 0 0 45% 45%;
|
|
99
|
+
box-shadow: 0 0 0 2px var(--color-sable);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---------------------------------------------------------------------------
|
|
103
|
+
* Charge size
|
|
104
|
+
* ------------------------------------------------------------------------- */
|
|
105
|
+
|
|
106
|
+
/* Charge width as a % of the shield, e.g. `charge-size-20` or `charge-size-[3rem]`.
|
|
107
|
+
* On a `charges` container it sizes every charge; on a charge, just that one. */
|
|
108
|
+
@utility charge-size-* {
|
|
109
|
+
--charge-size: calc(--value(integer) * 1%);
|
|
110
|
+
--charge-size: --value([percentage], [length]);
|
|
111
|
+
}
|
package/ordinaries.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Grailwind CSS: ordinaries, e.g. `fess-or`.
|
|
3
|
+
*/
|
|
4
|
+
import plugin from "tailwindcss/plugin";
|
|
5
|
+
|
|
6
|
+
const TINCTURES = ["or", "argent", "gules", "azure", "vert", "purpure", "sable"];
|
|
7
|
+
|
|
8
|
+
const ORDINARIES = {
|
|
9
|
+
pale: [[35, 0], [65, 0], [65, 100], [35, 100]],
|
|
10
|
+
fess: [[0, 30], [100, 30], [100, 60], [0, 60]],
|
|
11
|
+
bend: [[0, 0], [20, 0], [100, 80], [100, 100], [80, 100], [0, 20]],
|
|
12
|
+
"bend-sinister": [[100, 0], [80, 0], [0, 80], [0, 100], [20, 100], [100, 20]],
|
|
13
|
+
chevron: [[50, 25], [100, 75], [100, 95], [50, 45], [0, 95], [0, 75]],
|
|
14
|
+
cross: [
|
|
15
|
+
[40, 0], [60, 0], [60, 35], [100, 35], [100, 55], [60, 55],
|
|
16
|
+
[60, 100], [40, 100], [40, 55], [0, 55], [0, 35], [40, 35],
|
|
17
|
+
],
|
|
18
|
+
saltire: [
|
|
19
|
+
[0, 0], [15, 0], [50, 35], [85, 0], [100, 0], [100, 15], [65, 50], [100, 85],
|
|
20
|
+
[100, 100], [85, 100], [50, 65], [15, 100], [0, 100], [0, 85], [35, 50], [0, 15],
|
|
21
|
+
],
|
|
22
|
+
pall: [
|
|
23
|
+
[0, 0], [15, 0], [50, 35], [85, 0], [100, 0], [100, 15],
|
|
24
|
+
[60, 55], [60, 100], [40, 100], [40, 55], [0, 15],
|
|
25
|
+
],
|
|
26
|
+
chief: [[0, 0], [100, 0], [100, 30], [0, 30]],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const num = (v) => +v.toFixed(3);
|
|
30
|
+
const pct = (v) => (num(v) === 0 ? "0" : `${num(v)}%`);
|
|
31
|
+
const point = ([x, y]) => `${pct(x)} ${pct(y)}`;
|
|
32
|
+
const polygon = (vertices) => `polygon(${vertices.map(point).join(", ")})`;
|
|
33
|
+
|
|
34
|
+
const PSEUDO = { chief: "&::after" };
|
|
35
|
+
|
|
36
|
+
// Fimbriation: the ordinary's clip is widened and filled with the fimbriation
|
|
37
|
+
// tincture, and the ordinary's own tincture is painted over it as gradient
|
|
38
|
+
// bands.
|
|
39
|
+
const W = 100;
|
|
40
|
+
const H = 120;
|
|
41
|
+
const FIMBRIATION = 3;
|
|
42
|
+
|
|
43
|
+
const real = ([x, y]) => [(x * W) / 100, (y * H) / 100];
|
|
44
|
+
const onShieldEdge = (a, b) =>
|
|
45
|
+
(a[0] === b[0] && (a[0] === 0 || a[0] === 100)) ||
|
|
46
|
+
(a[1] === b[1] && (a[1] === 0 || a[1] === 100));
|
|
47
|
+
const signedArea = (vertices) =>
|
|
48
|
+
vertices.reduce((sum, [x1, y1], i) => {
|
|
49
|
+
const [x2, y2] = vertices[(i + 1) % vertices.length];
|
|
50
|
+
return sum + x1 * y2 - x2 * y1;
|
|
51
|
+
}, 0) / 2;
|
|
52
|
+
|
|
53
|
+
// The edge a→b as a line n·p = c, moved outward by `distance` real units.
|
|
54
|
+
const offsetLine = (a, b, outward, distance) => {
|
|
55
|
+
const [ax, ay] = real(a);
|
|
56
|
+
const [bx, by] = real(b);
|
|
57
|
+
const length = Math.hypot(bx - ax, by - ay);
|
|
58
|
+
const n = [(outward * (by - ay)) / length, (outward * (ax - bx)) / length];
|
|
59
|
+
return { n, c: n[0] * ax + n[1] * ay + distance };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// How far each vertex moves, in %, when fimbriated. Sides on the shield's edge
|
|
63
|
+
// don't move, so their vertices slide along it.
|
|
64
|
+
const widen = (vertices) => {
|
|
65
|
+
const outward = Math.sign(signedArea(vertices));
|
|
66
|
+
const count = vertices.length;
|
|
67
|
+
const line = (i) => {
|
|
68
|
+
const a = vertices[i];
|
|
69
|
+
const b = vertices[(i + 1) % count];
|
|
70
|
+
return offsetLine(a, b, outward, onShieldEdge(a, b) ? 0 : FIMBRIATION);
|
|
71
|
+
};
|
|
72
|
+
return vertices.map((v, i) => {
|
|
73
|
+
const p = line((i - 1 + count) % count);
|
|
74
|
+
const q = line(i);
|
|
75
|
+
const det = p.n[0] * q.n[1] - p.n[1] * q.n[0];
|
|
76
|
+
const [x, y] = [(p.c * q.n[1] - q.c * p.n[1]) / det, (p.n[0] * q.c - q.n[0] * p.c) / det];
|
|
77
|
+
const [rx, ry] = real(v);
|
|
78
|
+
return [((x - rx) * 100) / W, ((y - ry) * 100) / H];
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const shift = (v, d) =>
|
|
83
|
+
num(d) === 0
|
|
84
|
+
? pct(v)
|
|
85
|
+
: `calc(${num(v)}% ${d < 0 ? "-" : "+"} ${num(Math.abs(d))}% * var(--fimbriated, 0))`;
|
|
86
|
+
const widenedPolygon = (vertices) => {
|
|
87
|
+
const offsets = widen(vertices);
|
|
88
|
+
const points = vertices.map(
|
|
89
|
+
([x, y], i) => `${shift(x, offsets[i][0])} ${shift(y, offsets[i][1])}`,
|
|
90
|
+
);
|
|
91
|
+
return `polygon(${points.join(", ")})`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// The ordinary's tincture, top layer first. `x`/`y` bands run between two x or y
|
|
95
|
+
// values in %; `u-v`/`u+v` are diagonal, between values of x − y or x + y.
|
|
96
|
+
// A `tile` [x0, y0, x1, y1] confines a layer to part of the box (square, if
|
|
97
|
+
// diagonal). Stripes repaint the fimbriation where one arm's band runs on into
|
|
98
|
+
// another's.
|
|
99
|
+
const S = 12;
|
|
100
|
+
const band = (kind, from, to, tile) => ({ kind, from, to, tile });
|
|
101
|
+
const stripe = (kind, from, to, tile) => ({ kind, from, to, tile, stripe: true });
|
|
102
|
+
const BANDS = {
|
|
103
|
+
pale: [band("x", 35, 65)],
|
|
104
|
+
fess: [band("y", 30, 60)],
|
|
105
|
+
bend: [band("u-v", -20, 20)],
|
|
106
|
+
"bend-sinister": [band("u+v", 80, 120)],
|
|
107
|
+
chevron: [
|
|
108
|
+
stripe("u+v", 75 - S, 75),
|
|
109
|
+
stripe("u-v", 25, 25 + S),
|
|
110
|
+
band("u+v", 75, 95),
|
|
111
|
+
band("u-v", 5, 25),
|
|
112
|
+
],
|
|
113
|
+
cross: [band("x", 40, 60), band("y", 35, 55)],
|
|
114
|
+
saltire: [band("u-v", -15, 15), band("u+v", 85, 115)],
|
|
115
|
+
pall: [
|
|
116
|
+
stripe("x", 60, 60 + S, [0, 55, 100, 100]),
|
|
117
|
+
stripe("x", 40 - S, 40, [0, 55, 100, 100]),
|
|
118
|
+
stripe("u+v", 115, 115 + S, [60, 30, 100, 70]),
|
|
119
|
+
stripe("u-v", -15 - S, -15, [0, 30, 40, 70]),
|
|
120
|
+
band("u-v", -15, 15),
|
|
121
|
+
band("u+v", 85, 115),
|
|
122
|
+
band("x", 40, 60, [0, 55, 100, 100]),
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const DIRECTION = { x: "to right", y: "to bottom", "u-v": "to top right", "u+v": "to bottom right" };
|
|
127
|
+
|
|
128
|
+
// A band's edges as % along its gradient, within its tile.
|
|
129
|
+
const stop = ({ kind, tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }, value) => {
|
|
130
|
+
const w = x1 - x0;
|
|
131
|
+
const h = y1 - y0;
|
|
132
|
+
if (kind.length > 1 && w !== h) throw new Error("diagonal bands need a square tile");
|
|
133
|
+
switch (kind) {
|
|
134
|
+
case "x": return ((value - x0) * 100) / w;
|
|
135
|
+
case "y": return ((value - y0) * 100) / h;
|
|
136
|
+
case "u-v": return ((value - (x0 - y0)) * 100) / w / 2 + 50;
|
|
137
|
+
case "u+v": return ((value - x0 - y0) * 100) / w / 2;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Soften each edge by a pixel, as hard gradient stops aren't antialiased.
|
|
142
|
+
const gradient = (layer, colour) => {
|
|
143
|
+
const a = num(stop(layer, layer.from));
|
|
144
|
+
const b = num(stop(layer, layer.to));
|
|
145
|
+
return (
|
|
146
|
+
`linear-gradient(${DIRECTION[layer.kind]}, transparent calc(${a}% - 0.5px), ` +
|
|
147
|
+
`${colour} calc(${a}% + 0.5px) calc(${b}% - 0.5px), transparent calc(${b}% + 0.5px))`
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const tilePosition = (start, size) => (size === 100 ? "0" : pct((start * 100) / (100 - size)));
|
|
152
|
+
|
|
153
|
+
const bands = (layers, tincture) => ({
|
|
154
|
+
"background-image": layers
|
|
155
|
+
.map((l) => gradient(l, l.stripe ? "var(--fimbriation, transparent)" : `var(--color-${tincture})`))
|
|
156
|
+
.join(", "),
|
|
157
|
+
"background-size": layers
|
|
158
|
+
.map(({ tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }) => `${pct(x1 - x0)} ${pct(y1 - y0)}`)
|
|
159
|
+
.join(", "),
|
|
160
|
+
"background-position": layers
|
|
161
|
+
.map(({ tile: [x0, y0, x1, y1] = [0, 0, 100, 100] }) =>
|
|
162
|
+
`${tilePosition(x0, x1 - x0)} ${tilePosition(y0, y1 - y0)}`)
|
|
163
|
+
.join(", "),
|
|
164
|
+
"background-repeat": "no-repeat",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Where charges go for "between", as [left%, top%], plus their size as % of width.
|
|
168
|
+
const BETWEEN = {
|
|
169
|
+
pale: { size: 18, points: [[18, 42], [82, 42]] },
|
|
170
|
+
fess: { size: 20, points: [[30, 15], [70, 15], [50, 78]] },
|
|
171
|
+
bend: { size: 24, points: [[70, 26], [30, 68]] },
|
|
172
|
+
"bend-sinister": { size: 24, points: [[30, 26], [70, 68]] },
|
|
173
|
+
chevron: { size: 22, points: [[24, 22], [76, 22], [50, 70]] },
|
|
174
|
+
cross: { size: 18, points: [[20, 17], [80, 17], [24, 72], [76, 72]] },
|
|
175
|
+
saltire: { size: 16, points: [[50, 13], [14, 50], [86, 50], [50, 84]] },
|
|
176
|
+
pall: { size: 18, points: [[50, 11], [18, 56], [82, 56]] },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Where charges go for "on", along the ordinary's centre line, by number of charges.
|
|
180
|
+
const ON = {
|
|
181
|
+
pale: { size: 20, counts: { 1: [[50, 45]], 2: [[50, 28], [50, 62]], 3: [[50, 18], [50, 45], [50, 72]] } },
|
|
182
|
+
fess: { size: 22, counts: { 1: [[50, 45]], 2: [[30, 45], [70, 45]], 3: [[20, 45], [50, 45], [80, 45]] } },
|
|
183
|
+
bend: { size: 18, counts: { 1: [[50, 50]], 2: [[33, 33], [67, 67]], 3: [[24, 24], [50, 50], [76, 76]] } },
|
|
184
|
+
"bend-sinister": { size: 18, counts: { 1: [[50, 50]], 2: [[67, 33], [33, 67]], 3: [[76, 24], [50, 50], [24, 76]] } },
|
|
185
|
+
chevron: { size: 12, counts: { 1: [[50, 36]], 3: [[20, 65], [50, 36], [80, 65]] } },
|
|
186
|
+
cross: { size: 15, counts: { 1: [[50, 45]], 5: [[50, 15], [18, 45], [50, 45], [82, 45], [50, 80]] } },
|
|
187
|
+
saltire: { size: 14, counts: { 1: [[50, 50]], 5: [[22, 22], [78, 22], [50, 50], [22, 78], [78, 78]] } },
|
|
188
|
+
pall: { size: 14, counts: { 1: [[50, 50]], 3: [[22, 22], [78, 22], [50, 75]] } },
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const betweenVars = ({ size, points }) => {
|
|
192
|
+
const vars = { "--between-size": `${size}%` };
|
|
193
|
+
points.forEach(([left, top], i) => {
|
|
194
|
+
vars[`--between-${i + 1}-x`] = `${left}%`;
|
|
195
|
+
vars[`--between-${i + 1}-y`] = `${top}%`;
|
|
196
|
+
});
|
|
197
|
+
return vars;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const onVars = ({ size, counts }) => {
|
|
201
|
+
const vars = { "--on-size": `${size}%` };
|
|
202
|
+
for (const [count, points] of Object.entries(counts)) {
|
|
203
|
+
points.forEach(([left, top], i) => {
|
|
204
|
+
vars[`--on-${count}-${i + 1}-x`] = `${left}%`;
|
|
205
|
+
vars[`--on-${count}-${i + 1}-y`] = `${top}%`;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return vars;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default plugin(({ addUtilities }) => {
|
|
212
|
+
const utilities = {};
|
|
213
|
+
|
|
214
|
+
for (const tincture of TINCTURES) {
|
|
215
|
+
utilities[`.fimbriated-${tincture}`] = {
|
|
216
|
+
"--fimbriated": "1",
|
|
217
|
+
"--fimbriation": `var(--color-${tincture})`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const [ordinary, vertices] of Object.entries(ORDINARIES)) {
|
|
222
|
+
for (const tincture of TINCTURES) {
|
|
223
|
+
utilities[`.${ordinary}-${tincture}`] = {
|
|
224
|
+
position: "relative",
|
|
225
|
+
...(BETWEEN[ordinary] && betweenVars(BETWEEN[ordinary])),
|
|
226
|
+
...(ON[ordinary] && onVars(ON[ordinary])),
|
|
227
|
+
// With a chief, everything else is drawn in the field below it.
|
|
228
|
+
...(ordinary === "chief" && {
|
|
229
|
+
"--field-top": "30%",
|
|
230
|
+
// The chief's centre line, relative to the charges container below it.
|
|
231
|
+
"--chief-points-y": "calc(-15% / 0.7)",
|
|
232
|
+
"--chief-points-size": "18%",
|
|
233
|
+
}),
|
|
234
|
+
[PSEUDO[ordinary] ?? "&::before"]: {
|
|
235
|
+
content: '""',
|
|
236
|
+
position: "absolute",
|
|
237
|
+
inset: ordinary === "chief" ? "0" : "var(--field-top, 0%) 0 0",
|
|
238
|
+
...(BANDS[ordinary]
|
|
239
|
+
? {
|
|
240
|
+
"--fimbriated": "inherit",
|
|
241
|
+
"--fimbriation": "inherit",
|
|
242
|
+
"background-color": `var(--fimbriation, var(--color-${tincture}))`,
|
|
243
|
+
...bands(BANDS[ordinary], tincture),
|
|
244
|
+
"clip-path": widenedPolygon(vertices),
|
|
245
|
+
}
|
|
246
|
+
: {
|
|
247
|
+
"background-color": `var(--color-${tincture})`,
|
|
248
|
+
"clip-path": polygon(vertices),
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
addUtilities(utilities);
|
|
256
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grailwind",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build": "tailwindcss -i docs/app.css -o docs/site.css",
|
|
6
|
+
"dev": "tailwindcss -i docs/app.css -o docs/site.css --watch",
|
|
7
|
+
"test": "node test/modes.mjs",
|
|
8
|
+
"dev:watch": "tailwindcss -i docs/app.css -o docs/site.css --watch=always & python3 -m http.server ${PORT:-8000} -d docs"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"tailwindcss",
|
|
12
|
+
"tailwind",
|
|
13
|
+
"heraldry",
|
|
14
|
+
"blazon",
|
|
15
|
+
"css"
|
|
16
|
+
],
|
|
17
|
+
"author": "Paul Firmin",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"description": "Heraldic blazon for Tailwind CSS: tinctures, divisions, ordinaries and charges",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
22
|
+
"tailwindcss": "^4.3.3"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"style": "grailwind.css",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"style": "./grailwind.css",
|
|
29
|
+
"default": "./grailwind.css"
|
|
30
|
+
},
|
|
31
|
+
"./strict": {
|
|
32
|
+
"style": "./strict.css",
|
|
33
|
+
"default": "./strict.css"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"grailwind.css",
|
|
38
|
+
"divisions.js",
|
|
39
|
+
"ordinaries.js",
|
|
40
|
+
"charges.js",
|
|
41
|
+
"strict.css",
|
|
42
|
+
"palette-reset.css",
|
|
43
|
+
"CHANGELOG.md"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"tailwindcss": "^4.0.0"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://pafirmin.github.io/grailwind/",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/pafirmin/grailwind.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/pafirmin/grailwind/issues"
|
|
55
|
+
}
|
|
56
|
+
}
|