From 2c2f86f79bca6d8e63088940ae9b93ffdc9a77b7 Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Fri, 14 Aug 2026 01:42:48 +0100 Subject: [PATCH] feat(collections): map Outline's named icon set to SF Symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collection.icon can be a literal emoji or a string key into Outline's own icon library (~50 custom outline-icons keys + ~90 FontAwesome keys, per shared/utils/IconLibrary.tsx in outline/outline — a different mapping than the outline-icons package itself, which is keyed by PascalCase component name, not the string stored on the collection). Only emoji was handled before, so most non-emoji collections fell back to a generic folder glyph. Covers the high-confidence subset with an unambiguous SF Symbol match; unmapped keys keep falling back to the folder rather than risk an invalid symbol name rendering blank. --- .../Collections/CollectionRowView.swift | 9 +- Outpost/Support/OutlineIconMapping.swift | 96 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 Outpost/Support/OutlineIconMapping.swift diff --git a/Outpost/Features/Collections/CollectionRowView.swift b/Outpost/Features/Collections/CollectionRowView.swift index ee5b693..913b9e0 100644 --- a/Outpost/Features/Collections/CollectionRowView.swift +++ b/Outpost/Features/Collections/CollectionRowView.swift @@ -10,6 +10,9 @@ struct CollectionRowView: View { } icon: { if let emoji = collection.emojiIcon { Text(emoji) + } else if let symbolName = collection.icon.flatMap(OutlineIconMapping.sfSymbolName) { + Image(systemName: symbolName) + .foregroundStyle(tintColor) } else { Image(systemName: "folder.fill") .foregroundStyle(tintColor) @@ -23,9 +26,9 @@ struct CollectionRowView: View { } private extension OutlineCollection { - /// Outline's `icon` field holds either a literal emoji (when a user picked one) - /// or an internal icon-set name (e.g. from their built-in icon picker) — only - /// the former is renderable here without a name-to-glyph mapping table. + /// Outline's `icon` field holds either a literal emoji, or a string key + /// into Outline's own icon set (see OutlineIconMapping) — only the former + /// is renderable directly, the latter needs the lookup table. var emojiIcon: String? { // `isEmojiPresentation` misses symbol-class emoji that default to text // presentation (e.g. some picked without a variation selector) — `isEmoji` diff --git a/Outpost/Support/OutlineIconMapping.swift b/Outpost/Support/OutlineIconMapping.swift new file mode 100644 index 0000000..41ec8c5 --- /dev/null +++ b/Outpost/Support/OutlineIconMapping.swift @@ -0,0 +1,96 @@ +import Foundation + +/// Best-effort SF Symbol equivalents for Outline's non-emoji collection icons. +/// +/// `Collection.icon` is either a literal emoji or a string key into Outline's +/// own icon set — a mix of custom `outline-icons` components (camelCase keys) +/// and FontAwesome icons (kebab-case keys, e.g. `faCake` → `"cake"`). The +/// canonical key list lives in `outline/outline`'s `shared/utils/IconLibrary.tsx`, +/// not in the `outline-icons` package itself (that's just the raw component +/// library, keyed by PascalCase component name — a different naming scheme). +/// +/// SF Symbols has no exact match for everything in that set, so this only +/// covers keys with a confident, unambiguous equivalent. Anything else falls +/// back to the generic folder glyph — better than a blank/invalid symbol name +/// from a wrong guess. +enum OutlineIconMapping { + static func sfSymbolName(for outlineIconKey: String) -> String? { + mapping[outlineIconKey] + } + + private static let mapping: [String: String] = [ + // Outline's own custom icon set (camelCase keys). + "academicCap": "graduationcap.fill", + "bicycle": "bicycle", + "beaker": "testtube.2", + "buildingBlocks": "cube.fill", + "bookmark": "bookmark.fill", + "browser": "globe", + "collection": "square.grid.2x2.fill", + "coins": "dollarsign.circle.fill", + "camera": "camera.fill", + "carrot": "leaf.fill", + "clock": "clock.fill", + "cloud": "cloud.fill", + "code": "chevron.left.forwardslash.chevron.right", + "database": "cylinder.fill", + "done": "checkmark.circle.fill", + "email": "envelope.fill", + "eye": "eye.fill", + "feedback": "bubble.left.and.bubble.right.fill", + "flame": "flame.fill", + "graph": "chart.line.uptrend.xyaxis", + "globe": "globe", + "hashtag": "number", + "info": "info.circle.fill", + "image": "photo.fill", + "internet": "network", + "leaf": "leaf.fill", + "library": "books.vertical.fill", + "lightbulb": "lightbulb.fill", + "lightning": "bolt.fill", + "letter": "envelope.fill", + "math": "function", + "moon": "moon.fill", + "notepad": "note.text", + "padlock": "lock.fill", + "palette": "paintpalette.fill", + "pencil": "pencil", + "plane": "airplane", + "promote": "megaphone.fill", + "ramen": "fork.knife", + "question": "questionmark.circle.fill", + "server": "server.rack", + "sun": "sun.max.fill", + "shapes": "square.on.circle", + "sport": "sportscourt.fill", + "smiley": "face.smiling.fill", + "target": "target", + "team": "person.3.fill", + "terminal": "terminal.fill", + "thumbsup": "hand.thumbsup.fill", + "truck": "shippingbox.fill", + "tools": "wrench.and.screwdriver.fill", + "vehicle": "car.fill", + "warning": "exclamationmark.triangle.fill", + + // FontAwesome icons (kebab-case keys) — high-confidence subset only. + "book": "book.fill", + "cake": "birthday.cake.fill", + "cat": "cat.fill", + "dog": "dog.fill", + "cube": "cube.fill", + "heart": "heart.fill", + "gift": "gift.fill", + "hammer": "hammer.fill", + "laptop": "laptopcomputer", + "map": "map.fill", + "newspaper": "newspaper.fill", + "paw": "pawprint.fill", + "shield": "shield.fill", + "tree": "tree.fill", + "trophy": "trophy.fill", + "umbrella": "umbrella.fill", + "fish": "fish.fill" + ] +}