diff --git a/apps/logicsrc-web/public/openicon/add.svg b/apps/logicsrc-web/public/openicon/add.svg new file mode 100644 index 0000000..a31cd80 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/add.svg @@ -0,0 +1 @@ +Add diff --git a/apps/logicsrc-web/public/openicon/at.svg b/apps/logicsrc-web/public/openicon/at.svg new file mode 100644 index 0000000..6b3256b --- /dev/null +++ b/apps/logicsrc-web/public/openicon/at.svg @@ -0,0 +1 @@ +At diff --git a/apps/logicsrc-web/public/openicon/bell.svg b/apps/logicsrc-web/public/openicon/bell.svg new file mode 100644 index 0000000..8761b09 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/bell.svg @@ -0,0 +1 @@ +Bell diff --git a/apps/logicsrc-web/public/openicon/bluesky.svg b/apps/logicsrc-web/public/openicon/bluesky.svg new file mode 100644 index 0000000..5b2c9a4 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/bluesky.svg @@ -0,0 +1 @@ +Bluesky diff --git a/apps/logicsrc-web/public/openicon/calendar.svg b/apps/logicsrc-web/public/openicon/calendar.svg new file mode 100644 index 0000000..6da38dc --- /dev/null +++ b/apps/logicsrc-web/public/openicon/calendar.svg @@ -0,0 +1 @@ +Calendar diff --git a/apps/logicsrc-web/public/openicon/chat.svg b/apps/logicsrc-web/public/openicon/chat.svg new file mode 100644 index 0000000..2054928 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/chat.svg @@ -0,0 +1 @@ +Chat diff --git a/apps/logicsrc-web/public/openicon/cloud.svg b/apps/logicsrc-web/public/openicon/cloud.svg new file mode 100644 index 0000000..72f1b5a --- /dev/null +++ b/apps/logicsrc-web/public/openicon/cloud.svg @@ -0,0 +1 @@ +Cloud diff --git a/apps/logicsrc-web/public/openicon/code.svg b/apps/logicsrc-web/public/openicon/code.svg new file mode 100644 index 0000000..8e89a8c --- /dev/null +++ b/apps/logicsrc-web/public/openicon/code.svg @@ -0,0 +1 @@ +Code diff --git a/apps/logicsrc-web/public/openicon/copy.svg b/apps/logicsrc-web/public/openicon/copy.svg new file mode 100644 index 0000000..6a474b6 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/copy.svg @@ -0,0 +1 @@ +Copy diff --git a/apps/logicsrc-web/public/openicon/database.svg b/apps/logicsrc-web/public/openicon/database.svg new file mode 100644 index 0000000..da0bd07 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/database.svg @@ -0,0 +1 @@ +Database diff --git a/apps/logicsrc-web/public/openicon/delete.svg b/apps/logicsrc-web/public/openicon/delete.svg new file mode 100644 index 0000000..7e247e1 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/delete.svg @@ -0,0 +1 @@ +Delete diff --git a/apps/logicsrc-web/public/openicon/discord.svg b/apps/logicsrc-web/public/openicon/discord.svg new file mode 100644 index 0000000..ed4e322 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/discord.svg @@ -0,0 +1 @@ +Discord diff --git a/apps/logicsrc-web/public/openicon/download.svg b/apps/logicsrc-web/public/openicon/download.svg new file mode 100644 index 0000000..0d4a79a --- /dev/null +++ b/apps/logicsrc-web/public/openicon/download.svg @@ -0,0 +1 @@ +Download diff --git a/apps/logicsrc-web/public/openicon/edit.svg b/apps/logicsrc-web/public/openicon/edit.svg new file mode 100644 index 0000000..9b4fcd4 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/edit.svg @@ -0,0 +1 @@ +Edit diff --git a/apps/logicsrc-web/public/openicon/eye.svg b/apps/logicsrc-web/public/openicon/eye.svg new file mode 100644 index 0000000..b90e2d7 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/eye.svg @@ -0,0 +1 @@ +Eye diff --git a/apps/logicsrc-web/public/openicon/filter.svg b/apps/logicsrc-web/public/openicon/filter.svg new file mode 100644 index 0000000..cae2fc3 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/filter.svg @@ -0,0 +1 @@ +Filter diff --git a/apps/logicsrc-web/public/openicon/git-branch.svg b/apps/logicsrc-web/public/openicon/git-branch.svg new file mode 100644 index 0000000..354220d --- /dev/null +++ b/apps/logicsrc-web/public/openicon/git-branch.svg @@ -0,0 +1 @@ +Git branch diff --git a/apps/logicsrc-web/public/openicon/git-pull-request.svg b/apps/logicsrc-web/public/openicon/git-pull-request.svg new file mode 100644 index 0000000..ba1fd39 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/git-pull-request.svg @@ -0,0 +1 @@ +Git pull request diff --git a/apps/logicsrc-web/public/openicon/github.svg b/apps/logicsrc-web/public/openicon/github.svg new file mode 100644 index 0000000..b06a1c9 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/github.svg @@ -0,0 +1 @@ +GitHub diff --git a/apps/logicsrc-web/public/openicon/globe.svg b/apps/logicsrc-web/public/openicon/globe.svg new file mode 100644 index 0000000..fd71903 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/globe.svg @@ -0,0 +1 @@ +Globe diff --git a/apps/logicsrc-web/public/openicon/home.svg b/apps/logicsrc-web/public/openicon/home.svg new file mode 100644 index 0000000..09e1a91 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/home.svg @@ -0,0 +1 @@ +Home diff --git a/apps/logicsrc-web/public/openicon/key.svg b/apps/logicsrc-web/public/openicon/key.svg new file mode 100644 index 0000000..eb9d31f --- /dev/null +++ b/apps/logicsrc-web/public/openicon/key.svg @@ -0,0 +1 @@ +Key diff --git a/apps/logicsrc-web/public/openicon/link.svg b/apps/logicsrc-web/public/openicon/link.svg new file mode 100644 index 0000000..abb19e4 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/link.svg @@ -0,0 +1 @@ +Link diff --git a/apps/logicsrc-web/public/openicon/lock.svg b/apps/logicsrc-web/public/openicon/lock.svg new file mode 100644 index 0000000..fb0a00b --- /dev/null +++ b/apps/logicsrc-web/public/openicon/lock.svg @@ -0,0 +1 @@ +Lock diff --git a/apps/logicsrc-web/public/openicon/mail.svg b/apps/logicsrc-web/public/openicon/mail.svg new file mode 100644 index 0000000..85a45fb --- /dev/null +++ b/apps/logicsrc-web/public/openicon/mail.svg @@ -0,0 +1 @@ +Mail diff --git a/apps/logicsrc-web/public/openicon/map-pin.svg b/apps/logicsrc-web/public/openicon/map-pin.svg new file mode 100644 index 0000000..ae0c858 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/map-pin.svg @@ -0,0 +1 @@ +Map pin diff --git a/apps/logicsrc-web/public/openicon/mastodon.svg b/apps/logicsrc-web/public/openicon/mastodon.svg new file mode 100644 index 0000000..3fb26d8 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/mastodon.svg @@ -0,0 +1 @@ +Mastodon diff --git a/apps/logicsrc-web/public/openicon/menu.svg b/apps/logicsrc-web/public/openicon/menu.svg new file mode 100644 index 0000000..e8d6af6 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/menu.svg @@ -0,0 +1 @@ +Menu diff --git a/apps/logicsrc-web/public/openicon/npm.svg b/apps/logicsrc-web/public/openicon/npm.svg new file mode 100644 index 0000000..2da4ec9 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/npm.svg @@ -0,0 +1 @@ +npm diff --git a/apps/logicsrc-web/public/openicon/package.svg b/apps/logicsrc-web/public/openicon/package.svg new file mode 100644 index 0000000..90a87ec --- /dev/null +++ b/apps/logicsrc-web/public/openicon/package.svg @@ -0,0 +1 @@ +Package diff --git a/apps/logicsrc-web/public/openicon/phone.svg b/apps/logicsrc-web/public/openicon/phone.svg new file mode 100644 index 0000000..8b157b2 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/phone.svg @@ -0,0 +1 @@ +Phone diff --git a/apps/logicsrc-web/public/openicon/refresh.svg b/apps/logicsrc-web/public/openicon/refresh.svg new file mode 100644 index 0000000..5848815 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/refresh.svg @@ -0,0 +1 @@ +Refresh diff --git a/apps/logicsrc-web/public/openicon/rocket.svg b/apps/logicsrc-web/public/openicon/rocket.svg new file mode 100644 index 0000000..8d9235f --- /dev/null +++ b/apps/logicsrc-web/public/openicon/rocket.svg @@ -0,0 +1 @@ +Rocket diff --git a/apps/logicsrc-web/public/openicon/rss.svg b/apps/logicsrc-web/public/openicon/rss.svg new file mode 100644 index 0000000..139e0ad --- /dev/null +++ b/apps/logicsrc-web/public/openicon/rss.svg @@ -0,0 +1 @@ +Rss diff --git a/apps/logicsrc-web/public/openicon/search.svg b/apps/logicsrc-web/public/openicon/search.svg new file mode 100644 index 0000000..b9c999b --- /dev/null +++ b/apps/logicsrc-web/public/openicon/search.svg @@ -0,0 +1 @@ +Search diff --git a/apps/logicsrc-web/public/openicon/settings.svg b/apps/logicsrc-web/public/openicon/settings.svg new file mode 100644 index 0000000..ae3eb5c --- /dev/null +++ b/apps/logicsrc-web/public/openicon/settings.svg @@ -0,0 +1 @@ +Settings diff --git a/apps/logicsrc-web/public/openicon/share.svg b/apps/logicsrc-web/public/openicon/share.svg new file mode 100644 index 0000000..3842aee --- /dev/null +++ b/apps/logicsrc-web/public/openicon/share.svg @@ -0,0 +1 @@ +Share diff --git a/apps/logicsrc-web/public/openicon/signal.svg b/apps/logicsrc-web/public/openicon/signal.svg new file mode 100644 index 0000000..16c88dc --- /dev/null +++ b/apps/logicsrc-web/public/openicon/signal.svg @@ -0,0 +1 @@ +Signal diff --git a/apps/logicsrc-web/public/openicon/slack.svg b/apps/logicsrc-web/public/openicon/slack.svg new file mode 100644 index 0000000..6772c76 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/slack.svg @@ -0,0 +1 @@ +Slack diff --git a/apps/logicsrc-web/public/openicon/sparkles.svg b/apps/logicsrc-web/public/openicon/sparkles.svg new file mode 100644 index 0000000..abc01c6 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/sparkles.svg @@ -0,0 +1 @@ +Sparkles diff --git a/apps/logicsrc-web/public/openicon/terminal.svg b/apps/logicsrc-web/public/openicon/terminal.svg new file mode 100644 index 0000000..7776fcf --- /dev/null +++ b/apps/logicsrc-web/public/openicon/terminal.svg @@ -0,0 +1 @@ +Terminal diff --git a/apps/logicsrc-web/public/openicon/upload.svg b/apps/logicsrc-web/public/openicon/upload.svg new file mode 100644 index 0000000..6aa749e --- /dev/null +++ b/apps/logicsrc-web/public/openicon/upload.svg @@ -0,0 +1 @@ +Upload diff --git a/apps/logicsrc-web/public/openicon/user.svg b/apps/logicsrc-web/public/openicon/user.svg new file mode 100644 index 0000000..fc57687 --- /dev/null +++ b/apps/logicsrc-web/public/openicon/user.svg @@ -0,0 +1 @@ +User diff --git a/apps/logicsrc-web/public/openicon/users.svg b/apps/logicsrc-web/public/openicon/users.svg new file mode 100644 index 0000000..cf3b72c --- /dev/null +++ b/apps/logicsrc-web/public/openicon/users.svg @@ -0,0 +1 @@ +Users diff --git a/apps/logicsrc-web/public/openicon/whatsapp.svg b/apps/logicsrc-web/public/openicon/whatsapp.svg new file mode 100644 index 0000000..46ddcba --- /dev/null +++ b/apps/logicsrc-web/public/openicon/whatsapp.svg @@ -0,0 +1 @@ +WhatsApp diff --git a/apps/logicsrc-web/public/openicon/x.svg b/apps/logicsrc-web/public/openicon/x.svg new file mode 100644 index 0000000..31e23fc --- /dev/null +++ b/apps/logicsrc-web/public/openicon/x.svg @@ -0,0 +1 @@ +X diff --git a/apps/logicsrc-web/public/openicon/youtube.svg b/apps/logicsrc-web/public/openicon/youtube.svg new file mode 100644 index 0000000..9b26f1d --- /dev/null +++ b/apps/logicsrc-web/public/openicon/youtube.svg @@ -0,0 +1 @@ +YouTube diff --git a/apps/logicsrc-web/src/app/openicon/page.tsx b/apps/logicsrc-web/src/app/openicon/page.tsx new file mode 100644 index 0000000..2856c7d --- /dev/null +++ b/apps/logicsrc-web/src/app/openicon/page.tsx @@ -0,0 +1,248 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; +import type { Metadata } from "next"; +import { SiteShell } from "@/components/site-shell"; +import { mono, pre, table, td, th } from "../openontology/ui"; + +export const metadata: Metadata = { + title: "OpenIcon ยท LogicSRC", + description: + "OpenIcon is an icon set as a folder: one openicon.json naming every icon with aliases and keywords, 24x24 currentColor SVGs, and three terminal glyphs per icon (Nerd Font, Unicode, ASCII) so a TUI draws the best one it can. Brand logos say they are brands.", + alternates: { canonical: "/openicon" } +}; + +/** From the reference set, profullstack/openicon; copied into public/openicon. */ +const SAMPLE = [ + "mail", "phone", "chat", "at", "link", "search", "settings", "filter", "menu", "add", "edit", "delete", + "copy", "share", "download", "upload", "refresh", "bell", "calendar", "user", "users", "lock", "key", "eye", + "home", "map-pin", "globe", "cloud", "database", "package", "terminal", "code", "git-branch", "git-pull-request", + "rocket", "sparkles", "rss", "github", "x", "bluesky", "mastodon", "discord", "slack", "signal", "whatsapp", + "youtube", "npm" +]; + +/** key, Nerd Font codepoint, Unicode, ASCII: straight from openicon.json. */ +const GLYPHS: Array<[string, string, string, string]> = [ + ["mail", "U+F01F0", "โœ‰", "@"], + ["phone", "U+F0DF0", "โ˜Ž", "tel"], + ["link", "U+F0339", "๐Ÿ”—", "~"], + ["settings", "U+F0493", "โš™", "*"], + ["terminal", "U+F018D", "โŒจ", ">_"], + ["git-branch", "U+F418", "โއ", "Y"], + ["warning", "U+F002A", "โš ", "!"], + ["github", "U+F02A4", "๐Ÿ™", "gh"] +]; + +const EXAMPLE = `{ + "openicon": "0.1", + "name": "OpenIcon", + "license": "MIT", + "made_by": "ai", + "grid": { "size": 24, "stroke": 2, "padding": 2 }, + "sizes": [16, 24, 32, 64, 128], + "icons": [ + { "key": "mail", "category": "communication", + "aliases": ["email", "envelope"], "keywords": ["message", "inbox"], + "svg": "svg/mail.svg", "png": "png/{size}/mail.png", + "tui": { "nerd": "\\u{f01f0}", "nerd_code": "f01f0", + "nerd_name": "md-email_outline", "unicode": "โœ‰", "ascii": "@" } }, + { "key": "github", "category": "brand", "brand": true, + "trademark": "GitHub and its logo are trademarks of their owner. โ€ฆ", + "source": "simple-icons:github", "license": "CC0-1.0", "made_by": "human", + "svg": "svg/github.svg", + "tui": { "nerd_code": "f02a4", "unicode": "๐Ÿ™", "ascii": "gh" } } + ] +}`; + +const RULES: Array<[string, string]> = [ + ["A set is a folder", "openicon.json at the root, every path relative to it; unknown keys are kept."], + ["The key is the name", "Kebab case: mail, git-pull-request. The key is the file name; a brand's key is its own name."], + ["Aliases find one icon, keywords find many", "email is mail everywhere, so every key and alias is unique; keywords like money are shared search terms."], + ["Files take the text colour", "24x24 view box, currentColor, and a grid block stating size, stroke and padding so sets can be matched."], + ["Every icon has terminal glyphs", "tui.nerd (with its codepoint and Nerd Fonts name), tui.unicode (one character), tui.ascii (1 to 4 printable characters)."], + ["A terminal picks the best it can draw", "Nerd, then Unicode, then ASCII, from OPENICON_GLYPHS, NERD_FONT=1 or the locale. Pad to the cell width: an emoji can be two columns."], + ["A brand says it is one", "brand: true, a trademark note, its source and its own licence. Logos are taken from their owners or a set that publishes them, never redrawn."], + ["The set says what made it", "made_by and the W3C AI disclosure vocabulary, per set and per icon: the reference set's drawn icons are ai, its logos human."] +]; + +const ABSENT: Array<[string, string]> = [ + ["No colour", "Icons take the text colour. A brand's own colour is optional and ignorable."], + ["No style variants", "Outline and filled are two sets, or two keys. One key, one drawing."], + ["No webfont format", "A font is a build output a set may ship; SVG and the tui glyphs are the interchange."], + ["No name registry", "Keys are the set's own; aliases are how it answers to another set's names."], + ["No animation", "A spinner is a component, not an icon."] +]; + +export default function OpenIconPage(): ReactNode { + return ( + +
+
+

LogicSRC standards surface

+

OpenIcon

+

+ An icon set as a folder, and the first one that knows what to draw in a terminal: SVG in a browser, a + Nerd Font glyph in a patched terminal, a Unicode symbol in a plain one, ASCII over a serial line. +

+
+

+ Every interface needs the same few hundred icons, and every set names them differently: Lucide's mail + is Font Awesome's envelope is Material's email. Brand logos sit beside drawn icons as if a + trademark were just another shape. And no set says what an icon should be when there are no pixels, so + TUIs hard-code a Nerd Font codepoint and show a box everywhere else. +

+

+ Status: 0.1. A sibling of OpenEmoji. +

+
+ +
+
+

The reference set

+

+ 370 icons: 259 drawn on a 24x24 grid with 2px strokes, 111 brand logos from Simple Icons and Font Awesome + Free. profullstack/openicon. +

+
+
+ {SAMPLE.map((key) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
{key}
+
+ ))} +
+
+ +
+
+

Three glyphs per icon

+

What a TUI draws, best first. Your browser probably has no Nerd Font, so that column shows the codepoint.

+
+ + + + + + + + + + + {GLYPHS.map(([key, nerd, unicode, ascii]) => ( + + + + + + + ))} + +
IconNerd FontUnicodeASCII
+ {key} + + {nerd} + {unicode} + {ascii} +
+
+ +
+
+

The shape

+

openicon.json, trimmed to a drawn icon and a brand logo.

+
+
{EXAMPLE}
+
+ +
+
+

The eight rules

+

Every one of them degrades rather than fails.

+
+ + + + + + + + + {RULES.map(([rule, meaning], index) => ( + + + + + ))} + +
RuleWhat it means
+ + {index + 1}. {rule} + + {meaning}
+
+ +
+
+

What is deliberately absent

+
+ + + {ABSENT.map(([what, why]) => ( + + + + + ))} + +
+ {what} + {why}
+
+ +
+
+

Use it

+

+ icon in profullstack/cli-tools{" "} + builds the set; hqtui ships it as its default icon pack. +

+
+
{`icon build --out ./openicon   # openicon.json, svg/, png/, sprite.svg
+icon glyph mail               # the best glyph this terminal can draw
+
+// hqtui
+import { icon } from "@profullstack/hqtui";
+icon("mail");                 // "\\u{f01f0}", "โœ‰" or "@"`}
+
+ +
+
+

Where everything lives

+
+ +
+
+ ); +} diff --git a/apps/logicsrc-web/src/lib/specs.ts b/apps/logicsrc-web/src/lib/specs.ts index 24afcef..5497fcf 100644 --- a/apps/logicsrc-web/src/lib/specs.ts +++ b/apps/logicsrc-web/src/lib/specs.ts @@ -97,6 +97,7 @@ export const FAMILIES: Family[] = [ s("openrecipe", "OpenRecipe.md", "One Markdown file that is a recipe, with schema.org derived from it and never the reverse"), s("opensong", "OpenSong", "One plain-text file that is a song: title, style, exclusions and lyrics as the blocks a generator takes, kept beside the audio"), s("openemoji", "OpenEmoji", "An emoji set as a folder: one file that states coverage, licence and whether a person or a model drew it, and glyphs named by the codepoints they draw"), + s("openicon", "OpenIcon", "An icon set as a folder: every icon named, with aliases, 24x24 currentColor SVGs and a Nerd Font, Unicode and ASCII glyph each, so a terminal draws the best one it can"), s("openthreat", "OpenThreat", "One file a security tool serves about what it found in the open: public subjects only, secrets never located"), s("openrental", "OpenRental", "One file an operator serves about the agents and file swarms it rents out: members, metadata and rates through CoinPay", { landing: undefined, status: "draft" }), s("opensite", "OpenSite", "One record about a page or a site: the card a reader would draw, declared by the site or read from it, kept by an index"), diff --git a/docs/openicon.md b/docs/openicon.md new file mode 100644 index 0000000..aafdb01 --- /dev/null +++ b/docs/openicon.md @@ -0,0 +1,127 @@ +# OpenIcon + +OpenIcon is an icon set as a folder: one `openicon.json` at the top that names every icon, says where its files are and who or what drew it, and gives each icon three terminal glyphs, so the same `mail` icon is an SVG in a browser, a PNG in an email, ๓ฐ‡ฐ in a terminal with a Nerd Font, โœ‰ in one without, and `@` over a serial line. It is maintained by Profullstack, Inc. as part of the LogicSRC open-standards surface. + +Status: **0.1**. A sibling of [OpenEmoji](/openemoji): files first, one small descriptor, `made_by` on the work. + +Slug: `openicon` + +## The problem + +Every interface needs the same few hundred icons: mail, phone, link, search, settings, a trash can, a GitHub logo. Every icon set ships them under its own names (Lucide's `mail` is Font Awesome's `envelope` is Material's `email`), in its own folder layout, with its licence in a README, and with brand logos mixed in beside drawn icons as if a trademark were just another shape. + +Terminals are worse. A TUI that wants an icon has three choices, all bad: hard-code a Nerd Font codepoint and show a box on every terminal without one, hard-code an emoji and break the column width, or draw nothing. No icon set says what an icon should be when there are no pixels at all. + +## The shape + +``` +openicon.json the descriptor (rule 1) +svg/.svg 24x24, currentColor +png//.png rendered from the SVG +sprite.svg every icon as a , optional +``` + +```json +{ + "openicon": "0.1", + "name": "OpenIcon", + "version": "2026-09-24", + "license": "MIT", + "made_by": "ai", + "disclosure": "ai-generated", + "ai_model": "claude-opus-5-5", + "ai_provider": "Anthropic", + "grid": { "size": 24, "stroke": 2, "padding": 2 }, + "sizes": [16, 20, 24, 32, 48, 64, 128, 256], + "sprite": "sprite.svg", + "icons": [ + { + "key": "mail", + "name": "Mail", + "category": "communication", + "aliases": ["email", "envelope"], + "keywords": ["message", "inbox"], + "svg": "svg/mail.svg", + "png": "png/{size}/mail.png", + "tui": { "nerd": "๓ฐ‡ฐ", "nerd_code": "f01f0", "nerd_name": "md-email_outline", "unicode": "โœ‰", "ascii": "@" } + }, + { + "key": "github", + "name": "GitHub", + "category": "brand", + "brand": true, + "trademark": "GitHub and its logo are trademarks of their owner. Use them to refer to GitHub, not to imply endorsement.", + "source": "simple-icons:github", + "license": "CC0-1.0", + "made_by": "human", + "svg": "svg/github.svg", + "png": "png/{size}/github.png", + "tui": { "nerd": "๓ฐŠค", "nerd_code": "f02a4", "nerd_name": "md-github", "unicode": "๐Ÿ™", "ascii": "gh" } + } + ] +} +``` + +Two entries from the reference set: a drawn icon and a brand logo. + +## The rules + +There are eight, and every one of them degrades rather than fails. + +**1. A set is a folder with `openicon.json` at its root.** Every path is relative to that file. Top-level keys a reader should understand: `openicon` (the spec version, required), `name` (required), `version`, `license` (SPDX, covering every icon without its own), `homepage`, `grid`, `sizes`, `sprite` and `icons` (required). Unknown keys are kept and ignored. + +**2. An icon's key is its name, in kebab case.** Lowercase letters and digits joined by single hyphens: `mail`, `git-pull-request`, `arrow-up-right`. The key is also the file name. A brand's key is the brand's own name in the same form (`github`, `stack-overflow`), never a product code. + +**3. Aliases find one icon; keywords find many.** `aliases` are other names people type for the icon (`email` for `mail`, `trash` for `delete`), and every key and alias in a set is unique, so `icon('email')` is never ambiguous. `keywords` are search terms and may be shared (`money` finds `wallet`, `dollar` and `coins`). A reader resolves a name against keys first, then aliases. + +**4. Files are monochrome and take the text colour.** An SVG uses `currentColor` for every stroke and fill and a `0 0 24 24` view box. `grid` states the drawing rules the set keeps (`size`, `stroke` width, `padding`), so a reader mixing sets can scale one to match another. `png` may contain `{size}`, filled from `sizes`; PNGs are rendered in one colour, which the set's README states. A set without PNGs or without a sprite is still a set. + +**5. Every icon has terminal glyphs.** `tui.unicode` is one character or emoji that says the same thing, and `tui.ascii` is 1 to 4 printable ASCII characters, spaces only inside (`[ ]`, `>_`, `@`). `tui.nerd` is the Nerd Font character when there is one, with `nerd_code` (hex) and `nerd_name` (the Nerd Fonts glyph name, without `nf-`), so a reader can check it against the Nerd Fonts release it has. An icon with no Nerd Font glyph leaves the three `nerd` fields out. + +**6. A terminal picks the best glyph it can draw.** Nerd first, then Unicode, then ASCII. A Nerd Font cannot be detected from inside a terminal, so a reader takes it from configuration: `OPENICON_GLYPHS=nerd|unicode|ascii` wins, `NERD_FONT=1` means nerd, a UTF-8 locale means unicode, anything else means ascii. A reader that reserves a fixed cell width pads to it, because an emoji may be two columns wide where a Nerd glyph is one. + +**7. A brand says it is one.** A logo carries `brand: true`, a `trademark` note, its `source` (`simple-icons:`, `font-awesome:`, or a URL) and its own `license`, because the licence of a drawing is not permission to use a mark. Brand logos are taken from their owners or from a set that publishes them; a set does not redraw them. + +**8. The set says what made it.** `made_by` is `human`, `ai` or `both`, the [OpenWebring](/docs/openwebring) vocabulary, and `disclosure`, `ai_model`, `ai_provider` and `ai_prompt_url` are the W3C AI Content Disclosure vocabulary, as in [OpenEmoji](/docs/openemoji). Any of them may be repeated on an icon that differs from the set: in the reference set the drawn icons are `ai` and every brand logo is `human`. + +## Discovery + +- A `` in a page's head: this page draws its icons from that set. +- `/.well-known/openicon.json` on a site: a set, or a list of sets, `{"openicon": "0.1", "sets": [{"name": "โ€ฆ", "url": "โ€ฆ/openicon.json"}]}`. +- A package: a set published to npm, PyPI or a git repository keeps `openicon.json` at the package root. Served as JSON with CORS open. + +## Mapping from what exists + +| OpenIcon | Lucide | Tabler | Font Awesome | Nerd Fonts | Simple Icons | +|---|---|---|---|---|---| +| `key` | icon name | icon name | icon name | glyph name after the prefix | slug | +| `aliases` | `aliases` in the icon's JSON | none | `aliases.names` | none | `aliases.aka` | +| `keywords` | `tags` | `tags` | `search.terms` | none | none | +| `category` | `categories` | `category` | `categories` | the font the glyph came from | none | +| `grid` | 24, stroke 2 | 24, stroke 2 | 512 tall, filled | a font cell | 24, filled | +| `tui.nerd` | none | none | the codepoint, in a patched font | the codepoint | none | +| `brand` | none (no brands) | a few, unmarked | the `brands` style | some, unmarked | every icon | +| `license` per icon | none | none | per style | none | one for all (CC0) | + +An importer from any of these is a rename and a copy. Only `tui.unicode` and `tui.ascii` need a person or a model to write them, because none of the sets carries them. + +## What is deliberately absent + +- **No colour.** Icons are monochrome and take the text colour. A brand's own colour is the brand's business; a set may add `hex` to a brand entry and a reader may ignore it. +- **No style variants.** Outline, filled and duotone versions of one icon are three sets, or three icons with three keys. One key, one drawing. +- **No icon font format.** A webfont is a build output a set may ship, not part of the spec; the SVGs and the `tui` glyphs are the interchange. +- **No registry of names.** Keys are the set's own. Aliases are how one set answers to another's names. +- **No animation.** A spinner is a component, not an icon. + +## Reference implementation + +The reference set is [profullstack/openicon](https://github.com/profullstack/openicon): 370 icons, 259 drawn on a 24x24 grid with 2px strokes and 111 brand logos from Simple Icons and Font Awesome Free. 357 of them have a Nerd Font glyph. It is built by `icon` in [profullstack/cli-tools](https://github.com/profullstack/cli-tools): + +```sh +icon build --out ./openicon # openicon.json, svg/, png/, sprite.svg +icon glyph mail # ๓ฐ‡ฐ, โœ‰ or @, whichever this terminal can draw +``` + +[hqtui](https://hqtui.com) ships the set as its default icon pack, so `icon('mail')` in a TUI draws the best glyph the terminal has. + +Related: [OpenEmoji](/docs/openemoji) for the sibling format; [OpenWebring](/docs/openwebring) for `made_by`.