feat(collections): map Outline's named icon set to SF Symbols
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.
This commit is contained in:
@@ -10,6 +10,9 @@ struct CollectionRowView: View {
|
|||||||
} icon: {
|
} icon: {
|
||||||
if let emoji = collection.emojiIcon {
|
if let emoji = collection.emojiIcon {
|
||||||
Text(emoji)
|
Text(emoji)
|
||||||
|
} else if let symbolName = collection.icon.flatMap(OutlineIconMapping.sfSymbolName) {
|
||||||
|
Image(systemName: symbolName)
|
||||||
|
.foregroundStyle(tintColor)
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "folder.fill")
|
Image(systemName: "folder.fill")
|
||||||
.foregroundStyle(tintColor)
|
.foregroundStyle(tintColor)
|
||||||
@@ -23,9 +26,9 @@ struct CollectionRowView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension OutlineCollection {
|
private extension OutlineCollection {
|
||||||
/// Outline's `icon` field holds either a literal emoji (when a user picked one)
|
/// Outline's `icon` field holds either a literal emoji, or a string key
|
||||||
/// or an internal icon-set name (e.g. from their built-in icon picker) — only
|
/// into Outline's own icon set (see OutlineIconMapping) — only the former
|
||||||
/// the former is renderable here without a name-to-glyph mapping table.
|
/// is renderable directly, the latter needs the lookup table.
|
||||||
var emojiIcon: String? {
|
var emojiIcon: String? {
|
||||||
// `isEmojiPresentation` misses symbol-class emoji that default to text
|
// `isEmojiPresentation` misses symbol-class emoji that default to text
|
||||||
// presentation (e.g. some picked without a variation selector) — `isEmoji`
|
// presentation (e.g. some picked without a variation selector) — `isEmoji`
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user