fontico 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +214 -0
- data/docs/expand-check.png +0 -0
- data/docs/icon-authoring.html +371 -0
- data/docs/lucide-glyphs-extracted.png +0 -0
- data/docs/lucide-ttf-glyph.png +0 -0
- data/docs/rails-preview.png +0 -0
- data/docs/sprite-sheet.png +0 -0
- data/docs/stroke-vs-fill.png +0 -0
- data/docs/ttf-glyphs.png +0 -0
- data/lib/fontico/builder.rb +115 -0
- data/lib/fontico/emitters/font.rb +62 -0
- data/lib/fontico/emitters/sprite.rb +30 -0
- data/lib/fontico/emitters/stylesheet.rb +45 -0
- data/lib/fontico/helper.rb +105 -0
- data/lib/fontico/icon.rb +15 -0
- data/lib/fontico/lockfile.rb +87 -0
- data/lib/fontico/manifest.rb +75 -0
- data/lib/fontico/node/build_font.mjs +52 -0
- data/lib/fontico/node/extract_glyphs.mjs +35 -0
- data/lib/fontico/node/package.json +11 -0
- data/lib/fontico/node_runner.rb +66 -0
- data/lib/fontico/outliner.rb +80 -0
- data/lib/fontico/prawn.rb +62 -0
- data/lib/fontico/preprocessor.rb +238 -0
- data/lib/fontico/provider_fonts.rb +66 -0
- data/lib/fontico/railtie.rb +54 -0
- data/lib/fontico/resolver.rb +148 -0
- data/lib/fontico/version.rb +5 -0
- data/lib/fontico.rb +100 -0
- data/lib/tasks/fontico.rake +69 -0
- metadata +132 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1fa6282e972c966366c603283a95c34f09e021d6ced83b5af0e5bef66b768e03
|
|
4
|
+
data.tar.gz: e0a10573369be6b033cb1adf4670a562cc302a9be28f1a7a7a6efba8981045d0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 945f68cafbdc9191a2b0bf2cfc186a5def497fcbe2bcfaf8ef367aaca7a14a20d77cf321d57416219253c2b5696c73797b744ef8f6019d9557931dd55955ea3e
|
|
7
|
+
data.tar.gz: ac43b2da7d0db127e2a9cca54c76f82d1e62473f478051febd42b2749816867bee2405c8848ecc3beedd8f5408c7083f61bf3806565023f437702051462a6329
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Duto
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# fontico
|
|
2
|
+
|
|
3
|
+
Name icons by intent. Source them from anywhere. Ship one artifact.
|
|
4
|
+
|
|
5
|
+
```erb
|
|
6
|
+
<%= icon "save" %> <%# Lucide %>
|
|
7
|
+
<%= icon "delete" %> <%# Material Symbols %>
|
|
8
|
+
<%= icon "logo" %> <%# your own SVG file %>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Three providers, one call. Templates never name a vendor, so re-skinning the
|
|
12
|
+
app — or surviving an upstream rename — is a diff in one file.
|
|
13
|
+
|
|
14
|
+
## The manifest
|
|
15
|
+
|
|
16
|
+
`icons.yml`, one line per icon:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
defaults:
|
|
20
|
+
provider: lucide # bare names resolve here
|
|
21
|
+
|
|
22
|
+
targets: [sprite]
|
|
23
|
+
|
|
24
|
+
providers:
|
|
25
|
+
lucide: { license: ISC }
|
|
26
|
+
material-symbols: { license: Apache-2.0 }
|
|
27
|
+
local: { path: app/assets/icons }
|
|
28
|
+
|
|
29
|
+
icons:
|
|
30
|
+
save: lucide/save
|
|
31
|
+
delete: material-symbols/delete
|
|
32
|
+
logo: local/logo
|
|
33
|
+
nav:
|
|
34
|
+
menu: lucide/menu # -> icon("nav.menu")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Any of [Iconify's 200k+ icons](https://icon-sets.iconify.design/) work as a
|
|
38
|
+
provider prefix. Your own SVGs go in `app/assets/icons/`, filename as slug.
|
|
39
|
+
|
|
40
|
+
## Build
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rake fontico:build # resolve, normalise, emit
|
|
44
|
+
rake fontico:update # re-fetch, ignoring icons.lock
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Artifacts land in `app/assets/builds/`, which Propshaft serves automatically —
|
|
48
|
+
no manifest, no precompile list. `rake assets:precompile` is hooked, so deploys
|
|
49
|
+
need no extra step.
|
|
50
|
+
|
|
51
|
+
That directory is gitignored in a stock Rails app, so nothing is committed from
|
|
52
|
+
it. **`icons.lock` is the thing you commit**: it holds every normalised body,
|
|
53
|
+
so a deploy rebuilds the sprite from it in milliseconds with no network access
|
|
54
|
+
and no Node.
|
|
55
|
+
|
|
56
|
+
In development you rarely type either one. Saving `icons.yml` — or a local
|
|
57
|
+
SVG — rebuilds the artifacts and drops the cached manifest, so the icon is live
|
|
58
|
+
on the next request: no rake, no restart. A save that only reshuffles known
|
|
59
|
+
icons costs a couple of milliseconds; a brand-new one pays its provider fetch
|
|
60
|
+
once, then it is in the lock.
|
|
61
|
+
|
|
62
|
+
A rebuild never raises — building stays as forgiving as the rake task, so a
|
|
63
|
+
half-typed manifest does not take down a page that draws none of the broken
|
|
64
|
+
icons. The pages that *do* draw them raise instead, at the call site, and keep
|
|
65
|
+
raising until a save fixes it. That is deliberate: an icon left out of the
|
|
66
|
+
sprite still resolves through the manifest and renders a valid `<use>` at a
|
|
67
|
+
symbol that isn't there, which in a browser is an invisible empty box on a page
|
|
68
|
+
that returns 200. Silence is the one outcome worse than a stack trace.
|
|
69
|
+
|
|
70
|
+
An icon that cannot be resolved — a typo'd slug, a local file that isn't
|
|
71
|
+
there — is named in red and left out; the rest of the manifest still builds.
|
|
72
|
+
It keeps its codepoint reserved, so fixing the entry and rebuilding brings it
|
|
73
|
+
back with the same glyph. A provider that is unreachable is still fatal.
|
|
74
|
+
|
|
75
|
+
Measured on 35 icons across two remote providers and five local files:
|
|
76
|
+
**361ms cold, 2ms warm.** Vendor icons are fetched in one batched request per
|
|
77
|
+
provider — not one per icon.
|
|
78
|
+
|
|
79
|
+
## Why there is a lockfile
|
|
80
|
+
|
|
81
|
+
`icons.lock` pins two things that must not drift:
|
|
82
|
+
|
|
83
|
+
- **Codepoints**, append-only. Adding an icon must not renumber the others, or
|
|
84
|
+
every glyph in a built font moves and the committed artifact churns
|
|
85
|
+
whole-file on each addition. Retired codepoints are never reissued.
|
|
86
|
+
- **Normalised bodies**, so builds are reproducible and run offline. The
|
|
87
|
+
Iconify API serves *latest*; without this an icon can silently change shape
|
|
88
|
+
between two builds of the same manifest.
|
|
89
|
+
|
|
90
|
+
Commit it.
|
|
91
|
+
|
|
92
|
+
## What happens to your SVGs
|
|
93
|
+
|
|
94
|
+
First-party exports are not uniform the way vendor icons are, so everything
|
|
95
|
+
entering `app/assets/icons/` is normalised first:
|
|
96
|
+
|
|
97
|
+
| | |
|
|
98
|
+
|---|---|
|
|
99
|
+
| Editor chrome | `sodipodi:`, `inkscape:`, `<metadata>`, RDF, empty `<defs>` stripped |
|
|
100
|
+
| Ids | rewritten to `slug__id`, with `url(#…)`, `href`, `clip-path`, `mask` following |
|
|
101
|
+
| viewBox | any source box refitted into the target, centred, aspect preserved |
|
|
102
|
+
| Colour | folded to `currentColor`, unless the icon is detected as multicolour |
|
|
103
|
+
| Safety | `<script>`, `on*` handlers, `<foreignObject>`, external refs removed |
|
|
104
|
+
|
|
105
|
+
The id rewriting is not optional hygiene. Across 40 SVGs sampled from a real
|
|
106
|
+
machine, **34 shared `id="layer1"`** and 11 shared `id="path1"` — merging any
|
|
107
|
+
two of them into one document silently drops a definition.
|
|
108
|
+
|
|
109
|
+
Four things cannot be fixed downstream and are reported against the source
|
|
110
|
+
file: live `<text>`, embedded raster `<image>`, multicolour in a font target,
|
|
111
|
+
and `<filter>` effects. See **[docs/icon-authoring.html](docs/icon-authoring.html)**
|
|
112
|
+
for the full authoring spec.
|
|
113
|
+
|
|
114
|
+
## Making an `<svg>` behave like a glyph
|
|
115
|
+
|
|
116
|
+
A font glyph inherits `font-size` and `color` from the text around it. An
|
|
117
|
+
`<svg>` has no intrinsic size and sits on the baseline's bottom edge, so the
|
|
118
|
+
sprite needs a little help to match. `rake fontico:build` emits `icons.css`
|
|
119
|
+
alongside the sprite whenever `sprite` is a target:
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
.ico { display:inline-block; width:1em; height:1em; vertical-align:-0.125em; flex:none; }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The helper defaults to `width="1em" height="1em"`, so the same markup scales
|
|
126
|
+
with whatever type it sits in, and a CSS class still wins over the attributes:
|
|
127
|
+
|
|
128
|
+
```erb
|
|
129
|
+
<%= icon "save" %> <%# 1em, follows font-size %>
|
|
130
|
+
<%= icon "save", size: 18 %> <%# a bare number means px %>
|
|
131
|
+
<%= icon "save", size: "2em" %>
|
|
132
|
+
<%= icon "save", class: "size-6" %> <%# Tailwind overrides the attrs %>
|
|
133
|
+
<%= icon "save", title: "Save file" %> <%# role="img" + <title>, not hidden %>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The two sizing routes do not fight. `width`/`height` on an `<svg>` are
|
|
137
|
+
presentation attributes, and *any* CSS declaration outranks them — including
|
|
138
|
+
`icons.css`'s own `.ico { width: 1em }`. So an explicit `size:` is written
|
|
139
|
+
inline, where it beats the stylesheet, while an icon with no `size:` carries
|
|
140
|
+
only the attributes and stays free for `class: "size-6"` to size instead. Ask
|
|
141
|
+
for one or the other, not both.
|
|
142
|
+
|
|
143
|
+
Colour needs no help: bodies are folded to `currentColor`. That is not a
|
|
144
|
+
convenience — host CSS does **not** cascade into a cross-document `<use>`, but
|
|
145
|
+
inherited properties like `color` do reach it, so `currentColor` is the only
|
|
146
|
+
thing that makes an external sprite themeable.
|
|
147
|
+
|
|
148
|
+
`flex:none` matters more than it looks: an icon in a flex row gets squashed to
|
|
149
|
+
zero width without it.
|
|
150
|
+
|
|
151
|
+
### Same-origin
|
|
152
|
+
|
|
153
|
+
Cross-document `<use>` is subject to same-origin. If `asset_host` points at a
|
|
154
|
+
CDN the sprite silently renders nothing — reserved space, no icon. Serve the
|
|
155
|
+
sprite same-origin, or switch to inline mode:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Fontico.inline_sprite = true # then <%= icons_sprite %> in your layout
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Fonts, and Prawn
|
|
162
|
+
|
|
163
|
+
The `ttf` target exists for **PDF generation**, not the web — Prawn reads
|
|
164
|
+
TTF/OTF through ttfunk and cannot read woff2, and on the web the sprite is both
|
|
165
|
+
smaller over the wire (2.3KB brotli vs ~6KB woff2 here) and not render-blocking.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
require "fontico/prawn"
|
|
169
|
+
|
|
170
|
+
Prawn::Document.generate("invoice.pdf") do |pdf|
|
|
171
|
+
pdf.fontico! # register the family once
|
|
172
|
+
pdf.icon "save", size: 18
|
|
173
|
+
pdf.icon_at "mail", at: [40, 700], size: 24
|
|
174
|
+
pdf.text "#{Fontico.glyph("confirm")} done"
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`Fontico.codepoint("save")` and `Fontico.glyph("save")` expose the pinned
|
|
179
|
+
codepoint, so nothing gets hardcoded.
|
|
180
|
+
|
|
181
|
+
### Where outlines come from
|
|
182
|
+
|
|
183
|
+
A font glyph is filled contours; it has no strokes and no colour. Each icon
|
|
184
|
+
takes one of three routes:
|
|
185
|
+
|
|
186
|
+
| | |
|
|
187
|
+
|---|---|
|
|
188
|
+
| **filled** | used as-is — Material Symbols, most Iconify sets, flat first-party exports |
|
|
189
|
+
| **extracted** | stroke-based, but the provider ships a font whose glyphs are already expanded — lifted from there, losslessly (Lucide) |
|
|
190
|
+
| **refused** | stroke-based with no provider font — the build stops and names the icon |
|
|
191
|
+
|
|
192
|
+
There is deliberately no raster-trace fallback. Both published JS expanders
|
|
193
|
+
(`svg-outline-stroke`, `oslllo-svg-fixer`) run artwork through potrace and hand
|
|
194
|
+
back rounded corners and wobbling stems. Refusing beats shipping geometry that
|
|
195
|
+
quietly stopped matching the sprite. Outline strokes at source instead — the
|
|
196
|
+
build message says so, by icon name.
|
|
197
|
+
|
|
198
|
+
Multicolour icons cannot be glyphs at all, so they stay in the sprite and the
|
|
199
|
+
build names each one it dropped.
|
|
200
|
+
|
|
201
|
+
### Toolchain
|
|
202
|
+
|
|
203
|
+
Font targets need Node, installed on demand into `~/.cache/fontico`. A manifest
|
|
204
|
+
with `targets: [sprite]` never touches it and stays pure Ruby.
|
|
205
|
+
|
|
206
|
+
## Status
|
|
207
|
+
|
|
208
|
+
- ✅ Manifest, resolver, preprocessor, lockfile, sprite emitter, Rails helper
|
|
209
|
+
- ✅ TTF emitter with glyph extraction, codepoint API, Prawn helpers
|
|
210
|
+
- ⏳ `woff2` — declared targets are skipped with a notice
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
<title>The 24×24 Contract</title>
|
|
2
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
4
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Public+Sans:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
|
5
|
+
|
|
6
|
+
<style>
|
|
7
|
+
:root{
|
|
8
|
+
--paper:#FAFAFB; --surface:#FFFFFF; --surface-2:#F3F5F8;
|
|
9
|
+
--ink:#15181E; --ink-soft:#565F6D; --ink-faint:#8A93A1;
|
|
10
|
+
--rule:#E2E5EA; --rule-strong:#CBD2DC;
|
|
11
|
+
--accent:#C8106A; --accent-ink:#C8106A; --accent-soft:#FCE7F1;
|
|
12
|
+
--ok:#1F7A4D; --ok-soft:#E6F4EC;
|
|
13
|
+
--warn:#8F5400; --warn-soft:#FBEEDA;
|
|
14
|
+
--stop:#A8221B; --stop-soft:#FBE8E6;
|
|
15
|
+
--shadow:0 1px 2px rgba(21,24,30,.05), 0 8px 24px -12px rgba(21,24,30,.14);
|
|
16
|
+
--grid:rgba(86,95,109,.10);
|
|
17
|
+
}
|
|
18
|
+
@media (prefers-color-scheme: dark){
|
|
19
|
+
:root:not([data-theme="light"]){
|
|
20
|
+
--paper:#0E1116; --surface:#161A21; --surface-2:#1C222B;
|
|
21
|
+
--ink:#E8EBEF; --ink-soft:#98A2B0; --ink-faint:#6B7583;
|
|
22
|
+
--rule:#262C36; --rule-strong:#39424F;
|
|
23
|
+
--accent:#FF5CA8; --accent-ink:#FF7DB9; --accent-soft:#3B0F26;
|
|
24
|
+
--ok:#5BC48E; --ok-soft:#10281D;
|
|
25
|
+
--warn:#E2A03F; --warn-soft:#2C2110;
|
|
26
|
+
--stop:#F0796F; --stop-soft:#2E1412;
|
|
27
|
+
--shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
|
|
28
|
+
--grid:rgba(152,162,176,.11);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
:root[data-theme="dark"]{
|
|
32
|
+
--paper:#0E1116; --surface:#161A21; --surface-2:#1C222B;
|
|
33
|
+
--ink:#E8EBEF; --ink-soft:#98A2B0; --ink-faint:#6B7583;
|
|
34
|
+
--rule:#262C36; --rule-strong:#39424F;
|
|
35
|
+
--accent:#FF5CA8; --accent-ink:#FF7DB9; --accent-soft:#3B0F26;
|
|
36
|
+
--ok:#5BC48E; --ok-soft:#10281D;
|
|
37
|
+
--warn:#E2A03F; --warn-soft:#2C2110;
|
|
38
|
+
--stop:#F0796F; --stop-soft:#2E1412;
|
|
39
|
+
--shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.7);
|
|
40
|
+
--grid:rgba(152,162,176,.11);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
*{box-sizing:border-box}
|
|
44
|
+
body{
|
|
45
|
+
margin:0; background:var(--paper); color:var(--ink);
|
|
46
|
+
font-family:"Public Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
47
|
+
font-size:16.5px; line-height:1.65; -webkit-font-smoothing:antialiased;
|
|
48
|
+
}
|
|
49
|
+
h1,h2,h3{font-family:"Bricolage Grotesque","Public Sans",ui-sans-serif,system-ui,sans-serif;text-wrap:balance;margin:0}
|
|
50
|
+
code,kbd,.mono{font-family:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
51
|
+
|
|
52
|
+
.wrap{max-width:1180px;margin:0 auto;padding:0 28px}
|
|
53
|
+
|
|
54
|
+
/* ---------- hero ---------- */
|
|
55
|
+
.hero{
|
|
56
|
+
position:relative; overflow:hidden;
|
|
57
|
+
border-bottom:1px solid var(--rule);
|
|
58
|
+
background:
|
|
59
|
+
linear-gradient(var(--grid) 1px, transparent 1px) 0 0/24px 24px,
|
|
60
|
+
linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/24px 24px,
|
|
61
|
+
var(--surface);
|
|
62
|
+
}
|
|
63
|
+
.hero::after{
|
|
64
|
+
content:""; position:absolute; inset:0; pointer-events:none;
|
|
65
|
+
background:radial-gradient(120% 90% at 12% -10%, transparent 40%, var(--paper) 100%);
|
|
66
|
+
}
|
|
67
|
+
.hero .wrap{position:relative; z-index:1; padding-top:64px; padding-bottom:52px}
|
|
68
|
+
.eyebrow{
|
|
69
|
+
font-family:"IBM Plex Mono",monospace; font-size:11.5px; font-weight:600;
|
|
70
|
+
letter-spacing:.16em; text-transform:uppercase; color:var(--accent-ink); margin:0 0 18px;
|
|
71
|
+
}
|
|
72
|
+
h1{font-size:clamp(2.5rem,6vw,4.15rem);line-height:.98;font-weight:800;letter-spacing:-.025em;max-width:15ch}
|
|
73
|
+
.lede{margin:20px 0 0;max-width:62ch;font-size:1.14rem;color:var(--ink-soft)}
|
|
74
|
+
.lede strong{color:var(--ink);font-weight:600}
|
|
75
|
+
|
|
76
|
+
/* ---------- audit stats ---------- */
|
|
77
|
+
.audit{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));gap:1px;
|
|
78
|
+
background:var(--rule);border:1px solid var(--rule);border-radius:10px;overflow:hidden;margin-top:38px}
|
|
79
|
+
.audit div{background:var(--surface);padding:16px 18px}
|
|
80
|
+
.audit b{display:block;font-family:"Bricolage Grotesque",sans-serif;font-size:1.95rem;font-weight:700;
|
|
81
|
+
line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
|
|
82
|
+
.audit span{display:block;margin-top:7px;font-size:12.5px;line-height:1.4;color:var(--ink-soft)}
|
|
83
|
+
.audit .hot b{color:var(--accent-ink)}
|
|
84
|
+
.audit-note{margin:12px 0 0;font-size:12.5px;color:var(--ink-faint)}
|
|
85
|
+
|
|
86
|
+
/* ---------- layout ---------- */
|
|
87
|
+
.cols{display:grid;grid-template-columns:1fr;gap:0;padding:52px 0 88px}
|
|
88
|
+
@media(min-width:1020px){ .cols{grid-template-columns:210px 1fr;gap:56px} }
|
|
89
|
+
nav.toc{display:none}
|
|
90
|
+
@media(min-width:1020px){
|
|
91
|
+
nav.toc{display:block;position:sticky;top:32px;align-self:start}
|
|
92
|
+
nav.toc p{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.15em;
|
|
93
|
+
text-transform:uppercase;color:var(--ink-faint);margin:0 0 14px}
|
|
94
|
+
nav.toc a{display:block;padding:6px 0 6px 13px;border-left:2px solid var(--rule);
|
|
95
|
+
color:var(--ink-soft);text-decoration:none;font-size:13.5px;line-height:1.4}
|
|
96
|
+
nav.toc a:hover{color:var(--accent-ink);border-left-color:var(--accent)}
|
|
97
|
+
nav.toc a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
98
|
+
}
|
|
99
|
+
main{min-width:0}
|
|
100
|
+
section+section{margin-top:56px}
|
|
101
|
+
h2{font-size:1.72rem;font-weight:700;letter-spacing:-.02em;margin-bottom:6px}
|
|
102
|
+
h2 .n{font-family:"IBM Plex Mono",monospace;font-size:.85rem;font-weight:500;
|
|
103
|
+
color:var(--ink-faint);margin-right:12px;vertical-align:.32em;letter-spacing:.04em}
|
|
104
|
+
.sub{color:var(--ink-soft);max-width:66ch;margin:0 0 26px}
|
|
105
|
+
p{max-width:68ch}
|
|
106
|
+
|
|
107
|
+
/* ---------- tier chips ---------- */
|
|
108
|
+
.tier{display:inline-flex;align-items:center;gap:7px;font-family:"IBM Plex Mono",monospace;
|
|
109
|
+
font-size:10.5px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
|
|
110
|
+
padding:4px 9px;border-radius:999px;margin-bottom:14px}
|
|
111
|
+
.tier::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
|
|
112
|
+
.t-ok{color:var(--ok);background:var(--ok-soft)}
|
|
113
|
+
.t-warn{color:var(--warn);background:var(--warn-soft)}
|
|
114
|
+
.t-stop{color:var(--stop);background:var(--stop-soft)}
|
|
115
|
+
|
|
116
|
+
/* ---------- rule cards ---------- */
|
|
117
|
+
.rules{display:grid;gap:12px}
|
|
118
|
+
.rule{background:var(--surface);border:1px solid var(--rule);border-radius:11px;
|
|
119
|
+
padding:17px 19px;box-shadow:var(--shadow)}
|
|
120
|
+
.rule h3{font-size:1.02rem;font-weight:700;letter-spacing:-.005em;display:flex;
|
|
121
|
+
align-items:baseline;gap:10px;flex-wrap:wrap}
|
|
122
|
+
.rule h3 .tag{font-family:"IBM Plex Mono",monospace;font-size:10.5px;font-weight:500;
|
|
123
|
+
letter-spacing:.05em;color:var(--ink-faint);text-transform:none}
|
|
124
|
+
.rule p{margin:8px 0 0;font-size:14.6px;color:var(--ink-soft);max-width:70ch}
|
|
125
|
+
.rule p:last-child{margin-bottom:0}
|
|
126
|
+
.rule .why{color:var(--ink-faint);font-size:13.4px}
|
|
127
|
+
.s-ok{border-left:3px solid var(--ok)}
|
|
128
|
+
.s-warn{border-left:3px solid var(--warn)}
|
|
129
|
+
.s-stop{border-left:3px solid var(--stop)}
|
|
130
|
+
|
|
131
|
+
/* ---------- code ---------- */
|
|
132
|
+
pre{background:var(--surface-2);border:1px solid var(--rule);border-radius:9px;
|
|
133
|
+
padding:14px 16px;overflow-x:auto;margin:12px 0 0;font-size:13px;line-height:1.6}
|
|
134
|
+
pre code{color:var(--ink)}
|
|
135
|
+
:not(pre)>code{background:var(--surface-2);border:1px solid var(--rule);border-radius:5px;
|
|
136
|
+
padding:.1em .38em;font-size:.87em}
|
|
137
|
+
.del{color:var(--stop)} .add{color:var(--ok)} .dim{color:var(--ink-faint)}
|
|
138
|
+
|
|
139
|
+
/* ---------- tables ---------- */
|
|
140
|
+
.tablewrap{overflow-x:auto;border:1px solid var(--rule);border-radius:11px;background:var(--surface)}
|
|
141
|
+
table{border-collapse:collapse;width:100%;min-width:520px;font-size:14.3px}
|
|
142
|
+
th,td{text-align:left;padding:11px 16px;border-bottom:1px solid var(--rule);vertical-align:top}
|
|
143
|
+
th{font-family:"IBM Plex Mono",monospace;font-size:10.5px;font-weight:600;letter-spacing:.11em;
|
|
144
|
+
text-transform:uppercase;color:var(--ink-faint);background:var(--surface-2)}
|
|
145
|
+
tr:last-child td{border-bottom:none}
|
|
146
|
+
td code{white-space:nowrap}
|
|
147
|
+
|
|
148
|
+
/* ---------- checklist ---------- */
|
|
149
|
+
.check{background:var(--surface);border:1px solid var(--rule);border-radius:12px;
|
|
150
|
+
padding:8px 4px;box-shadow:var(--shadow)}
|
|
151
|
+
.check ol{margin:0;padding:0;list-style:none;counter-reset:c}
|
|
152
|
+
.check li{counter-increment:c;display:flex;gap:14px;padding:11px 18px;
|
|
153
|
+
border-bottom:1px solid var(--rule);font-size:14.7px;align-items:flex-start}
|
|
154
|
+
.check li:last-child{border-bottom:none}
|
|
155
|
+
.check li::before{content:counter(c,decimal-leading-zero);font-family:"IBM Plex Mono",monospace;
|
|
156
|
+
font-size:11.5px;font-weight:600;color:var(--accent-ink);padding-top:3px;flex:none;
|
|
157
|
+
font-variant-numeric:tabular-nums}
|
|
158
|
+
.check li b{font-weight:600}
|
|
159
|
+
.check li span{color:var(--ink-soft)}
|
|
160
|
+
|
|
161
|
+
.callout{border:1px solid var(--rule);border-left:3px solid var(--accent);
|
|
162
|
+
background:var(--accent-soft);border-radius:9px;padding:15px 18px;margin-top:22px}
|
|
163
|
+
.callout p{margin:0;font-size:14.6px;max-width:70ch}
|
|
164
|
+
.callout p+p{margin-top:9px}
|
|
165
|
+
|
|
166
|
+
footer{border-top:1px solid var(--rule);padding:26px 0 44px;color:var(--ink-faint);font-size:13px}
|
|
167
|
+
footer code{font-size:12.4px}
|
|
168
|
+
a{color:var(--accent-ink)}
|
|
169
|
+
</style>
|
|
170
|
+
|
|
171
|
+
<header class="hero">
|
|
172
|
+
<div class="wrap">
|
|
173
|
+
<p class="eyebrow">fontico · authoring spec</p>
|
|
174
|
+
<h1>The 24×24 Contract</h1>
|
|
175
|
+
<p class="lede">Every icon in the app — whether it comes from Lucide, Material Symbols, or your own hand — is merged into <strong>one build artifact</strong>. Vendor icons arrive uniform. Yours will not. This is the agreement between whoever draws the SVG and the pipeline that consumes it.</p>
|
|
176
|
+
|
|
177
|
+
<div class="audit">
|
|
178
|
+
<div class="hot"><b>34/40</b><span>sampled SVGs share <code>id="layer1"</code></span></div>
|
|
179
|
+
<div class="hot"><b>11</b><span>also share <code>id="path1"</code></span></div>
|
|
180
|
+
<div><b>4</b><span>embed raster <code><image></code> data</span></div>
|
|
181
|
+
<div><b>270.9</b><span>viewBox width on a file meant to be 24</span></div>
|
|
182
|
+
<div><b>1</b><span>ships live <code><text></code> instead of paths</span></div>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="audit-note">Measured across 40 SVGs found on this machine. These are not hypothetical failure modes — they are what your editor exports today.</p>
|
|
185
|
+
</div>
|
|
186
|
+
</header>
|
|
187
|
+
|
|
188
|
+
<div class="wrap cols">
|
|
189
|
+
<nav class="toc" aria-label="Contents">
|
|
190
|
+
<p>Contents</p>
|
|
191
|
+
<a href="#auto">Handled for you</a>
|
|
192
|
+
<a href="#yours">Your job</a>
|
|
193
|
+
<a href="#never">Cannot be fixed</a>
|
|
194
|
+
<a href="#export">Export settings</a>
|
|
195
|
+
<a href="#checklist">Ship checklist</a>
|
|
196
|
+
</nav>
|
|
197
|
+
|
|
198
|
+
<main>
|
|
199
|
+
<section id="auto">
|
|
200
|
+
<span class="tier t-ok">Handled automatically</span>
|
|
201
|
+
<h2><span class="n">01</span>What the preprocessor absorbs</h2>
|
|
202
|
+
<p class="sub">You do not have to hand-clean exports. Every SVG entering <code>app/assets/icons/</code> passes through normalisation before it reaches any emitter. These problems are solved, silently, on every build.</p>
|
|
203
|
+
|
|
204
|
+
<div class="rules">
|
|
205
|
+
<div class="rule s-ok">
|
|
206
|
+
<h3>Editor chrome is stripped <span class="tag">sodipodi · inkscape · metadata</span></h3>
|
|
207
|
+
<p>Namespaced editor state, RDF metadata blocks, XML declarations, DOCTYPEs, comments, and empty <code><defs></code> are removed entirely. An Inkscape file carrying window geometry and zoom level loses all of it.</p>
|
|
208
|
+
<pre><code><span class="del">- <sodipodi:namedview inkscape:zoom="0.19" inkscape:window-x="236" …/></span>
|
|
209
|
+
<span class="del">- <metadata id="metadata1"><rdf:RDF>…</rdf:RDF></metadata></span>
|
|
210
|
+
<span class="del">- <defs id="defs1" /></span></code></pre>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="rule s-ok">
|
|
214
|
+
<h3>Every id is namespaced <span class="tag">the collision fix</span></h3>
|
|
215
|
+
<p>Ids are rewritten to <code><slug>__<id></code>, and every internal reference is rewritten with them — <code>url(#…)</code> in fills and strokes, <code>href</code>, <code>clip-path</code>, <code>mask</code>, <code>filter</code>. This is what makes generic sequential ids safe to merge.</p>
|
|
216
|
+
<pre><code><span class="del">- <linearGradient id="path1"> fill="url(#path1)"</span>
|
|
217
|
+
<span class="add">+ <linearGradient id="logo__path1"> fill="url(#logo__path1)"</span></code></pre>
|
|
218
|
+
<p class="why">Without this, two icons both defining <code>path1</code> collapse into one definition and the second silently wins. With 34 of 40 files sharing <code>layer1</code>, collision is the default outcome, not the edge case.</p>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div class="rule s-ok">
|
|
222
|
+
<h3>viewBox is rescaled to the common box</h3>
|
|
223
|
+
<p>Any source box is fitted into the target, centred, aspect preserved, via a wrapping transform. Absolute <code>width</code>/<code>height</code> attributes are dropped so the icon scales with its container instead of pinning itself to 1024px.</p>
|
|
224
|
+
<p class="why">This matters beyond your own files: Lucide and Material are both 24, but <code>fa6-solid</code> is 512 with per-icon overrides to 576, and <code>bi</code> declares no width at all. The pipeline cannot assume a number.</p>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="rule s-ok">
|
|
228
|
+
<h3>Colour is folded to <code>currentColor</code></h3>
|
|
229
|
+
<p>Single-colour icons get their fills and strokes rewritten so they inherit CSS <code>color</code> like text. Icons declared <code>multicolor: true</code> in the manifest keep their palette untouched.</p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="rule s-ok">
|
|
233
|
+
<h3>Unsafe content is removed</h3>
|
|
234
|
+
<p><code><script></code>, all <code>on*</code> event handlers, <code><foreignObject></code>, and any external reference are dropped. An SVG from a designer is untrusted markup that you are about to inline into your DOM — an inlined handler in a sprite <code><symbol></code> executes.</p>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</section>
|
|
238
|
+
|
|
239
|
+
<section id="yours">
|
|
240
|
+
<span class="tier t-warn">Your job when drawing</span>
|
|
241
|
+
<h2><span class="n">02</span>What the pipeline can't decide for you</h2>
|
|
242
|
+
<p class="sub">Normalisation fixes structure. It cannot fix judgement. These are geometry and composition choices that have to be right in the source file.</p>
|
|
243
|
+
|
|
244
|
+
<div class="rules">
|
|
245
|
+
<div class="rule s-warn">
|
|
246
|
+
<h3>Draw on a 24×24 grid, snapped to whole pixels</h3>
|
|
247
|
+
<p>Set the document to 24×24 units and align edges to integers (half-integers for 1px-wide strokes). Rescaling from 270.93 to 24 is arithmetic the pipeline will do, but coordinates that were never on a grid stay off-grid afterwards, and land blurry at 16px.</p>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="rule s-warn">
|
|
251
|
+
<h3>Leave a 1–2 unit margin inside the box</h3>
|
|
252
|
+
<p>Artwork should occupy roughly the middle 20–22 units. Icons drawn edge-to-edge appear oversized beside vendor icons that respect the same padding, and the mismatch is obvious in a toolbar.</p>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<div class="rule s-warn">
|
|
256
|
+
<h3>Match the stroke weight you're sitting next to</h3>
|
|
257
|
+
<p>Lucide is 2 units at 24 with round caps and round joins. A first-party icon at 1.5 or 3 reads as a different family even when the shapes are right.</p>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="rule s-warn">
|
|
261
|
+
<h3>Outline your strokes if the icon goes in a font <span class="tag">required, not optional</span></h3>
|
|
262
|
+
<p>Save the artwork as filled contours rather than stroked centrelines. Nothing downstream can do this losslessly, so a stroked first-party icon in a font target stops the build by name rather than shipping a blob. The sprite renders strokes natively and does not care.</p>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="rule s-warn">
|
|
266
|
+
<h3>Union overlapping shapes; avoid self-intersection</h3>
|
|
267
|
+
<p>Merge overlapping subpaths into one contour before saving (<span class="mono">Path → Union</span>). Fonts fill by non-zero winding, so overlaps and reversed contour directions turn holes solid — the classic icon-font artifact.</p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="rule s-warn">
|
|
271
|
+
<h3>Flatten transforms into coordinates</h3>
|
|
272
|
+
<p>Nested <code>transform=</code> chains survive normalisation but compound with the fitting transform and make debugging geometry miserable. In Inkscape: <span class="mono">Preferences → Behaviour → Transforms → Store transformation: Optimized</span>.</p>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div class="rule s-warn">
|
|
276
|
+
<h3>Name the file as the slug</h3>
|
|
277
|
+
<p>The filename is the identifier. <code>empty-box.svg</code> resolves as <code>local/empty-box</code>. Lowercase, hyphens, no spaces, no version suffixes — <code>logo-final-v3.svg</code> becomes an icon literally named <code>logo-final-v3</code>.</p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</section>
|
|
281
|
+
|
|
282
|
+
<section id="never">
|
|
283
|
+
<span class="tier t-stop">No preprocessing can save these</span>
|
|
284
|
+
<h2><span class="n">03</span>Hard failures</h2>
|
|
285
|
+
<p class="sub">Four things are not cleanup problems. They are properties of the output format, and the only fix is upstream in the drawing.</p>
|
|
286
|
+
|
|
287
|
+
<div class="rules">
|
|
288
|
+
<div class="rule s-stop">
|
|
289
|
+
<h3>Live <code><text></code> elements</h3>
|
|
290
|
+
<p>Text renders with whatever font the viewer's machine has, or nothing. It cannot become a glyph outline at all. Convert every text object to paths before saving — <span class="mono">Path → Object to Path</span> in Inkscape, <span class="mono">Type → Create Outlines</span> in Illustrator, <span class="mono">Outline text</span> on Figma export.</p>
|
|
291
|
+
<p class="why">A real file in your projects is 1782 bytes whose entire artwork is one <code><text></code> node at <code>font-size:238.845px</code>. It will render differently on every machine that opens it.</p>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="rule s-stop">
|
|
295
|
+
<h3>Embedded raster <code><image></code> data</h3>
|
|
296
|
+
<p>A base64 PNG inside an SVG defeats the entire point: it does not scale, it cannot be a glyph, and it inflates the sprite by orders of magnitude. Redraw as vector or keep it out of the icon set.</p>
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<div class="rule s-stop">
|
|
300
|
+
<h3>Multiple colours, in a font target</h3>
|
|
301
|
+
<p>A TrueType glyph stores <em>no colour</em> — it is filled contours, coloured at render time by CSS <code>color</code>. Two-tone is unrepresentable, not merely difficult. Multicolour icons ride in the sprite and are skipped by the font emitter, which reports each one it dropped.</p>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div class="rule s-stop">
|
|
305
|
+
<h3>Filters, blurs, and blend modes</h3>
|
|
306
|
+
<p><code><filter></code> effects do not survive font conversion and are expensive to rasterise in a sprite. Bake the effect into geometry or drop it.</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div class="callout">
|
|
311
|
+
<p><b>The stroke trap, specifically.</b> A stroked path with <code>fill="none"</code> does not fail loudly in a font — it fills the <em>centreline</em> as a solid region and produces a blob that looks nothing like the icon.</p>
|
|
312
|
+
<p><b>Strokes are not expanded for you.</b> Both published JavaScript expanders run the artwork through potrace: they rasterise, re-trace, and hand back rounded corners and wobbling stems. Rather than ship geometry that quietly stopped matching the sprite, the build <em>refuses</em> and names the icon.</p>
|
|
313
|
+
<p>Vendor sets are handled — where a provider publishes its own font, outlines are lifted from there, already expanded and tuned by the people who drew them. For <b>your</b> icons there is no such source, so outlining is your job: <span class="mono">Path → Stroke to Path</span> in Inkscape, <span class="mono">Object → Path → Outline Stroke</span> in Illustrator, <span class="mono">Outline stroke</span> in Figma.</p>
|
|
314
|
+
</div>
|
|
315
|
+
</section>
|
|
316
|
+
|
|
317
|
+
<section id="export">
|
|
318
|
+
<h2><span class="n">04</span>Export settings</h2>
|
|
319
|
+
<p class="sub">Getting these right removes most of the cleanup before it starts, and makes diffs on icon files readable.</p>
|
|
320
|
+
|
|
321
|
+
<div class="tablewrap">
|
|
322
|
+
<table>
|
|
323
|
+
<thead><tr><th>Tool</th><th>Do this</th><th>Not this</th></tr></thead>
|
|
324
|
+
<tbody>
|
|
325
|
+
<tr>
|
|
326
|
+
<td><b>Inkscape</b></td>
|
|
327
|
+
<td><span class="mono">File → Save As → Optimized SVG</span>, with <em>Remove metadata</em>, <em>Remove comments</em>, and <em>Remove unused defs</em> checked; 3 decimal places</td>
|
|
328
|
+
<td><span class="mono">Inkscape SVG</span> — keeps <code>sodipodi:</code>, <code>inkscape:</code>, layer state, window geometry</td>
|
|
329
|
+
</tr>
|
|
330
|
+
<tr>
|
|
331
|
+
<td><b>Illustrator</b></td>
|
|
332
|
+
<td><span class="mono">Export As → SVG</span>, Styling: <em>Presentation Attributes</em>, Object IDs: <em>Minimal</em>, Decimals 2, Responsive <em>off</em></td>
|
|
333
|
+
<td>Styling: <em>Internal CSS</em> — emits a <code><style></code> block whose class names collide exactly like ids do</td>
|
|
334
|
+
</tr>
|
|
335
|
+
<tr>
|
|
336
|
+
<td><b>Figma</b></td>
|
|
337
|
+
<td>Frame at 24×24, <span class="mono">Export → SVG</span> with <em>Outline text</em> on and <em>Include "id" attribute</em> off</td>
|
|
338
|
+
<td>Exporting a nested component — brings the wrapper frame in as an extra clip path</td>
|
|
339
|
+
</tr>
|
|
340
|
+
</tbody>
|
|
341
|
+
</table>
|
|
342
|
+
</div>
|
|
343
|
+
</section>
|
|
344
|
+
|
|
345
|
+
<section id="checklist">
|
|
346
|
+
<h2><span class="n">05</span>Before you commit the file</h2>
|
|
347
|
+
<p class="sub">Run this once per icon. It takes under a minute and prevents every failure documented above.</p>
|
|
348
|
+
|
|
349
|
+
<div class="check">
|
|
350
|
+
<ol>
|
|
351
|
+
<li><b>Document is 24×24</b><span> — and the artwork sits inside the middle 20–22 units.</span></li>
|
|
352
|
+
<li><b>All text converted to paths</b><span> — no <code><text></code> or <code><tspan></code> anywhere in the file.</span></li>
|
|
353
|
+
<li><b>No embedded images</b><span> — search the file for <code><image</code> and for <code>base64</code>.</span></li>
|
|
354
|
+
<li><b>Overlapping shapes unioned</b><span> — one contour per visual region.</span></li>
|
|
355
|
+
<li><b>Single colour is pure black</b><span> — unless the icon is genuinely multicolour, in which case flag it in the manifest.</span></li>
|
|
356
|
+
<li><b>Saved as Optimized / Plain SVG</b><span> — not the editor's native format.</span></li>
|
|
357
|
+
<li><b>Filename is the slug</b><span> — lowercase, hyphenated, final.</span></li>
|
|
358
|
+
<li><b>Rendered at 16px</b><span> — if it turns to mud, the coordinates are off-grid.</span></li>
|
|
359
|
+
</ol>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div class="callout">
|
|
363
|
+
<p><b>You do not have to be perfect.</b> Items 1–8 make the output better and the diffs smaller, but the preprocessor is the safety net — a raw Inkscape export with <code>layer1</code>, a 270.93 viewBox, and editor metadata still builds correctly. Only section 03 contains things it genuinely cannot rescue.</p>
|
|
364
|
+
</div>
|
|
365
|
+
</section>
|
|
366
|
+
</main>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<footer class="wrap">
|
|
370
|
+
Authoring spec for <code>app/assets/icons/</code> · icons declared in <code>icons.yml</code> · rebuild with <code>rake fontico:build</code>
|
|
371
|
+
</footer>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/docs/ttf-glyphs.png
ADDED
|
Binary file
|