diff --git a/.gitignore b/.gitignore index 397ba4a..26f7e1b 100644 --- a/.gitignore +++ b/.gitignore @@ -87,5 +87,6 @@ CHANGES.md # Local scratch: DerivedData clones, stress logs, QA frames, AI notes, audits .tmp/ +.ai-skills-layout-shots/ .workbuddy-ai/ reports/ diff --git a/AGENTS.md b/AGENTS.md index b9db30d..45a767c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ writing commit messages that will ship to users. ### Version format -The current source-of-truth version is **1.7.5 (build 67)**. Releases use stable SemVer: +The current source-of-truth version is **1.7.5 (build 69)**. Releases use stable SemVer: | Field | File | Rule | |-------|------|------| diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edcc90..902cb69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Skills tab**: Home dock and iPad sidebar add Skills between Home and Styles; cards list default Reply / Summarize / Translate plus installable export skills (max 8 enabled, long-press drag to reorder). / **技能 Tab**:首页 Dock 与 iPad 侧栏在「首页」和「风格」之间新增「技能」;卡片列出默认的回复 / 总结 / 翻译以及可安装的出口技能(最多启用 8 个,长按拖动排序)。 - **Extract tasks skill**: the Skills tab opens a ready-made companion Shortcut named `OSG · 提取待办` on the system Add page (split lines → Reminders, default list). After you tap Add, copying text and tapping Tasks asks the LLM for to-dos and silently adds them; no tasks stays in the current app with a keyboard tip. / **提取待办技能**:技能页会打开已做好的配套捷径 `OSG · 提取待办` 的系统添加页(按行拆分并写入默认提醒清单)。点添加后,复制文字并点「待办」会让模型抽取待办并静默写入;没有待办则留在当前 App,键盘上给出提示。 +- **Extract events skill**: the Skills tab opens a ready-made companion Shortcut named `OSG · 提取日程` on the system Add page (multiple events into Calendar; date-only → all-day, time-only → today, optional end time and location). After you tap Add, copying text and tapping Events asks the LLM for events and silently adds them; no date or time stays in the current app with a keyboard tip. / **提取日程技能**:技能页会打开已做好的配套捷径 `OSG · 提取日程` 的系统添加页(可多条写入日历;只有日期为全天;只有时刻用当天;可带结束时间与地点)。点添加后,复制文字并点「日程」会让模型抽取日程并静默写入;没有日期或时间则留在当前 App,键盘上给出提示。 +- **Navigate skill**: the Skills tab opens a companion Shortcut named `OSG · 导航`. After you add it, copying text and tapping Navigate asks the LLM for one address (or origin → destination) and opens driving directions — Amap if installed, then Baidu Maps, then Apple Maps. No address stays in the current app with a keyboard tip. / **导航技能**:技能页打开配套捷径 `OSG · 导航`。添加后,复制文字并点「导航」会抽取一条地址(或起点→终点)并开始驾车导航——已装高德则用高德,否则百度,再否则 Apple 地图。没有地址则留在当前 App,键盘上给出提示。 +- **Skills clipboard guide**: when Clipboard History is off, the Skills tab shows a card that jumps to in-app Clipboard settings and to iOS Settings for paste authorization. / **技能页剪贴板指引**:未开启剪贴板历史时,技能页展示可点击卡片,分别跳转 App 内剪贴板设置和系统设置以完成粘贴授权。 +- **Custom skills**: Skills tab `+` adds a user skill (name, about, SF Symbol, prompt, required iCloud Shortcut link with name lookup, independent Shortcut name, thinking off by default). Built-in thinking stays off and disabled. No cap on how many custom skills you can save; the keyboard still holds at most 8. / **自定义技能**:技能页右上角 `+` 可添加用户技能(名称、介绍、SF Symbol、提示词、必填 iCloud 捷径链接并自动读取名称、可与技能名分开的捷径名、思考默认关)。内置技能思考固定关闭且不可开。自定义数量不设上限,键盘仍最多启用 8 个。 ### Fixed - **Extract tasks Shortcut**: receive Shortcut Input as Text, then split lines and add each title to Reminders — the previous recipe could finish successfully without creating items. / **提取待办捷径**:先把快捷指令输入收成文本,再按行写入提醒;旧配方会成功跑完但不创建条目。 +- **Skill reorder feedback**: long-press lifts a skill card and the grid slides live under the finger, matching Home Screen rearrange. / **技能拖动排序**:长按拎起技能卡片,网格随手指实时让位,接近主屏幕图标重排。 +- **Skill drag preview corners**: the lift preview clips to the card’s continuous rounded rect so square white corners no longer show. / **技能拖动圆角**:拖起预览按卡片连续圆角裁剪,去掉圆角外的直角白底。 ### Changed +- **Skill and style cards**: every card uses a top-right edit pencil and a bottom-right selected check; skill Done sits on the right like style details; tapping a skill card toggles the keyboard slot (unconfirmed Shortcut skills still open the install sheet). / **技能与风格卡片**:右上为编辑铅笔、右下为选中勾选;技能详情「完成」改到右侧;点技能卡片切换是否上键盘(未确认捷径的仍打开安装页)。 +- **Clipboard skill grid**: idle skill chips stay as one centered block, wrapping to two rows of up to four instead of scrolling sideways after four. / **剪贴板技能网格**:空闲技能按钮作为一整块居中,超过四个改为两行(每行最多四个),不再横向滚动。 +- **Skill sheet buttons**: Add, Turn off, Add Shortcut, I’ve added it, and Reinstall are solid capsules — primary actions use the brand green (`#3AA05A`), secondary actions use an elevated fill. / **技能弹窗按钮**:添加、关闭、添加捷径、我已添加、重新安装改为实心胶囊;主操作为品牌绿(`#3AA05A`),次要为抬升底。 - **AI idle hint keywords**: chips show the entity (from feed `metadata.title` / city / holiday name) instead of a long sentence; category prefixes like「全网热点:」are dropped, and LLM compression is only a last resort. / **AI 空闲建议关键词**:芯片展示实体(来自 feed 的 `metadata.title` / 城市 / 节日名)而不再是长句;去掉「全网热点:」一类前缀,LLM 压缩仅作兜底。 - **AI idle hint chrome**: each rotating suggestion sits in a Liquid Glass capsule with a category SF Symbol (calendar, weather, news, stocks, trending, search). / **AI 空闲建议样式**:轮播建议放入 Liquid Glass 胶囊,左侧为类型 SF Symbol(日历、天气、新闻、股票、热搜、搜索)。 - **Clipboard AI skills**: within 30 seconds of a copy, AI idle shows compact circular Reply / Summarize / Translate buttons; translate follows the keyboard language setting, or Chinese ↔ English when unset. The skill list is catalog-based so more actions can be added later. / **剪贴板 AI 技能**:复制后约 30 秒内,空闲态改为小圆形「回复 / 总结 / 翻译」按钮;翻译跟随键盘目标语言,未设置时中英互译。技能来自目录,便于日后扩展。 diff --git a/OSGKeyboard/Info.plist b/OSGKeyboard/Info.plist index d06719a..78992f7 100644 --- a/OSGKeyboard/Info.plist +++ b/OSGKeyboard/Info.plist @@ -70,6 +70,10 @@ skype zoomus shortcuts + iosamap + amapuri + baidumap + maps things todoist evernote diff --git a/OSGKeyboard/OSGKeyboardApp.swift b/OSGKeyboard/OSGKeyboardApp.swift index 3021bfb..e9f8fb3 100644 --- a/OSGKeyboard/OSGKeyboardApp.swift +++ b/OSGKeyboard/OSGKeyboardApp.swift @@ -36,6 +36,8 @@ struct OSGKeyboardApp: App { EditDemoView() } else if ProcessInfo.processInfo.arguments.contains("--ai-demo") { AIKeyboardDemoView() + } else if ProcessInfo.processInfo.arguments.contains("--ai-skills-demo") { + AIClipboardSkillLayoutDemoView() } else if ProcessInfo.processInfo.arguments.contains("--clipboard-demo") { ClipboardHistoryDemoView() } else if ProcessInfo.processInfo.arguments.contains("--edit-pager-ui-test") { diff --git a/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.shortcut b/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.shortcut new file mode 100644 index 0000000..e1f601f Binary files /dev/null and b/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.shortcut differ diff --git a/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.unsigned.shortcut b/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.unsigned.shortcut new file mode 100644 index 0000000..a467a09 Binary files /dev/null and b/OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.unsigned.shortcut differ diff --git a/OSGKeyboard/Resources/Shortcuts/OSGNavigate.shortcut b/OSGKeyboard/Resources/Shortcuts/OSGNavigate.shortcut new file mode 100644 index 0000000..75d2304 Binary files /dev/null and b/OSGKeyboard/Resources/Shortcuts/OSGNavigate.shortcut differ diff --git a/OSGKeyboard/Resources/Shortcuts/OSGNavigate.unsigned.shortcut b/OSGKeyboard/Resources/Shortcuts/OSGNavigate.unsigned.shortcut new file mode 100644 index 0000000..4a2f8a8 Binary files /dev/null and b/OSGKeyboard/Resources/Shortcuts/OSGNavigate.unsigned.shortcut differ diff --git a/OSGKeyboard/Services/AIAgentShortcutInstaller.swift b/OSGKeyboard/Services/AIAgentShortcutInstaller.swift index 8d973a5..ddbdea3 100644 --- a/OSGKeyboard/Services/AIAgentShortcutInstaller.swift +++ b/OSGKeyboard/Services/AIAgentShortcutInstaller.swift @@ -10,8 +10,6 @@ import UIKit import OSGKeyboardShared enum AIAgentShortcutInstaller { - static let bundledResourceName = "OSGExtractTodos" - @MainActor static func openInstallPage(for skill: AIClipboardSkill) { if let shareURL = skill.shortcutICloudURL, @@ -19,17 +17,19 @@ enum AIAgentShortcutInstaller { UIApplication.shared.open(installURL) return } - openBundledShortcut() + openBundledShortcut(for: skill) } @MainActor - private static func openBundledShortcut() { - guard let bundled = Bundle.main.url( - forResource: bundledResourceName, - withExtension: "shortcut" - ) else { return } + private static func openBundledShortcut(for skill: AIClipboardSkill) { + guard let resource = skill.shortcutResourceName, + let bundled = Bundle.main.url( + forResource: resource, + withExtension: "shortcut" + ) else { return } + let fileName = skill.shortcutName ?? resource let tmp = FileManager.default.temporaryDirectory - .appendingPathComponent("\(AIClipboardSkillCatalog.extractTodosShortcutName).shortcut") + .appendingPathComponent("\(fileName).shortcut") try? FileManager.default.removeItem(at: tmp) do { try FileManager.default.copyItem(at: bundled, to: tmp) diff --git a/OSGKeyboard/Services/AIAgentShortcutRunner.swift b/OSGKeyboard/Services/AIAgentShortcutRunner.swift index 95d61f2..5d64b06 100644 --- a/OSGKeyboard/Services/AIAgentShortcutRunner.swift +++ b/OSGKeyboard/Services/AIAgentShortcutRunner.swift @@ -1,7 +1,7 @@ // AIAgentShortcutRunner.swift // OSGKeyboard · Main App // -// Consumes the keyboard's pending extract-todos payload and opens the +// Consumes the keyboard's pending export-skill payload and opens the // companion Shortcut. Release builds stay in Shortcuts. DEBUG builds add // x-callback URLs so Console can record success / error / cancel. @@ -13,15 +13,22 @@ enum AIAgentShortcutRunner { static func runPendingIfNeeded() { AIAgentShortcutRun.trace("host.runPending begin") guard let payload = AppGroupStore().consumePendingShortcutRun() else { return } - guard let skill = AIClipboardSkillCatalog.skill(id: payload.skillID), + let catalog = AppGroupStore().agentUserSkillCatalog + guard let skill = AIClipboardSkillCatalog.skill(id: payload.skillID, userCatalog: catalog), let name = skill.shortcutName else { AIAgentShortcutRun.trace( "host.runPending skip unknownSkill=\(payload.skillID)" ) return } - AIAgentShortcutRun.traceBody("host.titlesToShortcut", payload.joinedTitles) - guard let url = shortcutsURL(name: name, text: payload.joinedTitles) else { + guard let text = shortcutText(for: skill, payload: payload) else { + AIAgentShortcutRun.trace( + "host.runPending skip textBuildFailed skill=\(payload.skillID)" + ) + return + } + AIAgentShortcutRun.traceBody("host.titlesToShortcut", text) + guard let url = shortcutsURL(name: name, text: text) else { AIAgentShortcutRun.trace("host.runPending skip URLBuildFailed name=\(name)") return } @@ -55,6 +62,21 @@ enum AIAgentShortcutRunner { ) } + /// Navigate: pick 高德 → 百度 → Apple Maps, pass that URL to the Shortcut. + /// Other export skills send the parsed lines unchanged. + private static func shortcutText( + for skill: AIClipboardSkill, + payload: AIAgentShortcutRunPayload + ) -> String? { + if skill.id == AIClipboardSkillCatalog.navigateID { + return AIMapNavigation.shortcutInput( + from: payload.joinedTitles, + canOpen: { UIApplication.shared.canOpenURL($0) } + ) + } + return payload.joinedTitles + } + private static func shortcutsURL(name: String, text: String) -> URL? { #if DEBUG return AIAgentShortcutRun.shortcutsRunURL( diff --git a/OSGKeyboard/Services/FlowSessionManager.swift b/OSGKeyboard/Services/FlowSessionManager.swift index 8a0eca9..349cafc 100644 --- a/OSGKeyboard/Services/FlowSessionManager.swift +++ b/OSGKeyboard/Services/FlowSessionManager.swift @@ -1358,13 +1358,15 @@ final class FlowSessionManager: ObservableObject { let utteranceId = command.utteranceId let commandSeq = command.commandSeq let sessionId = command.sessionId + let thinkingEnabled = command.aiThinkingEnabled ?? true Task { @MainActor [weak self] in await self?.answerPrefilledAIQuestion( question: question, conversationID: conversationID, sessionId: sessionId, utteranceId: utteranceId, - commandSeq: commandSeq + commandSeq: commandSeq, + thinkingEnabled: thinkingEnabled ) } } @@ -1385,7 +1387,8 @@ final class FlowSessionManager: ObservableObject { conversationID: UUID?, sessionId: UUID, utteranceId: UUID, - commandSeq: Int64 + commandSeq: Int64, + thinkingEnabled: Bool ) async { // Hint-card / prefilled questions never go through `finalizeUtterance`, // so this path must drop the processing gate itself. Leaving it set @@ -1415,7 +1418,8 @@ final class FlowSessionManager: ObservableObject { do { let service = try AIQuestionService.configured( store: pipelineStore, - conversations: aiConversations + conversations: aiConversations, + thinkingEnabled: thinkingEnabled ) aiAnswerStreamThrottle = AIAnswerStreamThrottle() let answer = try await service.answer( diff --git a/OSGKeyboard/Views/AIAgentSkillsView.swift b/OSGKeyboard/Views/AIAgentSkillsView.swift index 72884fb..1ee1f59 100644 --- a/OSGKeyboard/Views/AIAgentSkillsView.swift +++ b/OSGKeyboard/Views/AIAgentSkillsView.swift @@ -2,8 +2,9 @@ // OSGKeyboard · Main App // // Catalog of AI Agent clipboard skills. Enabled cards (max 8) appear on -// the keyboard after a copy; long-press drag reorders that row. Export -// skills confirm a companion Shortcut before they can occupy a slot. +// the keyboard after a copy; long-press drag rearranges that row live, +// like Home Screen icons. Export skills confirm a companion Shortcut +// before they can occupy a slot. Users can add custom export skills. import SwiftUI import UIKit @@ -15,13 +16,21 @@ struct AIAgentSkillsView: View { @ObservedObject private var store = AIAgentSkillLayoutStore.shared @State private var viewingSkill: AIClipboardSkill? + @State private var editingDraft: SkillEditorDraft? @State private var showFullAlert = false + @State private var showClipboardSettings = false + /// True while a skill card is lifted; locks the page scroll like SpringBoard. + @State private var isReordering = false private let columns = [ GridItem(.flexible(), spacing: Spacing.sm), GridItem(.flexible(), spacing: Spacing.sm), ] + private var skillCardShape: RoundedRectangle { + RoundedRectangle(cornerRadius: Radius.xl, style: .continuous) + } + var body: some View { NavigationStack { ScrollView { @@ -36,9 +45,23 @@ struct AIAgentSkillsView: View { } .tabBarScrollBottomPadding() } + .scrollDisabled(isReordering) .background(palette.background) .navigationTitle("skills.title") .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + editingDraft = .blank() + } label: { + Image(systemName: "plus") + } + .accessibilityLabel(Text("skills.add")) + } + } + .navigationDestination(isPresented: $showClipboardSettings) { + ClipboardSettingsView(config: config) + } } .sheet(item: $viewingSkill) { skill in SkillDetailSheet( @@ -49,6 +72,16 @@ struct AIAgentSkillsView: View { onConfirmInstall: confirmShortcutInstall ) } + .sheet(item: $editingDraft) { draft in + SkillEditorSheet( + draft: draft, + onSave: { try saveDraft($0) }, + onDelete: { id in + store.deleteUserSkill(id: id) + editingDraft = nil + } + ) + } .alert( AppL10n.string("skills.full.title", language: config.uiLanguage), isPresented: $showFullAlert @@ -61,16 +94,71 @@ struct AIAgentSkillsView: View { } private var clipboardHistoryBanner: some View { - Text("skills.clipboardHistory.banner") - .font(TypeStyle.caption) - .foregroundStyle(palette.textSecondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(Spacing.md) - .background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: Radius.xl, style: .continuous) - .stroke(palette.divider, lineWidth: 0.5) - ) + VStack(alignment: .leading, spacing: Spacing.sm) { + Label { + Text("skills.clipboard.guide.title") + .font(TypeStyle.bodyEmph) + .foregroundStyle(palette.textPrimary) + } icon: { + Image(systemName: "clipboard") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(palette.warning) + } + Text("skills.clipboard.guide.body") + .font(TypeStyle.caption) + .foregroundStyle(palette.textSecondary) + .fixedSize(horizontal: false, vertical: true) + + Button { + showClipboardSettings = true + } label: { + guideRow( + titleKey: "skills.clipboard.guide.openAppSettings", + systemImage: "slider.horizontal.3", + trailing: "chevron.right" + ) + } + .buttonStyle(.plain) + + Divider().background(palette.divider) + + Button { + AppPermissions.openSystemSettings() + } label: { + guideRow( + titleKey: "skills.clipboard.guide.openSystemSettings", + systemImage: "gearshape", + trailing: "arrow.up.right" + ) + } + .buttonStyle(.plain) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(Spacing.md) + .background(palette.surface, in: skillCardShape) + .overlay(skillCardShape.stroke(palette.divider, lineWidth: 0.5)) + } + + private func guideRow( + titleKey: LocalizedStringKey, + systemImage: String, + trailing: String + ) -> some View { + HStack(spacing: Spacing.sm) { + Image(systemName: systemImage) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(palette.accent) + .frame(width: 22) + Text(titleKey) + .font(TypeStyle.body) + .foregroundStyle(palette.textPrimary) + Spacer(minLength: 0) + Image(systemName: trailing) + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(palette.textTertiary) + } + .padding(.vertical, Spacing.xs) + .contentShape(Rectangle()) } private var enabledSection: some View { @@ -88,21 +176,18 @@ struct AIAgentSkillsView: View { .foregroundStyle(palette.textTertiary) .frame(maxWidth: .infinity, alignment: .leading) } else { - LazyVGrid(columns: columns, spacing: Spacing.sm) { - ForEach(store.enabledSkills) { skill in - skillCard(skill, showsEnabledBadge: true) - .onTapGesture { viewingSkill = skill } - .draggable(skill.id) { - skillCard(skill, showsEnabledBadge: true) - .frame(width: 160) - } - .dropDestination(for: String.self) { items, _ in - guard let dragged = items.first else { return false } - store.moveEnabled(id: dragged, onto: skill.id) - return true - } + EnabledSkillsReorderGrid( + skills: store.enabledSkills, + isReordering: $isReordering, + onTap: handleCardTap, + onEdit: handleEdit, + onMoveToIndex: { id, index in + store.moveEnabled(id: id, toIndex: index) + }, + card: { skill in + skillCardVisual(skill, isEnabled: true) } - } + ) } } } @@ -111,55 +196,119 @@ struct AIAgentSkillsView: View { CardSection("skills.available.section") { LazyVGrid(columns: columns, spacing: Spacing.sm) { ForEach(store.availableSkills) { skill in - Button { - viewingSkill = skill - } label: { - skillCard(skill, showsEnabledBadge: false) - } - .buttonStyle(.plain) + skillCardInteractive(skill, isEnabled: false) } } } } - private func skillCard(_ skill: AIClipboardSkill, showsEnabledBadge: Bool) -> some View { - VStack(alignment: .leading, spacing: Spacing.sm) { - HStack(spacing: Spacing.sm) { + private func skillCardInteractive(_ skill: AIClipboardSkill, isEnabled: Bool) -> some View { + ZStack(alignment: .topTrailing) { + Button { + handleCardTap(skill) + } label: { + skillCardVisual(skill, isEnabled: isEnabled) + } + .buttonStyle(.plain) + + Button { + handleEdit(skill) + } label: { + Color.clear + .frame( + width: CatalogCardChrome.editHitSize, + height: CatalogCardChrome.editHitSize + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(Text("skills.edit")) + } + .contextMenu { + if skill.isUserCreated { + Button("common.delete", role: .destructive) { + store.deleteUserSkill(id: skill.id) + } + } + } + } + + private func skillCardVisual(_ skill: AIClipboardSkill, isEnabled: Bool) -> some View { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: Spacing.sm) { Image(systemName: skill.systemImage) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(palette.accent) + Text(skillTitle(skill)) + .font(TypeStyle.body) + .foregroundStyle(palette.textPrimary) + .lineLimit(1) + .padding(.trailing, 32) + Text(skillDescription(skill)) + .font(TypeStyle.caption2) + .foregroundStyle(palette.textTertiary) + .lineLimit(2) Spacer(minLength: 0) - if showsEnabledBadge { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 18, weight: .semibold)) - .foregroundStyle(palette.accent) - } } - Text(AppL10n.string(skill.cardTitleKey, language: config.uiLanguage)) - .font(TypeStyle.body) - .foregroundStyle(palette.textPrimary) - .lineLimit(1) - Text(AppL10n.string(skill.descriptionKey, language: config.uiLanguage)) - .font(TypeStyle.caption2) - .foregroundStyle(palette.textTertiary) - .lineLimit(2) - Spacer(minLength: 0) + .frame(maxWidth: .infinity, minHeight: 96, alignment: .leading) + .padding(Spacing.md) + + CatalogCardChrome.editIcon(palette: palette) + .padding(Spacing.sm) + .allowsHitTesting(false) + + if isEnabled { + CatalogCardChrome.checkIcon(palette: palette) + } } - .frame(maxWidth: .infinity, minHeight: 96, alignment: .leading) - .padding(Spacing.md) .background( - showsEnabledBadge ? palette.accentMuted : palette.surface, - in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous) + isEnabled ? palette.accentMuted : palette.surface, + in: skillCardShape ) - .overlay( - RoundedRectangle(cornerRadius: Radius.xl, style: .continuous) - .stroke( - showsEnabledBadge ? palette.accent : palette.divider, - lineWidth: showsEnabledBadge ? 1.5 : 0.5 - ) - ) - .clipShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)) - .contentShape(RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)) + .overlay(skillCardShape.stroke( + isEnabled ? palette.accent : palette.divider, + lineWidth: isEnabled ? 1.5 : 0.5 + )) + .clipShape(skillCardShape) + .contentShape(skillCardShape) + .containerShape(skillCardShape) + } + + private func skillTitle(_ skill: AIClipboardSkill) -> String { + if let name = skill.customName?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty { + return name + } + return AppL10n.string(skill.cardTitleKey, language: config.uiLanguage) + } + + private func skillDescription(_ skill: AIClipboardSkill) -> String { + if skill.isUserCreated { + let summary = skill.customSummary?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !summary.isEmpty { return summary } + return AppL10n.string("skills.custom.description", language: config.uiLanguage) + } + return AppL10n.string(skill.descriptionKey, language: config.uiLanguage) + } + + private func handleCardTap(_ skill: AIClipboardSkill) { + if skill.requiresShortcut, !store.layout.hasConfirmedShortcut(skill.id) { + viewingSkill = skill + return + } + if store.layout.isEnabled(skill.id) { + store.disable(skill.id) + } else { + addOrWarn(skill) + } + } + + private func handleEdit(_ skill: AIClipboardSkill) { + if skill.isUserCreated, let user = store.userSkill(id: skill.id) { + editingDraft = .from(user) + } else { + viewingSkill = skill + } } private func addOrWarn(_ skill: AIClipboardSkill) { @@ -183,6 +332,244 @@ struct AIAgentSkillsView: View { break } } + + private func saveDraft(_ draft: SkillEditorDraft) throws { + guard let url = AIShortcutShareLink.parse(draft.shortcutLink) else { + throw AIUserSkillValidationError.invalidShortcutLink + } + let skill = AIUserSkill( + id: draft.id, + name: draft.name, + summary: draft.summary, + systemImage: draft.systemImage, + prompt: draft.prompt, + shortcutICloudURL: url, + shortcutName: draft.shortcutName, + thinkingEnabled: draft.thinkingEnabled + ) + try store.saveUserSkill(skill) + editingDraft = nil + } +} + +/// Home-screen style reorder: long-press lifts the card, other cells slide +/// into the gap as the finger crosses midpoints, drop settles in place. +private struct EnabledSkillsReorderGrid: View { + let skills: [AIClipboardSkill] + @Binding var isReordering: Bool + let onTap: (AIClipboardSkill) -> Void + let onEdit: (AIClipboardSkill) -> Void + let onMoveToIndex: (String, Int) -> Void + let card: (AIClipboardSkill) -> Card + + @State private var draggingID: String? + @State private var dragTranslation: CGSize = .zero + @State private var originFrame: CGRect = .zero + @State private var cellFrames: [String: CGRect] = [:] + @State private var ignoreTap = false + + private static var spaceName: String { "enabledSkillsGrid" } + private let columns = [ + GridItem(.flexible(), spacing: Spacing.sm), + GridItem(.flexible(), spacing: Spacing.sm), + ] + + var body: some View { + ZStack(alignment: .topLeading) { + LazyVGrid(columns: columns, spacing: Spacing.sm) { + ForEach(skills) { skill in + gridCell(skill) + } + } + .animation( + .interactiveSpring(response: 0.28, dampingFraction: 0.86), + value: skills.map(\.id) + ) + + if let draggingID, let skill = skills.first(where: { $0.id == draggingID }) { + card(skill) + .frame(width: originFrame.width, height: originFrame.height) + .scaleEffect(1.07) + .shadow(color: .black.opacity(0.28), radius: 16, y: 10) + .offset( + x: originFrame.minX + dragTranslation.width, + y: originFrame.minY + dragTranslation.height + ) + .allowsHitTesting(false) + } + } + .coordinateSpace(.named(Self.spaceName)) + .onPreferenceChange(SkillCellFrameKey.self) { cellFrames = $0 } + } + + private func gridCell(_ skill: AIClipboardSkill) -> some View { + ZStack(alignment: .topTrailing) { + Button { + guard draggingID == nil, !ignoreTap else { return } + onTap(skill) + } label: { + card(skill) + .opacity(draggingID == skill.id ? 0 : 1) + } + .buttonStyle(.plain) + + Button { + guard draggingID == nil, !ignoreTap else { return } + onEdit(skill) + } label: { + Color.clear + .frame( + width: CatalogCardChrome.editHitSize, + height: CatalogCardChrome.editHitSize + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(Text("skills.edit")) + .opacity(draggingID == skill.id ? 0 : 1) + } + .background { + GeometryReader { proxy in + Color.clear.preference( + key: SkillCellFrameKey.self, + value: [skill.id: proxy.frame(in: .named(Self.spaceName))] + ) + } + } + .simultaneousGesture(reorderGesture(for: skill)) + } + + private func reorderGesture(for skill: AIClipboardSkill) -> some Gesture { + LongPressGesture(minimumDuration: 0.22) + .sequenced( + before: DragGesture( + minimumDistance: 0, + coordinateSpace: .named(Self.spaceName) + ) + ) + .onChanged { value in + switch value { + case .second(_, let drag): + liftIfNeeded(skill) + guard let drag else { return } + dragTranslation = drag.translation + moveSlotIfNeeded(at: drag.location) + default: + break + } + } + .onEnded { _ in + endDrag() + } + } + + private func liftIfNeeded(_ skill: AIClipboardSkill) { + guard draggingID == nil else { return } + originFrame = cellFrames[skill.id] ?? .zero + draggingID = skill.id + isReordering = true + UIImpactFeedbackGenerator(style: .medium).impactOccurred(intensity: 0.9) + } + + private func moveSlotIfNeeded(at point: CGPoint) { + guard let draggingID else { return } + guard let target = nearestIndex(to: point) else { return } + guard skills[target].id != draggingID else { return } + UISelectionFeedbackGenerator().selectionChanged() + onMoveToIndex(draggingID, target) + } + + private func nearestIndex(to point: CGPoint) -> Int? { + guard !skills.isEmpty else { return nil } + var bestIndex: Int? + var bestDistance = CGFloat.greatestFiniteMagnitude + for (index, skill) in skills.enumerated() { + guard let frame = cellFrames[skill.id] else { continue } + let dx = point.x - frame.midX + let dy = point.y - frame.midY + let distance = dx * dx + dy * dy + if distance < bestDistance { + bestDistance = distance + bestIndex = index + } + } + return bestIndex + } + + private func endDrag() { + let wasDragging = draggingID != nil + if let draggingID, let slot = cellFrames[draggingID], originFrame.width > 0 { + withAnimation(.snappy(duration: 0.2)) { + dragTranslation = CGSize( + width: slot.minX - originFrame.minX, + height: slot.minY - originFrame.minY + ) + } completion: { + clearDragState() + } + } else { + clearDragState() + } + guard wasDragging else { return } + ignoreTap = true + DispatchQueue.main.async { + ignoreTap = false + } + } + + private func clearDragState() { + draggingID = nil + dragTranslation = .zero + isReordering = false + } +} + +private struct SkillCellFrameKey: PreferenceKey { + static let defaultValue: [String: CGRect] = [:] + + static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) { + value.merge(nextValue(), uniquingKeysWith: { _, new in new }) + } +} + +private struct SkillEditorDraft: Identifiable, Equatable { + let id: String + let isNew: Bool + var name: String + var summary: String + var systemImage: String + var prompt: String + var shortcutLink: String + var shortcutName: String + var thinkingEnabled: Bool + + static func blank() -> SkillEditorDraft { + SkillEditorDraft( + id: "user.\(UUID().uuidString.lowercased())", + isNew: true, + name: "", + summary: "", + systemImage: AIUserSkillLimits.defaultSystemImage, + prompt: AIUserSkillLimits.newPromptTemplate, + shortcutLink: "", + shortcutName: "", + thinkingEnabled: false + ) + } + + static func from(_ skill: AIUserSkill) -> SkillEditorDraft { + SkillEditorDraft( + id: skill.id, + isNew: false, + name: skill.name, + summary: skill.summary, + systemImage: skill.systemImage, + prompt: skill.prompt, + shortcutLink: skill.shortcutICloudURL.absoluteString, + shortcutName: skill.shortcutName, + thinkingEnabled: skill.thinkingEnabled + ) + } } private struct SkillDetailSheet: View { @@ -195,61 +582,80 @@ private struct SkillDetailSheet: View { let onAddOrWarn: (AIClipboardSkill) -> Void let onConfirmInstall: (AIClipboardSkill) -> Void - /// Content stack height; nav chrome is added for the detent. - @State private var contentHeight: CGFloat = 240 - private let navigationChrome: CGFloat = 56 + /// Inner stack height; nav chrome + home indicator are added for the detent. + @State private var contentHeight: CGFloat = 280 + private let navigationChrome: CGFloat = 72 var body: some View { NavigationStack { - CardPageContent( - spacing: Spacing.md, - topPadding: Spacing.md, - bottomPadding: Spacing.lg - ) { - header - explanation - skillActions - } - .background(palette.background) - .fixedSize(horizontal: false, vertical: true) - .onGeometryChange(for: CGFloat.self) { proxy in - proxy.size.height - } action: { newHeight in - let next = newHeight + navigationChrome - if abs(contentHeight - next) > 1 { - contentHeight = next + ScrollView { + CardPageContent( + spacing: Spacing.md, + topPadding: Spacing.md, + bottomPadding: Spacing.xl + ) { + header + Text(skillDescription) + .font(TypeStyle.body) + .foregroundStyle(palette.textSecondary) + .frame(maxWidth: .infinity, alignment: .leading) + promptBlock + thinkingRow + skillActions + } + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: CGFloat.self) { proxy in + proxy.size.height + } action: { newHeight in + let next = newHeight + navigationChrome + if abs(contentHeight - next) > 1 { + contentHeight = next + } } } + .scrollBounceBehavior(.basedOnSize) + .background(palette.background) .navigationTitle("skills.detail.title") .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .cancellationAction) { + ToolbarItem(placement: .confirmationAction) { Button("common.done", action: onDismiss) } } } - .presentationDetents([.height(contentHeight)]) + .presentationDetents([.height(clampedSheetHeight)]) .presentationDragIndicator(.visible) .presentationContentInteraction(.resizes) - .animation(.easeInOut(duration: 0.2), value: contentHeight) + .animation(.easeInOut(duration: 0.2), value: clampedSheetHeight) } - private var explanation: some View { - Text(AppL10n.string(explanationKey, language: config.uiLanguage)) - .font(TypeStyle.body) - .foregroundStyle(palette.textSecondary) - .lineLimit(1) - .frame(maxWidth: .infinity, minHeight: 80, maxHeight: 80, alignment: .topLeading) + private var clampedSheetHeight: CGFloat { + let screen = UIScreen.main.bounds.height + let maximum = screen * 0.92 + return min(max(contentHeight, 280), maximum) } - private var explanationKey: String { + private var skillDescription: String { + if skill.isUserCreated { + let summary = skill.customSummary?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !summary.isEmpty { return summary } + return AppL10n.string("skills.custom.description", language: config.uiLanguage) + } if skill.requiresShortcut, !store.layout.hasConfirmedShortcut(skill.id) { - return "skills.install.lead" + return AppL10n.string("skills.install.lead", language: config.uiLanguage) } if skill.requiresShortcut, store.layout.isEnabled(skill.id) { - return "skills.action.turnOffHint" + return AppL10n.string("skills.action.turnOffHint", language: config.uiLanguage) } - return skill.descriptionKey + return AppL10n.string(skill.descriptionKey, language: config.uiLanguage) + } + + private var promptText: String { + AIClipboardSkillCatalog.instruction( + for: skill, + locale: config.uiLanguage.resolvedLanguageCode() == "zh-Hans" ? "zh" : "en", + translationTargetLocaleId: config.translationTargetLocaleId + ) } private var header: some View { @@ -260,7 +666,7 @@ private struct SkillDetailSheet: View { .frame(width: 36, height: 36) .background(palette.accentMuted, in: Circle()) VStack(alignment: .leading, spacing: 2) { - Text(AppL10n.string(skill.cardTitleKey, language: config.uiLanguage)) + Text(headerTitle) .font(TypeStyle.title3) .foregroundStyle(palette.textPrimary) if skill.isDefault { @@ -273,6 +679,35 @@ private struct SkillDetailSheet: View { } } + private var headerTitle: String { + if let name = skill.customName?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty { + return name + } + return AppL10n.string(skill.cardTitleKey, language: config.uiLanguage) + } + + private var promptBlock: some View { + VStack(alignment: .leading, spacing: Spacing.xs) { + Text("skills.editor.prompt") + .font(TypeStyle.caption) + .foregroundStyle(palette.textTertiary) + Text(promptText) + .font(.body.monospaced()) + .foregroundStyle(palette.textPrimary) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(Spacing.md) + .surfaceCard() + } + } + + private var thinkingRow: some View { + Toggle("skills.editor.thinking", isOn: .constant(skill.thinkingEnabled)) + .disabled(true) + .font(TypeStyle.body) + } + @ViewBuilder private var skillActions: some View { let enabled = store.layout.isEnabled(skill.id) @@ -323,19 +758,8437 @@ private struct SkillDetailSheet: View { prominent: Bool, action: @escaping () -> Void ) -> some View { - let label = Text(titleKey) - .frame(maxWidth: .infinity, minHeight: 38) - if prominent { - Button(action: action) { label } - .buttonStyle(.borderedProminent) - .tint(palette.accent) - } else { - Button(action: action) { label } - .buttonStyle(.bordered) + Button(action: action) { + Text(titleKey) + .font(TypeStyle.bodyEmph) + .frame(maxWidth: .infinity, minHeight: 44) + .foregroundStyle(prominent ? palette.textOnAccent : palette.textPrimary) + .background( + prominent ? palette.accent : palette.surfaceElevated, + in: Capsule() + ) + .overlay( + Capsule() + .stroke(prominent ? Color.clear : palette.dividerStrong, lineWidth: 0.5) + ) } + .buttonStyle(.plain) } private func openShortcutInstall() { AIAgentShortcutInstaller.openInstallPage(for: skill) } } + +private struct SkillEditorSheet: View { + let draft: SkillEditorDraft + let onSave: (SkillEditorDraft) throws -> Void + let onDelete: (String) -> Void + + @Environment(\.dismiss) private var dismiss + @Environment(\.themePalette) private var palette + @State private var name: String + @State private var summary: String + @State private var systemImage: String + @State private var prompt: String + @State private var shortcutLink: String + @State private var shortcutName: String + @State private var thinkingEnabled: Bool + @State private var showSymbolPicker = false + @State private var isLookingUp = false + @State private var lookupMessage: String? + @State private var lookupTask: Task? + @State private var saveError: String? + @State private var confirmDelete = false + + init( + draft: SkillEditorDraft, + onSave: @escaping (SkillEditorDraft) throws -> Void, + onDelete: @escaping (String) -> Void + ) { + self.draft = draft + self.onSave = onSave + self.onDelete = onDelete + _name = State(initialValue: draft.name) + _summary = State(initialValue: draft.summary) + _systemImage = State(initialValue: draft.systemImage) + _prompt = State(initialValue: draft.prompt) + _shortcutLink = State(initialValue: draft.shortcutLink) + _shortcutName = State(initialValue: draft.shortcutName) + _thinkingEnabled = State(initialValue: draft.thinkingEnabled) + } + + var body: some View { + NavigationStack { + Form { + Section("skills.editor.name") { + TextField("skills.editor.namePlaceholder", text: $name) + } + Section("skills.editor.summary") { + TextField("skills.editor.summaryPlaceholder", text: $summary, axis: .vertical) + .lineLimit(6...12) + .frame(minHeight: 108, alignment: .top) + } + Section("skills.editor.icon") { + Button { + showSymbolPicker = true + } label: { + HStack(spacing: Spacing.md) { + Image(systemName: systemImage) + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(palette.accent) + .frame(width: 44, height: 44) + .background( + palette.accentMuted, + in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous) + ) + VStack(alignment: .leading, spacing: 2) { + Text("skills.editor.iconChoose") + .foregroundStyle(palette.textPrimary) + Text(systemImage) + .font(TypeStyle.caption) + .foregroundStyle(palette.textSecondary) + .lineLimit(1) + } + Spacer(minLength: 0) + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(palette.textTertiary) + } + } + } + Section { + TextEditor(text: $prompt) + .font(.body.monospaced()) + .frame(minHeight: 180) + } header: { + HStack { + Text("skills.editor.prompt") + Spacer() + Text("\(prompt.count)/\(AIUserSkillLimits.maximumPromptCharacters)") + .foregroundStyle( + prompt.count > AIUserSkillLimits.maximumPromptCharacters + ? palette.danger + : palette.textTertiary + ) + } + } + Section { + TextField("skills.editor.linkPlaceholder", text: $shortcutLink) + .textInputAutocapitalization(.never) + .keyboardType(.URL) + .autocorrectionDisabled() + TextField("skills.editor.shortcutNamePlaceholder", text: $shortcutName) + if isLookingUp { + Text("skills.editor.lookingUp") + .font(TypeStyle.caption) + .foregroundStyle(palette.textTertiary) + } else if let lookupMessage { + Text(lookupMessage) + .font(TypeStyle.caption) + .foregroundStyle(palette.textTertiary) + } + } header: { + Text("skills.editor.shortcut") + } footer: { + Text("skills.editor.shortcutHint") + } + Section { + Toggle("skills.editor.thinking", isOn: $thinkingEnabled) + } footer: { + Text("skills.editor.thinkingHint") + } + if !draft.isNew { + Section { + Button("common.delete", role: .destructive) { + confirmDelete = true + } + } + } + } + .navigationTitle(draft.isNew ? "skills.add" : "skills.edit") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("common.cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("common.save") { + do { + try save() + dismiss() + } catch { + saveError = localizedSaveError(error) + } + } + .disabled(!canSave) + } + } + .sheet(isPresented: $showSymbolPicker) { + SkillSymbolPicker(selection: $systemImage) + } + .alert("skills.delete.title", isPresented: $confirmDelete) { + Button("common.cancel", role: .cancel) {} + Button("common.delete", role: .destructive) { + onDelete(draft.id) + dismiss() + } + } message: { + Text("skills.delete.message") + } + .alert( + Text("skills.error.title"), + isPresented: Binding( + get: { saveError != nil }, + set: { if !$0 { saveError = nil } } + ) + ) { + Button("common.done") { saveError = nil } + } message: { + Text(saveError ?? "") + } + .onChange(of: shortcutLink) { _, newValue in + lookupShortcutNameIfNeeded(from: newValue) + } + .onDisappear { + lookupTask?.cancel() + } + } + } + + private var canSave: Bool { + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && !prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && prompt.count <= AIUserSkillLimits.maximumPromptCharacters + && !shortcutName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && AIShortcutShareLink.parse(shortcutLink) != nil + } + + private var currentDraft: SkillEditorDraft { + var next = draft + next.name = name + next.summary = summary + next.systemImage = systemImage + next.prompt = prompt + next.shortcutLink = shortcutLink + next.shortcutName = shortcutName + next.thinkingEnabled = thinkingEnabled + return next + } + + private func save() throws { + try onSave(currentDraft) + } + + private func localizedSaveError(_ error: Error) -> String { + switch error as? AIUserSkillValidationError { + case .emptyName: return AppL10n.string("skills.error.emptyName") + case .emptyPrompt: return AppL10n.string("skills.error.emptyPrompt") + case .emptyShortcutName: return AppL10n.string("skills.error.emptyShortcutName") + case .invalidShortcutLink: return AppL10n.string("skills.error.invalidLink") + case .emptyIcon: return AppL10n.string("skills.error.emptyIcon") + case .promptTooLong: return AppL10n.string("skills.error.promptTooLong") + case nil: return AppL10n.string("skills.error.generic") + } + } + + private func lookupShortcutNameIfNeeded(from raw: String) { + lookupTask?.cancel() + guard let url = AIShortcutShareLink.parse(raw) else { + lookupMessage = nil + isLookingUp = false + return + } + isLookingUp = true + lookupMessage = nil + lookupTask = Task { + do { + let fetched = try await AIShortcutShareMetadata.fetchName(from: url) + try Task.checkCancellation() + await MainActor.run { + isLookingUp = false + if shortcutName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + shortcutName = fetched + } + lookupMessage = AppL10n.format( + "skills.editor.resolvedName", + language: nil, + fetched + ) + } + } catch is CancellationError { + return + } catch { + await MainActor.run { + isLookingUp = false + lookupMessage = AppL10n.string("skills.editor.lookupFailed") + } + } + } + } +} + +private enum SFSymbolCatalog { + /// Full SF Symbol names from CoreGlyphs `name_availability.plist`. + /// Kept in source (not a bundle resource) so the existing Xcode project + /// does not need `xcodegen generate` to pick up a new file. + static let names: [String] = catalogText.split(whereSeparator: \.isNewline).map(String.init) + + private static let catalogText = """ +0.circle +0.circle.fill +0.square +0.square.fill +00.circle +00.circle.fill +00.square +00.square.fill +01.circle +01.circle.fill +01.square +01.square.fill +02.circle +02.circle.fill +02.square +02.square.fill +03.circle +03.circle.fill +03.square +03.square.fill +04.circle +04.circle.fill +04.square +04.square.fill +05.circle +05.circle.fill +05.square +05.square.fill +06.circle +06.circle.fill +06.square +06.square.fill +07.circle +07.circle.fill +07.square +07.square.fill +08.circle +08.circle.fill +08.square +08.square.fill +09.circle +09.circle.fill +09.square +09.square.fill +1.brakesignal +1.calendar +1.circle +1.circle.fill +1.lane +1.magnifyingglass +1.square +1.square.fill +10.arrow.trianglehead.clockwise +10.arrow.trianglehead.counterclockwise +10.calendar +10.circle +10.circle.fill +10.lane +10.square +10.square.fill +11.calendar +11.circle +11.circle.fill +11.lane +11.square +11.square.fill +12.calendar +12.circle +12.circle.fill +12.lane +12.square +12.square.fill +123.rectangle +123.rectangle.fill +13.calendar +13.circle +13.circle.fill +13.square +13.square.fill +14.calendar +14.circle +14.circle.fill +14.square +14.square.fill +15.arrow.trianglehead.clockwise +15.arrow.trianglehead.counterclockwise +15.calendar +15.circle +15.circle.fill +15.square +15.square.fill +16.calendar +16.circle +16.circle.fill +16.square +16.square.fill +17.calendar +17.circle +17.circle.fill +17.square +17.square.fill +18.calendar +18.circle +18.circle.fill +18.square +18.square.fill +19.calendar +19.circle +19.circle.fill +19.square +19.square.fill +2.brakesignal +2.calendar +2.circle +2.circle.fill +2.lane +2.square +2.square.fill +20.calendar +20.circle +20.circle.fill +20.square +20.square.fill +21.calendar +21.circle +21.circle.fill +21.square +21.square.fill +22.calendar +22.circle +22.circle.fill +22.square +22.square.fill +23.calendar +23.circle +23.circle.fill +23.square +23.square.fill +24.calendar +24.circle +24.circle.fill +24.square +24.square.fill +25.calendar +25.circle +25.circle.fill +25.square +25.square.fill +26.calendar +26.circle +26.circle.fill +26.square +26.square.fill +27.calendar +27.circle +27.circle.fill +27.square +27.square.fill +28.calendar +28.circle +28.circle.fill +28.square +28.square.fill +29.calendar +29.circle +29.circle.fill +29.square +29.square.fill +2h +2h.circle +2h.circle.fill +3.calendar +3.circle +3.circle.fill +3.lane +3.square +3.square.fill +30.arrow.trianglehead.clockwise +30.arrow.trianglehead.counterclockwise +30.calendar +30.circle +30.circle.fill +30.square +30.square.fill +31.calendar +31.circle +31.circle.fill +31.square +31.square.fill +32.circle +32.circle.fill +32.square +32.square.fill +33.circle +33.circle.fill +33.square +33.square.fill +34.circle +34.circle.fill +34.square +34.square.fill +35.circle +35.circle.fill +35.square +35.square.fill +36.circle +36.circle.fill +36.square +36.square.fill +37.circle +37.circle.fill +37.square +37.square.fill +38.circle +38.circle.fill +38.square +38.square.fill +39.circle +39.circle.fill +39.square +39.square.fill +4.alt.circle +4.alt.circle.fill +4.alt.square +4.alt.square.fill +4.calendar +4.circle +4.circle.fill +4.lane +4.square +4.square.fill +40.circle +40.circle.fill +40.square +40.square.fill +41.circle +41.circle.fill +41.square +41.square.fill +42.circle +42.circle.fill +42.square +42.square.fill +43.circle +43.circle.fill +43.square +43.square.fill +44.circle +44.circle.fill +44.square +44.square.fill +45.arrow.trianglehead.clockwise +45.arrow.trianglehead.counterclockwise +45.circle +45.circle.fill +45.square +45.square.fill +46.circle +46.circle.fill +46.square +46.square.fill +47.circle +47.circle.fill +47.square +47.square.fill +48.circle +48.circle.fill +48.square +48.square.fill +49.circle +49.circle.fill +49.square +49.square.fill +4a +4a.circle +4a.circle.fill +4h +4h.circle +4h.circle.fill +4k.tv +4k.tv.fill +4l +4l.circle +4l.circle.fill +5.arrow.trianglehead.clockwise +5.arrow.trianglehead.counterclockwise +5.calendar +5.circle +5.circle.fill +5.lane +5.square +5.square.fill +50.circle +50.circle.fill +50.square +50.square.fill +6.alt.circle +6.alt.circle.fill +6.alt.square +6.alt.square.fill +6.calendar +6.circle +6.circle.fill +6.lane +6.square +6.square.fill +60.arrow.trianglehead.clockwise +60.arrow.trianglehead.counterclockwise +7.calendar +7.circle +7.circle.fill +7.lane +7.square +7.square.fill +75.arrow.trianglehead.clockwise +75.arrow.trianglehead.counterclockwise +8.calendar +8.circle +8.circle.fill +8.lane +8.square +8.square.fill +9.alt.circle +9.alt.circle.fill +9.alt.square +9.alt.square.fill +9.calendar +9.circle +9.circle.fill +9.lane +9.square +9.square.fill +90.arrow.trianglehead.clockwise +90.arrow.trianglehead.counterclockwise +a +a.book.closed +a.book.closed.fill +a.circle +a.circle.fill +a.magnify +a.square +a.square.fill +abc +abs +abs.brakesignal +abs.brakesignal.slash +abs.circle +abs.circle.fill +ac +ac.slash +accessibility +accessibility.badge.arrow.up.right +accessibility.fill +air.car.side +air.car.side.fill +air.conditioner +air.conditioner.horizontal +air.conditioner.horizontal.fill +air.conditioner.slash +air.conditioner.vertical +air.conditioner.vertical.fill +air.convertible.side +air.convertible.side.fill +air.pickup.side +air.pickup.side.fill +air.purifier +air.purifier.fill +air.suv.side +air.suv.side.fill +airplane +airplane.arrival +airplane.circle +airplane.circle.fill +airplane.cloud +airplane.departure +airplane.landed +airplane.path.dotted +airplane.ticket +airplane.ticket.fill +airplane.up.forward +airplane.up.forward.app +airplane.up.forward.app.fill +airplane.up.right +airplane.up.right.app +airplane.up.right.app.fill +airplaneseat +airplay.audio +airplay.audio.badge.exclamationmark +airplay.audio.circle +airplay.audio.circle.fill +airplay.video +airplay.video.badge.exclamationmark +airplay.video.circle +airplay.video.circle.fill +airplayaudio +airplayaudio.badge.exclamationmark +airplayaudio.circle +airplayaudio.circle.fill +airplayvideo +airplayvideo.badge.exclamationmark +airplayvideo.circle +airplayvideo.circle.fill +airpod.gen3.left +airpod.gen3.right +airpod.left +airpod.right +airpodpro.left +airpodpro.right +airpods +airpods.chargingcase +airpods.chargingcase.fill +airpods.chargingcase.wireless +airpods.chargingcase.wireless.fill +airpods.gen3 +airpods.gen3.chargingcase.wireless +airpods.gen3.chargingcase.wireless.fill +airpods.gen4 +airpods.gen4.chargingcase.wireless +airpods.gen4.chargingcase.wireless.fill +airpods.gen4.left +airpods.gen4.right +airpods.max +airpods.pro +airpods.pro.chargingcase.wireless +airpods.pro.chargingcase.wireless.fill +airpods.pro.chargingcase.wireless.radiowaves.left.and.right +airpods.pro.chargingcase.wireless.radiowaves.left.and.right.fill +airpods.pro.left +airpods.pro.right +airpodsmax +airpodspro +airpodspro.chargingcase.wireless +airpodspro.chargingcase.wireless.fill +airpodspro.chargingcase.wireless.radiowaves.left.and.right +airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill +airport.express +airport.extreme +airport.extreme.tower +airtag +airtag.fill +airtag.radiowaves.forward +airtag.radiowaves.forward.fill +alarm +alarm.fill +alarm.waves.left.and.right +alarm.waves.left.and.right.fill +align.horizontal.center +align.horizontal.center.fill +align.horizontal.left +align.horizontal.left.fill +align.horizontal.right +align.horizontal.right.fill +align.vertical.bottom +align.vertical.bottom.fill +align.vertical.center +align.vertical.center.fill +align.vertical.top +align.vertical.top.fill +allergens +allergens.fill +alt +alternatingcurrent +american.football +american.football.circle +american.football.circle.fill +american.football.fill +american.football.professional +american.football.professional.circle +american.football.professional.circle.fill +american.football.professional.fill +amplifier +angle +ant +ant.circle +ant.circle.fill +ant.fill +antenna.radiowaves.left.and.right +antenna.radiowaves.left.and.right.circle +antenna.radiowaves.left.and.right.circle.fill +antenna.radiowaves.left.and.right.slash +antenna.radiowaves.left.and.right.slash.circle +antenna.radiowaves.left.and.right.slash.circle.fill +app +app.background.dotted +app.badge +app.badge.checkmark +app.badge.checkmark.fill +app.badge.clock +app.badge.clock.fill +app.badge.fill +app.connected.to.app.below.fill +app.dashed +app.fill +app.gift +app.gift.fill +app.grid +app.shadow +app.specular +app.translucent +appclip +append.page +append.page.fill +apple.books.pages +apple.books.pages.fill +apple.classical.pages +apple.classical.pages.fill +apple.haptics.and.exclamationmark.triangle +apple.haptics.and.music.note +apple.haptics.and.music.note.slash +apple.homekit +apple.image.playground +apple.image.playground.fill +apple.intelligence +apple.intelligence.badge.xmark +apple.logo +apple.meditate +apple.meditate.circle +apple.meditate.circle.fill +apple.meditate.square.stack +apple.meditate.square.stack.fill +apple.podcasts.pages +apple.podcasts.pages.fill +apple.terminal +apple.terminal.circle +apple.terminal.circle.fill +apple.terminal.fill +apple.terminal.on.rectangle +apple.terminal.on.rectangle.fill +apple.writing.tools +applelogo +applepencil +applepencil.adapter.usb.c +applepencil.adapter.usb.c.fill +applepencil.and.scribble +applepencil.doubletap +applepencil.gen1 +applepencil.gen2 +applepencil.hover +applepencil.squeeze +applepencil.tip +applescript +applescript.fill +appletv +appletv.badge.checkmark +appletv.badge.checkmark.fill +appletv.badge.exclamationmark +appletv.badge.exclamationmark.fill +appletv.fill +appletvremote.gen1 +appletvremote.gen1.fill +appletvremote.gen2 +appletvremote.gen2.fill +appletvremote.gen3 +appletvremote.gen3.fill +appletvremote.gen4 +appletvremote.gen4.fill +applewatch +applewatch.and.arrow.forward +applewatch.badge.checkmark +applewatch.badge.exclamationmark +applewatch.case.inset.filled +applewatch.case.sizes +applewatch.radiowaves.left.and.right +applewatch.side.right +applewatch.slash +applewatch.watchface +apps.ipad +apps.ipad.badge.checkmark +apps.ipad.badge.plus +apps.ipad.landscape +apps.ipad.on.rectangle.portrait.dashed +apps.iphone +apps.iphone.badge.checkmark +apps.iphone.badge.plus +apps.iphone.landscape +appwindow.swipe.rectangle +aqi.high +aqi.low +aqi.medium +aqi.medium.gauge.open +arcade.stick +arcade.stick.and.arrow.down +arcade.stick.and.arrow.left +arcade.stick.and.arrow.left.and.arrow.right +arcade.stick.and.arrow.left.and.arrow.right.outward +arcade.stick.and.arrow.right +arcade.stick.and.arrow.up +arcade.stick.and.arrow.up.and.arrow.down +arcade.stick.console +arcade.stick.console.fill +archivebox +archivebox.circle +archivebox.circle.fill +archivebox.fill +arkit +arkit.badge.xmark +arrow.2.circlepath +arrow.2.circlepath.circle +arrow.2.circlepath.circle.fill +arrow.2.squarepath +arrow.3.trianglepath +arrow.backward +arrow.backward.circle +arrow.backward.circle.dotted +arrow.backward.circle.fill +arrow.backward.square +arrow.backward.square.fill +arrow.backward.to.line +arrow.backward.to.line.circle +arrow.backward.to.line.circle.fill +arrow.backward.to.line.compact +arrow.backward.to.line.square +arrow.backward.to.line.square.fill +arrow.branch +arrow.circlepath +arrow.clockwise +arrow.clockwise.circle +arrow.clockwise.circle.fill +arrow.clockwise.heart +arrow.clockwise.heart.fill +arrow.clockwise.icloud +arrow.clockwise.icloud.fill +arrow.clockwise.square +arrow.clockwise.square.fill +arrow.counterclockwise +arrow.counterclockwise.circle +arrow.counterclockwise.circle.fill +arrow.counterclockwise.icloud +arrow.counterclockwise.icloud.fill +arrow.counterclockwise.square +arrow.counterclockwise.square.fill +arrow.down +arrow.down.and.line.horizontal.and.arrow.up +arrow.down.app +arrow.down.app.dashed +arrow.down.app.dashed.trianglebadge.exclamationmark +arrow.down.app.fill +arrow.down.applewatch +arrow.down.backward +arrow.down.backward.and.arrow.up.forward +arrow.down.backward.and.arrow.up.forward.circle +arrow.down.backward.and.arrow.up.forward.circle.fill +arrow.down.backward.and.arrow.up.forward.rectangle +arrow.down.backward.and.arrow.up.forward.rectangle.fill +arrow.down.backward.and.arrow.up.forward.square +arrow.down.backward.and.arrow.up.forward.square.fill +arrow.down.backward.circle +arrow.down.backward.circle.dotted +arrow.down.backward.circle.fill +arrow.down.backward.square +arrow.down.backward.square.fill +arrow.down.backward.toptrailing.rectangle +arrow.down.backward.toptrailing.rectangle.fill +arrow.down.circle +arrow.down.circle.badge.pause +arrow.down.circle.badge.pause.fill +arrow.down.circle.badge.xmark +arrow.down.circle.badge.xmark.fill +arrow.down.circle.dotted +arrow.down.circle.fill +arrow.down.doc +arrow.down.doc.fill +arrow.down.document +arrow.down.document.fill +arrow.down.forward +arrow.down.forward.and.arrow.up.backward +arrow.down.forward.and.arrow.up.backward.circle +arrow.down.forward.and.arrow.up.backward.circle.fill +arrow.down.forward.and.arrow.up.backward.rectangle +arrow.down.forward.and.arrow.up.backward.rectangle.fill +arrow.down.forward.and.arrow.up.backward.square +arrow.down.forward.and.arrow.up.backward.square.fill +arrow.down.forward.circle +arrow.down.forward.circle.dotted +arrow.down.forward.circle.fill +arrow.down.forward.square +arrow.down.forward.square.fill +arrow.down.forward.topleading.rectangle +arrow.down.forward.topleading.rectangle.fill +arrow.down.heart +arrow.down.heart.fill +arrow.down.left +arrow.down.left.and.arrow.up.right +arrow.down.left.and.arrow.up.right.circle +arrow.down.left.and.arrow.up.right.circle.fill +arrow.down.left.and.arrow.up.right.rectangle +arrow.down.left.and.arrow.up.right.rectangle.fill +arrow.down.left.and.arrow.up.right.square +arrow.down.left.and.arrow.up.right.square.fill +arrow.down.left.arrow.up.right +arrow.down.left.arrow.up.right.circle +arrow.down.left.arrow.up.right.circle.fill +arrow.down.left.arrow.up.right.square +arrow.down.left.arrow.up.right.square.fill +arrow.down.left.circle +arrow.down.left.circle.dotted +arrow.down.left.circle.fill +arrow.down.left.square +arrow.down.left.square.fill +arrow.down.left.topright.rectangle +arrow.down.left.topright.rectangle.fill +arrow.down.left.video +arrow.down.left.video.fill +arrow.down.message +arrow.down.message.fill +arrow.down.right +arrow.down.right.and.arrow.up.left +arrow.down.right.and.arrow.up.left.circle +arrow.down.right.and.arrow.up.left.circle.fill +arrow.down.right.and.arrow.up.left.rectangle +arrow.down.right.and.arrow.up.left.rectangle.fill +arrow.down.right.and.arrow.up.left.square +arrow.down.right.and.arrow.up.left.square.fill +arrow.down.right.circle +arrow.down.right.circle.dotted +arrow.down.right.circle.fill +arrow.down.right.square +arrow.down.right.square.fill +arrow.down.right.topleft.rectangle +arrow.down.right.topleft.rectangle.fill +arrow.down.square +arrow.down.square.fill +arrow.down.to.line +arrow.down.to.line.alt +arrow.down.to.line.circle +arrow.down.to.line.circle.fill +arrow.down.to.line.compact +arrow.down.to.line.square +arrow.down.to.line.square.fill +arrow.forward +arrow.forward.circle +arrow.forward.circle.dotted +arrow.forward.circle.fill +arrow.forward.folder +arrow.forward.folder.fill +arrow.forward.square +arrow.forward.square.fill +arrow.forward.to.line +arrow.forward.to.line.circle +arrow.forward.to.line.circle.fill +arrow.forward.to.line.compact +arrow.forward.to.line.square +arrow.forward.to.line.square.fill +arrow.left +arrow.left.and.line.vertical.and.arrow.right +arrow.left.and.right +arrow.left.and.right.circle +arrow.left.and.right.circle.fill +arrow.left.and.right.righttriangle.left.righttriangle.right +arrow.left.and.right.righttriangle.left.righttriangle.right.fill +arrow.left.and.right.square +arrow.left.and.right.square.fill +arrow.left.and.right.text.vertical +arrow.left.arrow.right +arrow.left.arrow.right.circle +arrow.left.arrow.right.circle.fill +arrow.left.arrow.right.square +arrow.left.arrow.right.square.fill +arrow.left.circle +arrow.left.circle.dotted +arrow.left.circle.fill +arrow.left.square +arrow.left.square.fill +arrow.left.to.line +arrow.left.to.line.alt +arrow.left.to.line.circle +arrow.left.to.line.circle.fill +arrow.left.to.line.compact +arrow.left.to.line.square +arrow.left.to.line.square.fill +arrow.merge +arrow.rectanglepath +arrow.right +arrow.right.and.line.vertical.and.arrow.left +arrow.right.arrow.left +arrow.right.arrow.left.circle +arrow.right.arrow.left.circle.fill +arrow.right.arrow.left.square +arrow.right.arrow.left.square.fill +arrow.right.circle +arrow.right.circle.dotted +arrow.right.circle.fill +arrow.right.doc.on.clipboard +arrow.right.filled.filter.arrow.right +arrow.right.page.on.clipboard +arrow.right.square +arrow.right.square.fill +arrow.right.to.line +arrow.right.to.line.alt +arrow.right.to.line.circle +arrow.right.to.line.circle.fill +arrow.right.to.line.compact +arrow.right.to.line.square +arrow.right.to.line.square.fill +arrow.swap +arrow.triangle.2.circlepath +arrow.triangle.2.circlepath.camera +arrow.triangle.2.circlepath.camera.fill +arrow.triangle.2.circlepath.circle +arrow.triangle.2.circlepath.circle.fill +arrow.triangle.2.circlepath.doc.on.clipboard +arrow.triangle.2.circlepath.icloud +arrow.triangle.2.circlepath.icloud.fill +arrow.triangle.branch +arrow.triangle.capsulepath +arrow.triangle.merge +arrow.triangle.pull +arrow.triangle.swap +arrow.triangle.turn.up.right.circle +arrow.triangle.turn.up.right.circle.fill +arrow.triangle.turn.up.right.diamond +arrow.triangle.turn.up.right.diamond.fill +arrow.trianglehead.2.clockwise +arrow.trianglehead.2.clockwise.rotate.90 +arrow.trianglehead.2.clockwise.rotate.90.camera +arrow.trianglehead.2.clockwise.rotate.90.camera.fill +arrow.trianglehead.2.clockwise.rotate.90.circle +arrow.trianglehead.2.clockwise.rotate.90.circle.fill +arrow.trianglehead.2.clockwise.rotate.90.icloud +arrow.trianglehead.2.clockwise.rotate.90.icloud.fill +arrow.trianglehead.2.clockwise.rotate.90.page.on.clipboard +arrow.trianglehead.2.counterclockwise +arrow.trianglehead.2.counterclockwise.rotate.90 +arrow.trianglehead.bottomleft.capsulepath.clockwise +arrow.trianglehead.branch +arrow.trianglehead.clockwise +arrow.trianglehead.clockwise.heart +arrow.trianglehead.clockwise.heart.fill +arrow.trianglehead.clockwise.icloud +arrow.trianglehead.clockwise.icloud.fill +arrow.trianglehead.clockwise.rotate.90 +arrow.trianglehead.counterclockwise +arrow.trianglehead.counterclockwise.icloud +arrow.trianglehead.counterclockwise.icloud.fill +arrow.trianglehead.counterclockwise.rotate.90 +arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right +arrow.trianglehead.left.and.right.righttriangle.left.righttriangle.right.fill +arrow.trianglehead.merge +arrow.trianglehead.pull +arrow.trianglehead.rectanglepath +arrow.trianglehead.swap +arrow.trianglehead.topright.capsulepath.clockwise +arrow.trianglehead.turn.up.right +arrow.trianglehead.turn.up.right.circle +arrow.trianglehead.turn.up.right.circle.fill +arrow.trianglehead.turn.up.right.diamond +arrow.trianglehead.turn.up.right.diamond.fill +arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down +arrow.trianglehead.up.and.down.righttriangle.up.righttriangle.down.fill +arrow.turn.down.left +arrow.turn.down.right +arrow.turn.left.down +arrow.turn.left.up +arrow.turn.right.down +arrow.turn.right.up +arrow.turn.up.forward.iphone +arrow.turn.up.forward.iphone.fill +arrow.turn.up.left +arrow.turn.up.right +arrow.up +arrow.up.and.down +arrow.up.and.down.and.arrow.left.and.right +arrow.up.and.down.and.sparkles +arrow.up.and.down.circle +arrow.up.and.down.circle.fill +arrow.up.and.down.righttriangle.up.fill.righttriangle.down.fill +arrow.up.and.down.righttriangle.up.righttriangle.down +arrow.up.and.down.righttriangle.up.righttriangle.down.fill +arrow.up.and.down.square +arrow.up.and.down.square.fill +arrow.up.and.down.text.horizontal +arrow.up.and.line.horizontal.and.arrow.down +arrow.up.and.person.rectangle.portrait +arrow.up.and.person.rectangle.turn.left +arrow.up.and.person.rectangle.turn.right +arrow.up.arrow.down +arrow.up.arrow.down.circle +arrow.up.arrow.down.circle.fill +arrow.up.arrow.down.square +arrow.up.arrow.down.square.fill +arrow.up.backward +arrow.up.backward.and.arrow.down.forward +arrow.up.backward.and.arrow.down.forward.circle +arrow.up.backward.and.arrow.down.forward.circle.fill +arrow.up.backward.and.arrow.down.forward.rectangle +arrow.up.backward.and.arrow.down.forward.rectangle.fill +arrow.up.backward.and.arrow.down.forward.square +arrow.up.backward.and.arrow.down.forward.square.fill +arrow.up.backward.bottomtrailing.rectangle +arrow.up.backward.bottomtrailing.rectangle.fill +arrow.up.backward.circle +arrow.up.backward.circle.dotted +arrow.up.backward.circle.fill +arrow.up.backward.square +arrow.up.backward.square.fill +arrow.up.bin +arrow.up.bin.fill +arrow.up.circle +arrow.up.circle.badge.clock +arrow.up.circle.dotted +arrow.up.circle.fill +arrow.up.doc +arrow.up.doc.fill +arrow.up.doc.on.clipboard +arrow.up.document +arrow.up.document.fill +arrow.up.folder +arrow.up.folder.fill +arrow.up.forward +arrow.up.forward.and.arrow.down.backward +arrow.up.forward.and.arrow.down.backward.circle +arrow.up.forward.and.arrow.down.backward.circle.fill +arrow.up.forward.and.arrow.down.backward.rectangle +arrow.up.forward.and.arrow.down.backward.rectangle.fill +arrow.up.forward.and.arrow.down.backward.square +arrow.up.forward.and.arrow.down.backward.square.fill +arrow.up.forward.app +arrow.up.forward.app.fill +arrow.up.forward.bottomleading.rectangle +arrow.up.forward.bottomleading.rectangle.fill +arrow.up.forward.circle +arrow.up.forward.circle.dotted +arrow.up.forward.circle.fill +arrow.up.forward.square +arrow.up.forward.square.fill +arrow.up.heart +arrow.up.heart.fill +arrow.up.left +arrow.up.left.and.arrow.down.right +arrow.up.left.and.arrow.down.right.circle +arrow.up.left.and.arrow.down.right.circle.fill +arrow.up.left.and.arrow.down.right.rectangle +arrow.up.left.and.arrow.down.right.rectangle.fill +arrow.up.left.and.arrow.down.right.square +arrow.up.left.and.arrow.down.right.square.fill +arrow.up.left.and.down.right.and.arrow.up.right.and.down.left +arrow.up.left.and.down.right.magnifyingglass +arrow.up.left.arrow.down.right +arrow.up.left.arrow.down.right.circle +arrow.up.left.arrow.down.right.circle.fill +arrow.up.left.arrow.down.right.square +arrow.up.left.arrow.down.right.square.fill +arrow.up.left.bottomright.rectangle +arrow.up.left.bottomright.rectangle.fill +arrow.up.left.circle +arrow.up.left.circle.dotted +arrow.up.left.circle.fill +arrow.up.left.square +arrow.up.left.square.fill +arrow.up.message +arrow.up.message.fill +arrow.up.page.on.clipboard +arrow.up.right +arrow.up.right.and.arrow.down.left +arrow.up.right.and.arrow.down.left.circle +arrow.up.right.and.arrow.down.left.circle.fill +arrow.up.right.and.arrow.down.left.rectangle +arrow.up.right.and.arrow.down.left.rectangle.fill +arrow.up.right.and.arrow.down.left.square +arrow.up.right.and.arrow.down.left.square.fill +arrow.up.right.bottomleft.rectangle +arrow.up.right.bottomleft.rectangle.fill +arrow.up.right.circle +arrow.up.right.circle.dotted +arrow.up.right.circle.fill +arrow.up.right.diamond +arrow.up.right.diamond.fill +arrow.up.right.square +arrow.up.right.square.fill +arrow.up.right.video +arrow.up.right.video.fill +arrow.up.square +arrow.up.square.fill +arrow.up.to.line +arrow.up.to.line.alt +arrow.up.to.line.circle +arrow.up.to.line.circle.fill +arrow.up.to.line.compact +arrow.up.to.line.square +arrow.up.to.line.square.fill +arrow.up.trash +arrow.up.trash.fill +arrow.uturn.backward +arrow.uturn.backward.circle +arrow.uturn.backward.circle.badge.ellipsis +arrow.uturn.backward.circle.fill +arrow.uturn.backward.square +arrow.uturn.backward.square.fill +arrow.uturn.down +arrow.uturn.down.circle +arrow.uturn.down.circle.fill +arrow.uturn.down.square +arrow.uturn.down.square.fill +arrow.uturn.forward +arrow.uturn.forward.circle +arrow.uturn.forward.circle.fill +arrow.uturn.forward.square +arrow.uturn.forward.square.fill +arrow.uturn.left +arrow.uturn.left.circle +arrow.uturn.left.circle.badge.ellipsis +arrow.uturn.left.circle.fill +arrow.uturn.left.square +arrow.uturn.left.square.fill +arrow.uturn.right +arrow.uturn.right.circle +arrow.uturn.right.circle.fill +arrow.uturn.right.square +arrow.uturn.right.square.fill +arrow.uturn.up +arrow.uturn.up.circle +arrow.uturn.up.circle.fill +arrow.uturn.up.square +arrow.uturn.up.square.fill +arrowkeys +arrowkeys.down.filled +arrowkeys.fill +arrowkeys.left.filled +arrowkeys.right.filled +arrowkeys.up.filled +arrowshape.backward +arrowshape.backward.circle +arrowshape.backward.circle.fill +arrowshape.backward.fill +arrowshape.bounce.forward +arrowshape.bounce.forward.fill +arrowshape.bounce.right +arrowshape.bounce.right.fill +arrowshape.down +arrowshape.down.circle +arrowshape.down.circle.fill +arrowshape.down.fill +arrowshape.forward +arrowshape.forward.circle +arrowshape.forward.circle.fill +arrowshape.forward.fill +arrowshape.left +arrowshape.left.arrowshape.right +arrowshape.left.arrowshape.right.fill +arrowshape.left.circle +arrowshape.left.circle.fill +arrowshape.left.fill +arrowshape.right +arrowshape.right.circle +arrowshape.right.circle.fill +arrowshape.right.fill +arrowshape.turn.up.backward +arrowshape.turn.up.backward.2 +arrowshape.turn.up.backward.2.circle +arrowshape.turn.up.backward.2.circle.fill +arrowshape.turn.up.backward.2.fill +arrowshape.turn.up.backward.badge.clock +arrowshape.turn.up.backward.badge.clock.fill +arrowshape.turn.up.backward.circle +arrowshape.turn.up.backward.circle.fill +arrowshape.turn.up.backward.fill +arrowshape.turn.up.forward +arrowshape.turn.up.forward.circle +arrowshape.turn.up.forward.circle.fill +arrowshape.turn.up.forward.fill +arrowshape.turn.up.left +arrowshape.turn.up.left.2 +arrowshape.turn.up.left.2.circle +arrowshape.turn.up.left.2.circle.fill +arrowshape.turn.up.left.2.fill +arrowshape.turn.up.left.circle +arrowshape.turn.up.left.circle.fill +arrowshape.turn.up.left.fill +arrowshape.turn.up.right +arrowshape.turn.up.right.circle +arrowshape.turn.up.right.circle.fill +arrowshape.turn.up.right.fill +arrowshape.up +arrowshape.up.circle +arrowshape.up.circle.fill +arrowshape.up.fill +arrowshape.zigzag.forward +arrowshape.zigzag.forward.fill +arrowshape.zigzag.right +arrowshape.zigzag.right.fill +arrowtriangle.backward +arrowtriangle.backward.circle +arrowtriangle.backward.circle.fill +arrowtriangle.backward.fill +arrowtriangle.backward.inset.filled.trailingthird.rectangle +arrowtriangle.backward.square +arrowtriangle.backward.square.fill +arrowtriangle.down +arrowtriangle.down.2 +arrowtriangle.down.2.fill +arrowtriangle.down.circle +arrowtriangle.down.circle.fill +arrowtriangle.down.fill +arrowtriangle.down.square +arrowtriangle.down.square.fill +arrowtriangle.forward +arrowtriangle.forward.circle +arrowtriangle.forward.circle.fill +arrowtriangle.forward.fill +arrowtriangle.forward.inset.filled.trailingthird.rectangle +arrowtriangle.forward.square +arrowtriangle.forward.square.fill +arrowtriangle.left +arrowtriangle.left.and.line.vertical.and.arrowtriangle.right +arrowtriangle.left.and.line.vertical.and.arrowtriangle.right.fill +arrowtriangle.left.circle +arrowtriangle.left.circle.fill +arrowtriangle.left.fill +arrowtriangle.left.fill.and.line.vertical.and.arrowtriangle.right.fill +arrowtriangle.left.square +arrowtriangle.left.square.fill +arrowtriangle.right +arrowtriangle.right.and.line.vertical.and.arrowtriangle.left +arrowtriangle.right.and.line.vertical.and.arrowtriangle.left.fill +arrowtriangle.right.circle +arrowtriangle.right.circle.fill +arrowtriangle.right.fill +arrowtriangle.right.fill.and.line.vertical.and.arrowtriangle.left.fill +arrowtriangle.right.square +arrowtriangle.right.square.fill +arrowtriangle.up +arrowtriangle.up.2 +arrowtriangle.up.2.fill +arrowtriangle.up.arrowtriangle.down.window.left +arrowtriangle.up.arrowtriangle.down.window.right +arrowtriangle.up.circle +arrowtriangle.up.circle.fill +arrowtriangle.up.fill +arrowtriangle.up.square +arrowtriangle.up.square.fill +aspectratio +aspectratio.fill +asterisk +asterisk.circle +asterisk.circle.fill +at +at.badge.minus +at.badge.plus +at.circle +at.circle.fill +atom +audio.jack.mono +audio.jack.stereo +australian.football +australian.football.circle +australian.football.circle.fill +australian.football.fill +australiandollarsign +australiandollarsign.arrow.circlepath +australiandollarsign.arrow.trianglehead.counterclockwise.rotate.90 +australiandollarsign.bank.building +australiandollarsign.bank.building.fill +australiandollarsign.circle +australiandollarsign.circle.fill +australiandollarsign.gauge.chart.lefthalf.righthalf +australiandollarsign.gauge.chart.leftthird.topthird.rightthird +australiandollarsign.ring +australiandollarsign.ring.dashed +australiandollarsign.square +australiandollarsign.square.fill +australsign +australsign.arrow.circlepath +australsign.arrow.trianglehead.counterclockwise.rotate.90 +australsign.bank.building +australsign.bank.building.fill +australsign.circle +australsign.circle.fill +australsign.gauge.chart.lefthalf.righthalf +australsign.gauge.chart.leftthird.topthird.rightthird +australsign.ring +australsign.ring.dashed +australsign.square +australsign.square.fill +auto.brakesignal +auto.headlight.high.beam +auto.headlight.high.beam.fill +auto.headlight.low.beam +auto.headlight.low.beam.fill +automatic.brakesignal +automatic.headlight.high.beam +automatic.headlight.high.beam.fill +automatic.headlight.low.beam +automatic.headlight.low.beam.fill +autostartstop +autostartstop.slash +autostartstop.trianglebadge.exclamationmark +av.remote +av.remote.fill +axel.2 +axel.2.front.and.rear.engaged +axel.2.front.engaged +axel.2.rear.engaged +axle.2 +axle.2.driveshaft.disengaged +axle.2.front.and.rear.engaged +axle.2.front.disengaged +axle.2.front.engaged +axle.2.rear.disengaged +axle.2.rear.engaged +axle.2.rear.lock +b.circle +b.circle.fill +b.square +b.square.fill +backpack +backpack.circle +backpack.circle.fill +backpack.fill +backpack.sensor.tag.radiowaves.left.and.right +backpack.sensor.tag.radiowaves.left.and.right.fill +backward +backward.circle +backward.circle.fill +backward.end +backward.end.alt +backward.end.alt.fill +backward.end.circle +backward.end.circle.fill +backward.end.fill +backward.fill +backward.frame +backward.frame.fill +badge.plus.radiowaves.forward +badge.plus.radiowaves.right +bag +bag.badge.minus +bag.badge.plus +bag.badge.questionmark +bag.circle +bag.circle.fill +bag.fill +bag.fill.badge.minus +bag.fill.badge.plus +bag.fill.badge.questionmark +bahtsign +bahtsign.arrow.circlepath +bahtsign.arrow.trianglehead.counterclockwise.rotate.90 +bahtsign.bank.building +bahtsign.bank.building.fill +bahtsign.circle +bahtsign.circle.fill +bahtsign.gauge.chart.lefthalf.righthalf +bahtsign.gauge.chart.leftthird.topthird.rightthird +bahtsign.ring +bahtsign.ring.dashed +bahtsign.square +bahtsign.square.fill +balloon +balloon.2 +balloon.2.fill +balloon.fill +bandage +bandage.fill +banknote +banknote.fill +barcode +barcode.viewfinder +barometer +base.unit +baseball +baseball.circle +baseball.circle.fill +baseball.diamond.bases +baseball.diamond.bases.outs.indicator +baseball.fill +basket +basket.fill +basketball +basketball.circle +basketball.circle.fill +basketball.fill +bathtub +bathtub.fill +battery.0 +battery.0percent +battery.100 +battery.100.bolt +battery.100.circle +battery.100.circle.fill +battery.100percent +battery.100percent.bolt +battery.100percent.circle +battery.100percent.circle.fill +battery.25 +battery.25percent +battery.50 +battery.50percent +battery.75 +battery.75percent +batteryblock +batteryblock.fill +batteryblock.slash +batteryblock.slash.fill +batteryblock.stack +batteryblock.stack.badge.snowflake +batteryblock.stack.badge.snowflake.fill +batteryblock.stack.fill +batteryblock.stack.trianglebadge.exclamationmark +batteryblock.stack.trianglebadge.exclamationmark.fill +beach.umbrella +beach.umbrella.fill +beats.earphones +beats.fit.pro +beats.fit.pro.chargingcase +beats.fit.pro.chargingcase.fill +beats.fit.pro.left +beats.fit.pro.right +beats.fitpro +beats.fitpro.chargingcase +beats.fitpro.chargingcase.fill +beats.fitpro.left +beats.fitpro.right +beats.headphones +beats.pill +beats.pill.fill +beats.powerbeats +beats.powerbeats.left +beats.powerbeats.pro +beats.powerbeats.pro.2 +beats.powerbeats.pro.2.chargingcase +beats.powerbeats.pro.2.chargingcase.fill +beats.powerbeats.pro.2.left +beats.powerbeats.pro.2.right +beats.powerbeats.pro.chargingcase +beats.powerbeats.pro.chargingcase.fill +beats.powerbeats.pro.left +beats.powerbeats.pro.right +beats.powerbeats.right +beats.powerbeats3 +beats.powerbeats3.left +beats.powerbeats3.right +beats.powerbeatspro +beats.powerbeatspro.chargingcase +beats.powerbeatspro.chargingcase.fill +beats.powerbeatspro.left +beats.powerbeatspro.right +beats.solobuds +beats.solobuds.chargingcase +beats.solobuds.chargingcase.fill +beats.solobuds.left +beats.solobuds.right +beats.studiobud.left +beats.studiobud.right +beats.studiobuds +beats.studiobuds.chargingcase +beats.studiobuds.chargingcase.fill +beats.studiobuds.left +beats.studiobuds.plus +beats.studiobuds.plus.chargingcase +beats.studiobuds.plus.chargingcase.fill +beats.studiobuds.plus.left +beats.studiobuds.plus.right +beats.studiobuds.right +beats.studiobudsplus +beats.studiobudsplus.chargingcase +beats.studiobudsplus.chargingcase.fill +beats.studiobudsplus.left +beats.studiobudsplus.right +bed.double +bed.double.badge.checkmark +bed.double.badge.checkmark.fill +bed.double.circle +bed.double.circle.fill +bed.double.fill +bell +bell.and.waveform +bell.and.waveform.fill +bell.and.waves.left.and.right +bell.and.waves.left.and.right.fill +bell.badge +bell.badge.circle +bell.badge.circle.fill +bell.badge.fill +bell.badge.slash +bell.badge.slash.fill +bell.badge.waveform +bell.badge.waveform.fill +bell.badge.waveform.slash +bell.badge.waveform.slash.fill +bell.circle +bell.circle.fill +bell.fill +bell.slash +bell.slash.circle +bell.slash.circle.fill +bell.slash.fill +bell.square +bell.square.fill +beziercurve +bicycle +bicycle.circle +bicycle.circle.fill +bicycle.sensor.tag.radiowaves.left.and.right +bicycle.sensor.tag.radiowaves.left.and.right.fill +bin.xmark +bin.xmark.fill +binoculars +binoculars.circle +binoculars.circle.fill +binoculars.fill +bird +bird.circle +bird.circle.fill +bird.fill +birthday.cake +birthday.cake.fill +bitcoinsign +bitcoinsign.arrow.circlepath +bitcoinsign.arrow.trianglehead.counterclockwise.rotate.90 +bitcoinsign.bank.building +bitcoinsign.bank.building.fill +bitcoinsign.circle +bitcoinsign.circle.fill +bitcoinsign.gauge.chart.lefthalf.righthalf +bitcoinsign.gauge.chart.leftthird.topthird.rightthird +bitcoinsign.ring +bitcoinsign.ring.dashed +bitcoinsign.square +bitcoinsign.square.fill +blinds.horizontal.closed +blinds.horizontal.open +blinds.vertical.closed +blinds.vertical.open +blood.pressure.cuff +blood.pressure.cuff.badge.gauge.with.needle +blood.pressure.cuff.badge.gauge.with.needle.fill +blood.pressure.cuff.fill +bold +bold.italic.underline +bold.underline +bolt +bolt.badge.a +bolt.badge.a.fill +bolt.badge.automatic +bolt.badge.automatic.fill +bolt.badge.checkmark +bolt.badge.checkmark.fill +bolt.badge.clock +bolt.badge.clock.fill +bolt.badge.xmark +bolt.badge.xmark.fill +bolt.batteryblock +bolt.batteryblock.fill +bolt.brakesignal +bolt.car +bolt.car.circle +bolt.car.circle.fill +bolt.car.fill +bolt.circle +bolt.circle.fill +bolt.fill +bolt.fill.batteryblock +bolt.fill.batteryblock.fill +bolt.heart +bolt.heart.fill +bolt.horizontal +bolt.horizontal.circle +bolt.horizontal.circle.fill +bolt.horizontal.fill +bolt.horizontal.icloud +bolt.horizontal.icloud.fill +bolt.house +bolt.house.fill +bolt.ring.closed +bolt.shield +bolt.shield.fill +bolt.slash +bolt.slash.circle +bolt.slash.circle.fill +bolt.slash.fill +bolt.square +bolt.square.fill +bolt.trianglebadge.exclamationmark +bolt.trianglebadge.exclamationmark.fill +bonjour +book +book.and.wrench +book.and.wrench.fill +book.badge.plus +book.badge.plus.fill +book.circle +book.circle.fill +book.closed +book.closed.circle +book.closed.circle.fill +book.closed.fill +book.fill +book.pages +book.pages.fill +bookmark +bookmark.circle +bookmark.circle.fill +bookmark.fill +bookmark.slash +bookmark.slash.fill +bookmark.square +bookmark.square.fill +books.vertical +books.vertical.circle +books.vertical.circle.fill +books.vertical.fill +box.truck +box.truck.badge.clock +box.truck.badge.clock.fill +box.truck.fill +brain +brain.fill +brain.filled.head.profile +brain.head.profile +brain.head.profile.fill +brakesignal +brakesignal.dashed +brazilianrealsign +brazilianrealsign.arrow.circlepath +brazilianrealsign.arrow.trianglehead.counterclockwise.rotate.90 +brazilianrealsign.bank.building +brazilianrealsign.bank.building.fill +brazilianrealsign.circle +brazilianrealsign.circle.fill +brazilianrealsign.gauge.chart.lefthalf.righthalf +brazilianrealsign.gauge.chart.leftthird.topthird.rightthird +brazilianrealsign.ring +brazilianrealsign.ring.dashed +brazilianrealsign.square +brazilianrealsign.square.fill +briefcase +briefcase.circle +briefcase.circle.fill +briefcase.fill +briefcase.sensor.tag.radiowaves.left.and.right +briefcase.sensor.tag.radiowaves.left.and.right.fill +bubble +bubble.and.pencil +bubble.circle +bubble.circle.fill +bubble.fill +bubble.left +bubble.left.and.bubble.right +bubble.left.and.bubble.right.fill +bubble.left.and.exclamationmark.bubble.right +bubble.left.and.exclamationmark.bubble.right.fill +bubble.left.and.text.bubble.right +bubble.left.and.text.bubble.right.fill +bubble.left.circle +bubble.left.circle.fill +bubble.left.fill +bubble.middle.bottom +bubble.middle.bottom.fill +bubble.middle.top +bubble.middle.top.fill +bubble.right +bubble.right.circle +bubble.right.circle.fill +bubble.right.fill +bubbles.and.sparkles +bubbles.and.sparkles.fill +building +building.2 +building.2.crop.circle +building.2.crop.circle.fill +building.2.fill +building.columns +building.columns.circle +building.columns.circle.fill +building.columns.fill +building.fill +burn +burst +burst.fill +bus +bus.doubledecker +bus.doubledecker.fill +bus.fill +button.angledbottom.horizontal.left +button.angledbottom.horizontal.left.fill +button.angledbottom.horizontal.right +button.angledbottom.horizontal.right.fill +button.angledtop.vertical.left +button.angledtop.vertical.left.fill +button.angledtop.vertical.right +button.angledtop.vertical.right.fill +button.horizontal +button.horizontal.fill +button.horizontal.top +button.horizontal.top.fill +button.horizontal.top.press +button.horizontal.top.press.fill +button.programmable +button.programmable.square +button.programmable.square.fill +button.roundedbottom.horizontal +button.roundedbottom.horizontal.fill +button.roundedtop.horizontal +button.roundedtop.horizontal.fill +button.vertical.left +button.vertical.left.fill +button.vertical.left.press +button.vertical.left.press.fill +button.vertical.right +button.vertical.right.fill +button.vertical.right.press +button.vertical.right.press.fill +c.circle +c.circle.fill +c.square +c.square.fill +cabinet +cabinet.fill +cable.coaxial +cable.connector +cable.connector.horizontal +cable.connector.slash +cable.connector.video +cablecar +cablecar.fill +calendar +calendar.and.person +calendar.badge +calendar.badge.checkmark +calendar.badge.clock +calendar.badge.exclamationmark +calendar.badge.lock +calendar.badge.minus +calendar.badge.plus +calendar.circle +calendar.circle.fill +calendar.day.timeline.leading +calendar.day.timeline.leading.circle +calendar.day.timeline.leading.circle.fill +calendar.day.timeline.left +calendar.day.timeline.left.circle +calendar.day.timeline.left.circle.fill +calendar.day.timeline.right +calendar.day.timeline.right.circle +calendar.day.timeline.right.circle.fill +calendar.day.timeline.trailing +calendar.day.timeline.trailing.circle +calendar.day.timeline.trailing.circle.fill +camera +camera.aperture +camera.badge.clock +camera.badge.clock.fill +camera.badge.ellipsis +camera.badge.ellipsis.fill +camera.circle +camera.circle.fill +camera.fill +camera.fill.badge.ellipsis +camera.filters +camera.macro +camera.macro.circle +camera.macro.circle.fill +camera.macro.slash +camera.macro.slash.circle +camera.macro.slash.circle.fill +camera.metering.center.weighted +camera.metering.center.weighted.average +camera.metering.matrix +camera.metering.multispot +camera.metering.none +camera.metering.partial +camera.metering.spot +camera.metering.unknown +camera.on.rectangle +camera.on.rectangle.fill +camera.rotate +camera.rotate.fill +camera.sensor.tag.radiowaves.left.and.right +camera.sensor.tag.radiowaves.left.and.right.fill +camera.shutter.button +camera.shutter.button.fill +camera.viewfinder +camera.viewfinder.badge.automatic +candybarphone +capslock +capslock.fill +capsule +capsule.bottomhalf.filled +capsule.fill +capsule.inset.filled +capsule.lefthalf.filled +capsule.on.capsule +capsule.on.capsule.fill +capsule.on.rectangle +capsule.on.rectangle.fill +capsule.portrait +capsule.portrait.bottomhalf.filled +capsule.portrait.fill +capsule.portrait.inset.filled +capsule.portrait.lefthalf.filled +capsule.portrait.righthalf.filled +capsule.portrait.tophalf.filled +capsule.righthalf.filled +capsule.tophalf.filled +captions.bubble +captions.bubble.fill +car +car.2 +car.2.fill +car.badge.gearshape +car.badge.gearshape.fill +car.circle +car.circle.fill +car.ferry +car.ferry.fill +car.fill +car.front.waves.down +car.front.waves.down.fill +car.front.waves.left.and.right.and.up +car.front.waves.left.and.right.and.up.fill +car.front.waves.up +car.front.waves.up.fill +car.rear +car.rear.and.collision.road.lane +car.rear.and.collision.road.lane.slash +car.rear.and.tire.marks +car.rear.and.tire.marks.off +car.rear.and.tire.marks.slash +car.rear.fill +car.rear.hazardsign +car.rear.hazardsign.fill +car.rear.road.lane +car.rear.road.lane.dashed +car.rear.road.lane.dashed.arrowtriangle.2.outward +car.rear.road.lane.distance.1 +car.rear.road.lane.distance.1.and.gauge.open.with.lines.needle.67percent.and.arrowtriangle +car.rear.road.lane.distance.2 +car.rear.road.lane.distance.2.and.gauge.open.with.lines.needle.67percent.and.arrowtriangle +car.rear.road.lane.distance.3 +car.rear.road.lane.distance.3.and.gauge.open.with.lines.needle.67percent.and.arrowtriangle +car.rear.road.lane.distance.4 +car.rear.road.lane.distance.4.and.gauge.open.with.lines.needle.67percent.and.arrowtriangle +car.rear.road.lane.distance.5 +car.rear.road.lane.distance.5.and.gauge.open.with.lines.needle.67percent.and.arrowtriangle +car.rear.road.lane.off +car.rear.road.lane.wave.up +car.rear.tilt.road.lanes.curved.right +car.rear.waves.up +car.rear.waves.up.fill +car.side +car.side.air.circulate +car.side.air.circulate.fill +car.side.air.fresh +car.side.air.fresh.fill +car.side.and.exclamationmark +car.side.and.exclamationmark.fill +car.side.arrow.left.and.right +car.side.arrow.left.and.right.fill +car.side.arrowtriangle.down +car.side.arrowtriangle.down.fill +car.side.arrowtriangle.up +car.side.arrowtriangle.up.arrowtriangle.down +car.side.arrowtriangle.up.arrowtriangle.down.fill +car.side.arrowtriangle.up.fill +car.side.fill +car.side.front.open +car.side.front.open.crop +car.side.front.open.crop.fill +car.side.front.open.fill +car.side.hill.descent.control +car.side.hill.descent.control.fill +car.side.hill.down +car.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle +car.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle.fill +car.side.hill.down.fill +car.side.hill.up +car.side.hill.up.fill +car.side.lock +car.side.lock.fill +car.side.lock.open +car.side.lock.open.fill +car.side.rear.and.collision.and.car.side.front +car.side.rear.and.collision.and.car.side.front.and.arrow.forward +car.side.rear.and.collision.and.car.side.front.and.steeringwheel +car.side.rear.and.collision.and.car.side.front.slash +car.side.rear.and.exclamationmark.and.car.side.front +car.side.rear.and.exclamationmark.and.car.side.front.off +car.side.rear.and.wave.3.and.car.side.front +car.side.rear.crop.trunk.partition +car.side.rear.crop.trunk.partition.fill +car.side.rear.open +car.side.rear.open.crop +car.side.rear.open.crop.fill +car.side.rear.open.fill +car.side.rear.tow.hitch +car.side.rear.tow.hitch.fill +car.side.roof.cargo.carrier +car.side.roof.cargo.carrier.fill +car.side.roof.cargo.carrier.slash +car.side.roof.cargo.carrier.slash.fill +car.top.arrowtriangle.front.left +car.top.arrowtriangle.front.left.fill +car.top.arrowtriangle.front.right +car.top.arrowtriangle.front.right.fill +car.top.arrowtriangle.rear.left +car.top.arrowtriangle.rear.left.fill +car.top.arrowtriangle.rear.right +car.top.arrowtriangle.rear.right.fill +car.top.door.front.left.and.front.right.and.rear.left.and.rear.right.open +car.top.door.front.left.and.front.right.and.rear.left.and.rear.right.open.fill +car.top.door.front.left.and.front.right.and.rear.left.open +car.top.door.front.left.and.front.right.and.rear.left.open.fill +car.top.door.front.left.and.front.right.and.rear.right.open +car.top.door.front.left.and.front.right.and.rear.right.open.fill +car.top.door.front.left.and.front.right.open +car.top.door.front.left.and.front.right.open.fill +car.top.door.front.left.and.rear.left.and.rear.right.open +car.top.door.front.left.and.rear.left.and.rear.right.open.fill +car.top.door.front.left.and.rear.left.open +car.top.door.front.left.and.rear.left.open.fill +car.top.door.front.left.and.rear.right.open +car.top.door.front.left.and.rear.right.open.fill +car.top.door.front.left.open +car.top.door.front.left.open.fill +car.top.door.front.right.and.rear.left.and.rear.right.open +car.top.door.front.right.and.rear.left.and.rear.right.open.fill +car.top.door.front.right.and.rear.left.open +car.top.door.front.right.and.rear.left.open.fill +car.top.door.front.right.and.rear.right.open +car.top.door.front.right.and.rear.right.open.fill +car.top.door.front.right.open +car.top.door.front.right.open.fill +car.top.door.rear.left.and.rear.right.open +car.top.door.rear.left.and.rear.right.open.fill +car.top.door.rear.left.open +car.top.door.rear.left.open.fill +car.top.door.rear.right.open +car.top.door.rear.right.open.fill +car.top.door.sliding.left.open +car.top.door.sliding.left.open.fill +car.top.door.sliding.right.open +car.top.door.sliding.right.open.fill +car.top.front.radiowaves.front.left.and.front.and.front.right +car.top.front.radiowaves.front.left.and.front.and.front.right.fill +car.top.frontleft.arrowtriangle +car.top.frontleft.arrowtriangle.fill +car.top.frontright.arrowtriangle +car.top.frontright.arrowtriangle.fill +car.top.lane.dashed.arrowtriangle.inward +car.top.lane.dashed.arrowtriangle.inward.fill +car.top.lane.dashed.badge.steeringwheel +car.top.lane.dashed.badge.steeringwheel.fill +car.top.lane.dashed.departure.left +car.top.lane.dashed.departure.left.fill +car.top.lane.dashed.departure.left.slash +car.top.lane.dashed.departure.left.slash.fill +car.top.lane.dashed.departure.right +car.top.lane.dashed.departure.right.fill +car.top.lane.dashed.departure.right.slash +car.top.lane.dashed.departure.right.slash.fill +car.top.radiowaves.2.front.left.front.front.right +car.top.radiowaves.2.front.left.front.front.right.fill +car.top.radiowaves.2.rear.left.rear.rear.right +car.top.radiowaves.2.rear.left.rear.rear.right.fill +car.top.radiowaves.front +car.top.radiowaves.front.fill +car.top.radiowaves.rear +car.top.radiowaves.rear.fill +car.top.radiowaves.rear.left +car.top.radiowaves.rear.left.and.rear.right +car.top.radiowaves.rear.left.and.rear.right.fill +car.top.radiowaves.rear.left.car.top.front +car.top.radiowaves.rear.left.car.top.front.fill +car.top.radiowaves.rear.left.fill +car.top.radiowaves.rear.right +car.top.radiowaves.rear.right.badge.exclamationmark +car.top.radiowaves.rear.right.badge.exclamationmark.fill +car.top.radiowaves.rear.right.badge.xmark +car.top.radiowaves.rear.right.badge.xmark.fill +car.top.radiowaves.rear.right.car.top.front +car.top.radiowaves.rear.right.car.top.front.fill +car.top.radiowaves.rear.right.fill +car.top.rear.radiowaves.rear.left.and.rear.and.rear.right +car.top.rear.radiowaves.rear.left.and.rear.and.rear.right.fill +car.top.rearleft.arrowtriangle +car.top.rearleft.arrowtriangle.fill +car.top.rearright.arrowtriangle +car.top.rearright.arrowtriangle.fill +car.top.video.rear.left +car.top.video.rear.left.fill +car.top.video.rear.right +car.top.video.rear.right.fill +car.window.left +car.window.left.badge.exclamationmark +car.window.left.badge.lock +car.window.left.badge.xmark +car.window.left.exclamationmark +car.window.left.xmark +car.window.right +car.window.right.badge.exclamationmark +car.window.right.badge.lock +car.window.right.badge.xmark +car.window.right.exclamationmark +car.window.right.xmark +carbon.dioxide.cloud +carbon.dioxide.cloud.fill +carbon.monoxide.cloud +carbon.monoxide.cloud.fill +carrot +carrot.fill +carseat.left +carseat.left.1 +carseat.left.1.fill +carseat.left.2 +carseat.left.2.fill +carseat.left.3 +carseat.left.3.fill +carseat.left.and.heat.waves +carseat.left.and.heat.waves.fill +carseat.left.backrest.up.and.down +carseat.left.backrest.up.and.down.fill +carseat.left.fan +carseat.left.fan.fill +carseat.left.fill +carseat.left.forward.and.backward +carseat.left.forward.and.backward.fill +carseat.left.massage +carseat.left.massage.fill +carseat.left.up.and.down +carseat.left.up.and.down.fill +carseat.right +carseat.right.1 +carseat.right.1.fill +carseat.right.2 +carseat.right.2.fill +carseat.right.3 +carseat.right.3.fill +carseat.right.and.heat.waves +carseat.right.and.heat.waves.fill +carseat.right.backrest.up.and.down +carseat.right.backrest.up.and.down.fill +carseat.right.fan +carseat.right.fan.fill +carseat.right.fill +carseat.right.forward.and.backward +carseat.right.forward.and.backward.fill +carseat.right.massage +carseat.right.massage.fill +carseat.right.up.and.down +carseat.right.up.and.down.fill +cart +cart.badge.clock +cart.badge.clock.fill +cart.badge.minus +cart.badge.plus +cart.badge.questionmark +cart.circle +cart.circle.fill +cart.fill +cart.fill.badge.minus +cart.fill.badge.plus +cart.fill.badge.questionmark +case +case.fill +cat +cat.circle +cat.circle.fill +cat.fill +cedisign +cedisign.arrow.circlepath +cedisign.arrow.trianglehead.counterclockwise.rotate.90 +cedisign.bank.building +cedisign.bank.building.fill +cedisign.circle +cedisign.circle.fill +cedisign.gauge.chart.lefthalf.righthalf +cedisign.gauge.chart.leftthird.topthird.rightthird +cedisign.ring +cedisign.ring.dashed +cedisign.square +cedisign.square.fill +cellularbars +cellularbars.circle +cellularbars.circle.fill +centsign +centsign.arrow.circlepath +centsign.arrow.trianglehead.counterclockwise.rotate.90 +centsign.bank.building +centsign.bank.building.fill +centsign.circle +centsign.circle.fill +centsign.gauge.chart.lefthalf.righthalf +centsign.gauge.chart.leftthird.topthird.rightthird +centsign.ring +centsign.ring.dashed +centsign.square +centsign.square.fill +chair +chair.fill +chair.lounge +chair.lounge.fill +chandelier +chandelier.fill +character +character.bn +character.book.closed +character.book.closed.bn +character.book.closed.fill +character.book.closed.fill.bn +character.book.closed.fill.gu +character.book.closed.fill.kn +character.book.closed.fill.ml +character.book.closed.fill.mni +character.book.closed.fill.mr +character.book.closed.fill.or +character.book.closed.fill.pa +character.book.closed.fill.sat +character.book.closed.fill.si +character.book.closed.fill.ta +character.book.closed.fill.te +character.book.closed.gu +character.book.closed.kn +character.book.closed.ml +character.book.closed.mni +character.book.closed.mr +character.book.closed.or +character.book.closed.pa +character.book.closed.sat +character.book.closed.si +character.book.closed.ta +character.book.closed.te +character.bubble +character.bubble.bn +character.bubble.fill +character.bubble.fill.bn +character.bubble.fill.gu +character.bubble.fill.kn +character.bubble.fill.ml +character.bubble.fill.mni +character.bubble.fill.mr +character.bubble.fill.or +character.bubble.fill.pa +character.bubble.fill.sat +character.bubble.fill.si +character.bubble.fill.ta +character.bubble.fill.te +character.bubble.gu +character.bubble.kn +character.bubble.ml +character.bubble.mni +character.bubble.mr +character.bubble.or +character.bubble.pa +character.bubble.sat +character.bubble.si +character.bubble.ta +character.bubble.te +character.circle +character.circle.bn +character.circle.fill +character.circle.fill.bn +character.circle.fill.gu +character.circle.fill.kn +character.circle.fill.ml +character.circle.fill.mni +character.circle.fill.mr +character.circle.fill.or +character.circle.fill.pa +character.circle.fill.sat +character.circle.fill.si +character.circle.fill.ta +character.circle.fill.te +character.circle.gu +character.circle.kn +character.circle.ml +character.circle.mni +character.circle.mr +character.circle.or +character.circle.pa +character.circle.sat +character.circle.si +character.circle.ta +character.circle.te +character.cursor.ibeam +character.cursor.ibeam.bn +character.cursor.ibeam.gu +character.cursor.ibeam.kn +character.cursor.ibeam.ml +character.cursor.ibeam.mni +character.cursor.ibeam.mr +character.cursor.ibeam.or +character.cursor.ibeam.pa +character.cursor.ibeam.sat +character.cursor.ibeam.si +character.cursor.ibeam.ta +character.cursor.ibeam.te +character.duployan +character.gu +character.kn +character.magnify +character.magnify.bn +character.magnify.gu +character.magnify.kn +character.magnify.ml +character.magnify.mni +character.magnify.mr +character.magnify.or +character.magnify.pa +character.magnify.sat +character.magnify.si +character.magnify.ta +character.magnify.te +character.ml +character.mni +character.mr +character.or +character.pa +character.phonetic +character.sat +character.si +character.square +character.square.bn +character.square.fill +character.square.fill.bn +character.square.fill.gu +character.square.fill.kn +character.square.fill.ml +character.square.fill.mni +character.square.fill.mr +character.square.fill.or +character.square.fill.pa +character.square.fill.sat +character.square.fill.si +character.square.fill.ta +character.square.fill.te +character.square.gu +character.square.kn +character.square.ml +character.square.mni +character.square.mr +character.square.or +character.square.pa +character.square.sat +character.square.si +character.square.ta +character.square.te +character.sutton +character.ta +character.te +character.text.justify +character.text.justify.bn +character.text.justify.gu +character.text.justify.kn +character.text.justify.ml +character.text.justify.mni +character.text.justify.mr +character.text.justify.or +character.text.justify.pa +character.text.justify.sat +character.text.justify.si +character.text.justify.ta +character.text.justify.te +character.textbox +character.textbox.badge.sparkles +character.textbox.badge.sparkles.bn +character.textbox.badge.sparkles.gu +character.textbox.badge.sparkles.kn +character.textbox.badge.sparkles.ml +character.textbox.badge.sparkles.mni +character.textbox.badge.sparkles.mr +character.textbox.badge.sparkles.or +character.textbox.badge.sparkles.pa +character.textbox.badge.sparkles.sat +character.textbox.badge.sparkles.si +character.textbox.badge.sparkles.ta +character.textbox.badge.sparkles.te +character.textbox.bn +character.textbox.gu +character.textbox.kn +character.textbox.ml +character.textbox.mni +character.textbox.mr +character.textbox.or +character.textbox.pa +character.textbox.sat +character.textbox.si +character.textbox.ta +character.textbox.te +characters.lowercase +characters.lowercase.ru +characters.uppercase +characters.uppercase.ru +chart.bar +chart.bar.doc.horizontal +chart.bar.doc.horizontal.fill +chart.bar.fill +chart.bar.horizontal.page +chart.bar.horizontal.page.fill +chart.bar.xaxis +chart.bar.xaxis.ascending +chart.bar.xaxis.ascending.badge.clock +chart.bar.xaxis.descending +chart.bar.yaxis +chart.dots.scatter +chart.line.downtrend.xyaxis +chart.line.downtrend.xyaxis.circle +chart.line.downtrend.xyaxis.circle.fill +chart.line.flattrend.xyaxis +chart.line.flattrend.xyaxis.circle +chart.line.flattrend.xyaxis.circle.fill +chart.line.text.clipboard +chart.line.text.clipboard.fill +chart.line.uptrend.xyaxis +chart.line.uptrend.xyaxis.circle +chart.line.uptrend.xyaxis.circle.fill +chart.pie +chart.pie.fill +chart.xyaxis.line +checkerboard.rectangle +checkerboard.shield +checklist +checklist.checked +checklist.unchecked +checkmark +checkmark.app +checkmark.app.fill +checkmark.applewatch +checkmark.arrow.trianglehead.clockwise +checkmark.arrow.trianglehead.counterclockwise +checkmark.bubble +checkmark.bubble.fill +checkmark.circle +checkmark.circle.badge.airplane +checkmark.circle.badge.airplane.fill +checkmark.circle.badge.plus +checkmark.circle.badge.plus.fill +checkmark.circle.badge.questionmark +checkmark.circle.badge.questionmark.fill +checkmark.circle.badge.xmark +checkmark.circle.badge.xmark.fill +checkmark.circle.dotted +checkmark.circle.fill +checkmark.circle.trianglebadge.exclamationmark +checkmark.circle.trianglebadge.exclamationmark.fill +checkmark.diamond +checkmark.diamond.fill +checkmark.gobackward +checkmark.icloud +checkmark.icloud.fill +checkmark.message +checkmark.message.fill +checkmark.rectangle +checkmark.rectangle.fill +checkmark.rectangle.portrait +checkmark.rectangle.portrait.fill +checkmark.rectangle.stack +checkmark.rectangle.stack.fill +checkmark.seal +checkmark.seal.fill +checkmark.seal.text.page +checkmark.seal.text.page.fill +checkmark.shield +checkmark.shield.fill +checkmark.square +checkmark.square.fill +chevron.backward +chevron.backward.2 +chevron.backward.chevron.backward.dotted +chevron.backward.circle +chevron.backward.circle.fill +chevron.backward.square +chevron.backward.square.fill +chevron.backward.to.line +chevron.compact.backward +chevron.compact.down +chevron.compact.forward +chevron.compact.left +chevron.compact.left.chevron.compact.right +chevron.compact.right +chevron.compact.up +chevron.compact.up.chevron.compact.down +chevron.compact.up.chevron.compact.right.chevron.compact.down.chevron.compact.left +chevron.down +chevron.down.2 +chevron.down.circle +chevron.down.circle.fill +chevron.down.dotted.2 +chevron.down.forward.2 +chevron.down.forward.dotted.2 +chevron.down.right.2 +chevron.down.right.dotted.2 +chevron.down.square +chevron.down.square.fill +chevron.forward +chevron.forward.2 +chevron.forward.circle +chevron.forward.circle.fill +chevron.forward.dotted.chevron.forward +chevron.forward.square +chevron.forward.square.fill +chevron.forward.to.line +chevron.left +chevron.left.2 +chevron.left.chevron.left.dotted +chevron.left.chevron.right +chevron.left.circle +chevron.left.circle.fill +chevron.left.forwardslash.chevron.right +chevron.left.slash.chevron.right +chevron.left.square +chevron.left.square.fill +chevron.left.to.line +chevron.right +chevron.right.2 +chevron.right.circle +chevron.right.circle.fill +chevron.right.dotted.chevron.right +chevron.right.square +chevron.right.square.fill +chevron.right.to.line +chevron.up +chevron.up.2 +chevron.up.chevron.down +chevron.up.chevron.down.square +chevron.up.chevron.down.square.fill +chevron.up.chevron.right.chevron.down.chevron.left +chevron.up.circle +chevron.up.circle.fill +chevron.up.dotted.2 +chevron.up.forward.2 +chevron.up.forward.dotted.2 +chevron.up.right.2 +chevron.up.right.dotted.2 +chevron.up.square +chevron.up.square.fill +chineseyuanrenminbisign +chineseyuanrenminbisign.arrow.circlepath +chineseyuanrenminbisign.arrow.trianglehead.counterclockwise.rotate.90 +chineseyuanrenminbisign.bank.building +chineseyuanrenminbisign.bank.building.fill +chineseyuanrenminbisign.circle +chineseyuanrenminbisign.circle.fill +chineseyuanrenminbisign.gauge.chart.lefthalf.righthalf +chineseyuanrenminbisign.gauge.chart.leftthird.topthird.rightthird +chineseyuanrenminbisign.ring +chineseyuanrenminbisign.ring.dashed +chineseyuanrenminbisign.square +chineseyuanrenminbisign.square.fill +circle +circle.and.line.horizontal +circle.and.line.horizontal.fill +circle.badge.checkmark +circle.badge.checkmark.fill +circle.badge.exclamationmark +circle.badge.exclamationmark.fill +circle.badge.minus +circle.badge.minus.fill +circle.badge.plus +circle.badge.plus.fill +circle.badge.questionmark +circle.badge.questionmark.fill +circle.badge.xmark +circle.badge.xmark.fill +circle.bottomhalf.fill +circle.bottomhalf.filled +circle.bottomhalf.filled.inverse +circle.bottomrighthalf.checkered +circle.bottomrighthalf.pattern.checkered +circle.bottomthird.split +circle.circle +circle.circle.fill +circle.dashed +circle.dashed.inset.fill +circle.dashed.inset.filled +circle.dashed.rectangle +circle.dotted +circle.dotted.and.circle +circle.dotted.circle +circle.dotted.circle.fill +circle.fill +circle.fill.square.fill +circle.filled.ipad +circle.filled.ipad.fill +circle.filled.ipad.landscape +circle.filled.ipad.landscape.fill +circle.filled.iphone +circle.filled.iphone.fill +circle.filled.pattern.diagonalline.rectangle +circle.grid.2x1 +circle.grid.2x1.fill +circle.grid.2x1.left.filled +circle.grid.2x1.right.filled +circle.grid.2x2 +circle.grid.2x2.fill +circle.grid.2x2.topleft.checkmark.filled +circle.grid.3x3 +circle.grid.3x3.circle +circle.grid.3x3.circle.fill +circle.grid.3x3.fill +circle.grid.cross +circle.grid.cross.down.fill +circle.grid.cross.down.filled +circle.grid.cross.fill +circle.grid.cross.left.fill +circle.grid.cross.left.filled +circle.grid.cross.right.fill +circle.grid.cross.right.filled +circle.grid.cross.up.fill +circle.grid.cross.up.filled +circle.grid.hex +circle.grid.hex.fill +circle.hexagongrid +circle.hexagongrid.circle +circle.hexagongrid.circle.fill +circle.hexagongrid.fill +circle.hexagonpath +circle.hexagonpath.fill +circle.inset.filled +circle.lefthalf.fill +circle.lefthalf.filled +circle.lefthalf.filled.inverse +circle.lefthalf.filled.righthalf.striped.horizontal +circle.lefthalf.filled.righthalf.striped.horizontal.inverse +circle.lefthalf.striped.horizontal +circle.lefthalf.striped.horizontal.inverse +circle.on.square +circle.on.square.intersection.dotted +circle.on.square.merge +circle.rectangle.dashed +circle.rectangle.filled.pattern.diagonalline +circle.righthalf.fill +circle.righthalf.filled +circle.righthalf.filled.inverse +circle.slash +circle.slash.fill +circle.square +circle.square.fill +circle.tophalf.fill +circle.tophalf.filled +circle.tophalf.filled.inverse +circlebadge +circlebadge.2 +circlebadge.2.fill +circlebadge.fill +circles.hexagongrid +circles.hexagongrid.fill +circles.hexagonpath +circles.hexagonpath.fill +clear +clear.fill +clipboard +clipboard.fill +clock +clock.arrow.2.circlepath +clock.arrow.circlepath +clock.arrow.trianglehead.2.counterclockwise.rotate.90 +clock.arrow.trianglehead.clockwise.rotate.90.path.dotted +clock.arrow.trianglehead.counterclockwise.rotate.90 +clock.badge +clock.badge.airplane +clock.badge.airplane.fill +clock.badge.checkmark +clock.badge.checkmark.fill +clock.badge.exclamationmark +clock.badge.exclamationmark.fill +clock.badge.fill +clock.badge.questionmark +clock.badge.questionmark.fill +clock.badge.xmark +clock.badge.xmark.fill +clock.circle +clock.circle.fill +clock.fill +cloud +cloud.bolt +cloud.bolt.circle +cloud.bolt.circle.fill +cloud.bolt.fill +cloud.bolt.rain +cloud.bolt.rain.circle +cloud.bolt.rain.circle.fill +cloud.bolt.rain.fill +cloud.circle +cloud.circle.fill +cloud.drizzle +cloud.drizzle.circle +cloud.drizzle.circle.fill +cloud.drizzle.fill +cloud.fill +cloud.fog +cloud.fog.circle +cloud.fog.circle.fill +cloud.fog.fill +cloud.hail +cloud.hail.circle +cloud.hail.circle.fill +cloud.hail.fill +cloud.heavyrain +cloud.heavyrain.circle +cloud.heavyrain.circle.fill +cloud.heavyrain.fill +cloud.moon +cloud.moon.bolt +cloud.moon.bolt.circle +cloud.moon.bolt.circle.fill +cloud.moon.bolt.fill +cloud.moon.circle +cloud.moon.circle.fill +cloud.moon.fill +cloud.moon.rain +cloud.moon.rain.circle +cloud.moon.rain.circle.fill +cloud.moon.rain.fill +cloud.rain +cloud.rain.circle +cloud.rain.circle.fill +cloud.rain.fill +cloud.rainbow.crop +cloud.rainbow.crop.fill +cloud.rainbow.half +cloud.rainbow.half.fill +cloud.sleet +cloud.sleet.circle +cloud.sleet.circle.fill +cloud.sleet.fill +cloud.snow +cloud.snow.circle +cloud.snow.circle.fill +cloud.snow.fill +cloud.sun +cloud.sun.bolt +cloud.sun.bolt.circle +cloud.sun.bolt.circle.fill +cloud.sun.bolt.fill +cloud.sun.circle +cloud.sun.circle.fill +cloud.sun.fill +cloud.sun.rain +cloud.sun.rain.circle +cloud.sun.rain.circle.fill +cloud.sun.rain.fill +coat +coat.circle +coat.circle.fill +coat.fill +coloncurrencysign +coloncurrencysign.arrow.circlepath +coloncurrencysign.arrow.trianglehead.counterclockwise.rotate.90 +coloncurrencysign.bank.building +coloncurrencysign.bank.building.fill +coloncurrencysign.circle +coloncurrencysign.circle.fill +coloncurrencysign.gauge.chart.lefthalf.righthalf +coloncurrencysign.gauge.chart.leftthird.topthird.rightthird +coloncurrencysign.ring +coloncurrencysign.ring.dashed +coloncurrencysign.square +coloncurrencysign.square.fill +comb +comb.fill +command +command.circle +command.circle.fill +command.square +command.square.fill +compass.drawing +computermouse +computermouse.fill +cone +cone.fill +contact.sensor +contact.sensor.fill +contextualmenu.and.cursorarrow +contextualmenu.and.pointer.arrow +control +convertible.side +convertible.side.air.circulate +convertible.side.air.circulate.fill +convertible.side.air.fresh +convertible.side.air.fresh.fill +convertible.side.and.exclamationmark +convertible.side.and.exclamationmark.fill +convertible.side.arrow.left.and.right +convertible.side.arrow.left.and.right.fill +convertible.side.arrow.trianglehead.backward +convertible.side.arrow.trianglehead.backward.fill +convertible.side.arrow.trianglehead.forward +convertible.side.arrow.trianglehead.forward.and.backward +convertible.side.arrow.trianglehead.forward.and.backward.fill +convertible.side.arrow.trianglehead.forward.fill +convertible.side.arrowtriangle.down +convertible.side.arrowtriangle.down.fill +convertible.side.arrowtriangle.up +convertible.side.arrowtriangle.up.arrowtriangle.down +convertible.side.arrowtriangle.up.arrowtriangle.down.fill +convertible.side.arrowtriangle.up.fill +convertible.side.fill +convertible.side.front.open +convertible.side.front.open.crop +convertible.side.front.open.crop.fill +convertible.side.front.open.fill +convertible.side.hill.descent.control +convertible.side.hill.descent.control.fill +convertible.side.hill.down +convertible.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle +convertible.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle.fill +convertible.side.hill.down.fill +convertible.side.hill.up +convertible.side.hill.up.fill +convertible.side.lock +convertible.side.lock.fill +convertible.side.lock.open +convertible.side.lock.open.fill +cooktop +cooktop.fill +cpu +cpu.fill +creditcard +creditcard.and.123 +creditcard.and.numbers +creditcard.arrow.trianglehead.2.clockwise.rotate.90 +creditcard.circle +creditcard.circle.fill +creditcard.fill +creditcard.rewards +creditcard.rewards.fill +creditcard.trianglebadge.exclamationmark +creditcard.trianglebadge.exclamationmark.fill +creditcard.viewfinder +cricket.ball +cricket.ball.circle +cricket.ball.circle.fill +cricket.ball.fill +crop +crop.rotate +cross +cross.case +cross.case.circle +cross.case.circle.fill +cross.case.fill +cross.circle +cross.circle.fill +cross.fill +cross.vial +cross.vial.fill +crown +crown.fill +cruzeirosign +cruzeirosign.arrow.circlepath +cruzeirosign.arrow.trianglehead.counterclockwise.rotate.90 +cruzeirosign.bank.building +cruzeirosign.bank.building.fill +cruzeirosign.circle +cruzeirosign.circle.fill +cruzeirosign.gauge.chart.lefthalf.righthalf +cruzeirosign.gauge.chart.leftthird.topthird.rightthird +cruzeirosign.ring +cruzeirosign.ring.dashed +cruzeirosign.square +cruzeirosign.square.fill +cube +cube.box +cube.box.fill +cube.circle +cube.circle.fill +cube.fill +cube.transparent +cube.transparent.fill +cup.and.heat.waves +cup.and.heat.waves.fill +cup.and.saucer +cup.and.saucer.fill +curlybraces +curlybraces.square +curlybraces.square.fill +cursor.rays +cursorarrow +cursorarrow.and.square.on.square.dashed +cursorarrow.click +cursorarrow.click.2 +cursorarrow.click.badge.clock +cursorarrow.motionlines +cursorarrow.motionlines.click +cursorarrow.rays +cursorarrow.resize.north.east.south.east +cursorarrow.slash +cursorarrow.slash.square +cursorarrow.slash.square.fill +cursorarrow.square +cursorarrow.square.fill +curtains.closed +curtains.open +cylinder +cylinder.fill +cylinder.split.1x2 +cylinder.split.1x2.fill +d.circle +d.circle.fill +d.square +d.square.fill +danishkronesign +danishkronesign.arrow.circlepath +danishkronesign.arrow.trianglehead.counterclockwise.rotate.90 +danishkronesign.bank.building +danishkronesign.bank.building.fill +danishkronesign.circle +danishkronesign.circle.fill +danishkronesign.gauge.chart.lefthalf.righthalf +danishkronesign.gauge.chart.leftthird.topthird.rightthird +danishkronesign.ring +danishkronesign.ring.dashed +danishkronesign.square +danishkronesign.square.fill +decrease.indent +decrease.quotelevel +degreesign.celsius +degreesign.fahrenheit +dehumidifier +dehumidifier.fill +delete.backward +delete.backward.fill +delete.forward +delete.forward.fill +delete.left +delete.left.fill +delete.right +delete.right.fill +deskclock +deskclock.fill +desktopcomputer +desktopcomputer.and.arrow.down +desktopcomputer.and.macbook +desktopcomputer.badge.checkmark +desktopcomputer.badge.shield.checkmark +desktopcomputer.trianglebadge.exclamationmark +deskview +deskview.fill +dial +dial.fill +dial.high +dial.high.fill +dial.low +dial.low.fill +dial.max +dial.max.fill +dial.medium +dial.medium.fill +dial.min +dial.min.fill +diamond +diamond.bottomhalf.filled +diamond.circle +diamond.circle.fill +diamond.fill +diamond.inset.filled +diamond.lefthalf.filled +diamond.righthalf.filled +diamond.tophalf.filled +dice +dice.fill +die.face.1 +die.face.1.fill +die.face.2 +die.face.2.fill +die.face.3 +die.face.3.fill +die.face.4 +die.face.4.fill +die.face.5 +die.face.5.fill +die.face.6 +die.face.6.fill +digitalcrown +digitalcrown.arrow.clockwise +digitalcrown.arrow.clockwise.fill +digitalcrown.arrow.counterclockwise +digitalcrown.arrow.counterclockwise.fill +digitalcrown.fill +digitalcrown.horizontal +digitalcrown.horizontal.arrow.clockwise +digitalcrown.horizontal.arrow.clockwise.fill +digitalcrown.horizontal.arrow.counterclockwise +digitalcrown.horizontal.arrow.counterclockwise.fill +digitalcrown.horizontal.fill +digitalcrown.horizontal.press +digitalcrown.horizontal.press.fill +digitalcrown.press +digitalcrown.press.fill +directcurrent +dishwasher +dishwasher.circle +dishwasher.circle.fill +dishwasher.fill +display +display.2 +display.and.arrow.down +display.and.screwdriver +display.trianglebadge.exclamationmark +distribute.horizontal +distribute.horizontal.center +distribute.horizontal.center.fill +distribute.horizontal.fill +distribute.horizontal.left +distribute.horizontal.left.fill +distribute.horizontal.right +distribute.horizontal.right.fill +distribute.vertical +distribute.vertical.bottom +distribute.vertical.bottom.fill +distribute.vertical.center +distribute.vertical.center.fill +distribute.vertical.fill +distribute.vertical.top +distribute.vertical.top.fill +divide +divide.circle +divide.circle.fill +divide.square +divide.square.fill +doc +doc.append +doc.append.fill +doc.badge.arrow.up +doc.badge.arrow.up.fill +doc.badge.clock +doc.badge.clock.fill +doc.badge.ellipsis +doc.badge.gearshape +doc.badge.gearshape.fill +doc.badge.plus +doc.circle +doc.circle.fill +doc.fill +doc.fill.badge.ellipsis +doc.fill.badge.plus +doc.on.clipboard +doc.on.clipboard.fill +doc.on.doc +doc.on.doc.fill +doc.plaintext +doc.plaintext.fill +doc.questionmark +doc.questionmark.fill +doc.richtext +doc.richtext.fill +doc.text +doc.text.below.ecg +doc.text.below.ecg.fill +doc.text.fill +doc.text.fill.viewfinder +doc.text.image +doc.text.image.fill +doc.text.magnifyingglass +doc.text.viewfinder +doc.viewfinder +doc.viewfinder.fill +doc.zipper +dock.arrow.down.rectangle +dock.arrow.up.rectangle +dock.rectangle +document +document.badge.arrow.up +document.badge.arrow.up.fill +document.badge.clock +document.badge.clock.fill +document.badge.ellipsis +document.badge.ellipsis.fill +document.badge.gearshape +document.badge.gearshape.fill +document.badge.plus +document.badge.plus.fill +document.circle +document.circle.fill +document.fill +document.on.clipboard +document.on.clipboard.fill +document.on.document +document.on.document.fill +document.on.trash +document.on.trash.fill +document.viewfinder +document.viewfinder.fill +dog +dog.circle +dog.circle.fill +dog.fill +dollarsign +dollarsign.arrow.circlepath +dollarsign.arrow.trianglehead.counterclockwise.rotate.90 +dollarsign.bank.building +dollarsign.bank.building.fill +dollarsign.circle +dollarsign.circle.fill +dollarsign.gauge.chart.lefthalf.righthalf +dollarsign.gauge.chart.leftthird.topthird.rightthird +dollarsign.ring +dollarsign.ring.dashed +dollarsign.square +dollarsign.square.fill +dongsign +dongsign.arrow.circlepath +dongsign.arrow.trianglehead.counterclockwise.rotate.90 +dongsign.bank.building +dongsign.bank.building.fill +dongsign.circle +dongsign.circle.fill +dongsign.gauge.chart.lefthalf.righthalf +dongsign.gauge.chart.leftthird.topthird.rightthird +dongsign.ring +dongsign.ring.dashed +dongsign.square +dongsign.square.fill +door.french.closed +door.french.open +door.garage.closed +door.garage.closed.trianglebadge.exclamationmark +door.garage.double.bay.closed +door.garage.double.bay.closed.trianglebadge.exclamationmark +door.garage.double.bay.open +door.garage.double.bay.open.trianglebadge.exclamationmark +door.garage.open +door.garage.open.trianglebadge.exclamationmark +door.left.hand.closed +door.left.hand.open +door.right.hand.closed +door.right.hand.open +door.sliding.left.hand.closed +door.sliding.left.hand.open +door.sliding.right.hand.closed +door.sliding.right.hand.open +dot.arrowtriangles.up.right.down.left.circle +dot.car.top.radiowaves.2.rear.left.rear.rear.right +dot.car.top.radiowaves.2.rear.left.rear.rear.right.fill +dot.circle.and.cursorarrow +dot.circle.and.hand.point.up.left.fill +dot.circle.and.pointer.arrow +dot.circle.viewfinder +dot.crosshair +dot.radiowaves.forward +dot.radiowaves.left.and.right +dot.radiowaves.right +dot.radiowaves.up.forward +dot.scope +dot.scope.display +dot.scope.laptopcomputer +dot.square +dot.square.fill +dot.squareshape +dot.squareshape.fill +dot.squareshape.split.2x2 +dot.viewfinder +dots.and.line.vertical.and.cursorarrow.rectangle +dots.and.line.vertical.and.pointer.arrow.rectangle +dpad +dpad.down.fill +dpad.down.filled +dpad.fill +dpad.left.fill +dpad.left.filled +dpad.right.fill +dpad.right.filled +dpad.up.fill +dpad.up.filled +drone +drone.fill +drop +drop.circle +drop.circle.fill +drop.degreesign +drop.degreesign.fill +drop.degreesign.slash +drop.degreesign.slash.fill +drop.fill +drop.halffull +drop.keypad.rectangle +drop.keypad.rectangle.fill +drop.transmission +drop.triangle +drop.triangle.fill +dryer +dryer.circle +dryer.circle.fill +dryer.fill +duffle.bag +duffle.bag.fill +dumbbell +dumbbell.fill +e.circle +e.circle.fill +e.square +e.square.fill +ear +ear.and.waveform +ear.badge.checkmark +ear.badge.waveform +ear.fill +ear.trianglebadge.exclamationmark +earbud.left +earbud.right +earbuds +earbuds.bone.conduction +earbuds.bone.conduction.left +earbuds.bone.conduction.right +earbuds.case +earbuds.case.fill +earbuds.in.ear +earbuds.in.ear.left +earbuds.in.ear.right +earbuds.stemless +earbuds.stemless.left +earbuds.stemless.right +earpods +ecg.text.page +ecg.text.page.fill +eject +eject.circle +eject.circle.fill +eject.fill +electronic.toll.collection +electronic.toll.collection.rectangle +electronic.toll.collection.rectangle.fill +electronic.toll.collection.rectangle.slash +electronic.toll.collection.rectangle.slash.fill +electronic.toll.collection.rectangle.trianglebadge.exclamationmark +electronic.toll.collection.rectangle.trianglebadge.exclamationmark.fill +ellipses.bubble +ellipses.bubble.fill +ellipsis +ellipsis.bubble +ellipsis.bubble.fill +ellipsis.calendar +ellipsis.circle +ellipsis.circle.badge +ellipsis.circle.badge.fill +ellipsis.circle.fill +ellipsis.curlybraces +ellipsis.message +ellipsis.message.fill +ellipsis.rectangle +ellipsis.rectangle.fill +ellipsis.vertical.bubble +ellipsis.vertical.bubble.fill +ellipsis.viewfinder +engine.combustion +engine.combustion.badge.exclamationmark +engine.combustion.badge.exclamationmark.fill +engine.combustion.fill +engine.emission.and.drop.2.water.wave.below +engine.emission.and.exclamationmark +engine.emission.and.filter +entry.lever.keypad +entry.lever.keypad.fill +entry.lever.keypad.trianglebadge.exclamationmark +entry.lever.keypad.trianglebadge.exclamationmark.fill +envelope +envelope.and.arrow.3.down +envelope.and.arrow.3.down.fill +envelope.and.arrow.trianglehead.branch +envelope.and.arrow.trianglehead.branch.fill +envelope.and.hand.raised +envelope.and.hand.raised.fill +envelope.arrow.triangle.branch +envelope.arrow.triangle.branch.fill +envelope.badge +envelope.badge.fill +envelope.badge.minus +envelope.badge.minus.fill +envelope.badge.person.crop +envelope.badge.person.crop.fill +envelope.badge.plus +envelope.badge.plus.fill +envelope.badge.shield.half.filled +envelope.badge.shield.half.filled.fill +envelope.badge.shield.leadinghalf.fill +envelope.circle +envelope.circle.fill +envelope.fill +envelope.fill.badge.shield.trailinghalf.fill +envelope.front +envelope.front.fill +envelope.open +envelope.open.badge.clock +envelope.open.badge.clock.fill +envelope.open.fill +envelope.stack +envelope.stack.fill +environments +environments.circle +environments.circle.fill +environments.fill +environments.slash +environments.slash.circle +environments.slash.circle.fill +environments.slash.fill +equal +equal.circle +equal.circle.fill +equal.square +equal.square.fill +eraser +eraser.badge.xmark +eraser.badge.xmark.fill +eraser.fill +eraser.line.dashed +eraser.line.dashed.fill +eraser.slash +eraser.slash.fill +eraser.trianglebadge.exclamationmark +eraser.trianglebadge.exclamationmark.fill +escape +esim +esim.fill +eurosign +eurosign.arrow.circlepath +eurosign.arrow.trianglehead.counterclockwise.rotate.90 +eurosign.bank.building +eurosign.bank.building.fill +eurosign.circle +eurosign.circle.fill +eurosign.gauge.chart.lefthalf.righthalf +eurosign.gauge.chart.leftthird.topthird.rightthird +eurosign.ring +eurosign.ring.dashed +eurosign.square +eurosign.square.fill +eurozonesign +eurozonesign.arrow.circlepath +eurozonesign.arrow.trianglehead.counterclockwise.rotate.90 +eurozonesign.bank.building +eurozonesign.bank.building.fill +eurozonesign.circle +eurozonesign.circle.fill +eurozonesign.gauge.chart.lefthalf.righthalf +eurozonesign.gauge.chart.leftthird.topthird.rightthird +eurozonesign.ring +eurozonesign.ring.dashed +eurozonesign.square +eurozonesign.square.fill +ev.charger +ev.charger.arrowtriangle.left +ev.charger.arrowtriangle.left.fill +ev.charger.arrowtriangle.right +ev.charger.arrowtriangle.right.fill +ev.charger.exclamationmark +ev.charger.exclamationmark.fill +ev.charger.fill +ev.charger.slash +ev.charger.slash.fill +ev.plug.ac.gb.t +ev.plug.ac.gb.t.fill +ev.plug.ac.type.1 +ev.plug.ac.type.1.fill +ev.plug.ac.type.2 +ev.plug.ac.type.2.fill +ev.plug.dc.ccs1 +ev.plug.dc.ccs1.fill +ev.plug.dc.ccs2 +ev.plug.dc.ccs2.fill +ev.plug.dc.chademo +ev.plug.dc.chademo.fill +ev.plug.dc.gb.t +ev.plug.dc.gb.t.fill +ev.plug.dc.nacs +ev.plug.dc.nacs.fill +exclamationmark +exclamationmark.2 +exclamationmark.3 +exclamationmark.applewatch +exclamationmark.arrow.circlepath +exclamationmark.arrow.triangle.2.circlepath +exclamationmark.arrow.trianglehead.2.clockwise.rotate.90 +exclamationmark.arrow.trianglehead.counterclockwise.rotate.90 +exclamationmark.brakesignal +exclamationmark.bubble +exclamationmark.bubble.circle +exclamationmark.bubble.circle.fill +exclamationmark.bubble.fill +exclamationmark.circle +exclamationmark.circle.fill +exclamationmark.icloud +exclamationmark.icloud.fill +exclamationmark.lock +exclamationmark.lock.fill +exclamationmark.magnifyingglass +exclamationmark.message +exclamationmark.message.fill +exclamationmark.octagon +exclamationmark.octagon.fill +exclamationmark.questionmark +exclamationmark.shield +exclamationmark.shield.fill +exclamationmark.square +exclamationmark.square.fill +exclamationmark.tirepressure +exclamationmark.transmission +exclamationmark.triangle +exclamationmark.triangle.fill +exclamationmark.triangle.text.page +exclamationmark.triangle.text.page.fill +exclamationmark.warninglight +exclamationmark.warninglight.fill +externaldrive +externaldrive.badge.checkmark +externaldrive.badge.exclamationmark +externaldrive.badge.icloud +externaldrive.badge.minus +externaldrive.badge.person.crop +externaldrive.badge.plus +externaldrive.badge.questionmark +externaldrive.badge.timemachine +externaldrive.badge.wifi +externaldrive.badge.xmark +externaldrive.connected.to.line.below +externaldrive.connected.to.line.below.fill +externaldrive.fill +externaldrive.fill.badge.checkmark +externaldrive.fill.badge.exclamationmark +externaldrive.fill.badge.icloud +externaldrive.fill.badge.minus +externaldrive.fill.badge.person.crop +externaldrive.fill.badge.plus +externaldrive.fill.badge.questionmark +externaldrive.fill.badge.timemachine +externaldrive.fill.badge.wifi +externaldrive.fill.badge.xmark +externaldrive.fill.trianglebadge.exclamationmark +externaldrive.trianglebadge.exclamationmark +eye +eye.circle +eye.circle.fill +eye.fill +eye.half.closed +eye.half.closed.fill +eye.slash +eye.slash.circle +eye.slash.circle.fill +eye.slash.fill +eye.square +eye.square.fill +eye.trianglebadge.exclamationmark +eye.trianglebadge.exclamationmark.fill +eyebrow +eyedropper +eyedropper.full +eyedropper.halffull +eyeglasses +eyeglasses.slash +eyes +eyes.inverse +f.circle +f.circle.fill +f.cursive +f.cursive.circle +f.cursive.circle.fill +f.cursive.slash +f.square +f.square.fill +face.dashed +face.dashed.fill +face.smiling +face.smiling.fill +face.smiling.inverse +faceid +facemask +facemask.fill +fan +fan.and.light.ceiling +fan.and.light.ceiling.fill +fan.badge.arrow.up.and.down.and.arrow.left.and.right +fan.badge.arrow.up.and.down.and.arrow.left.and.right.fill +fan.badge.automatic +fan.badge.automatic.fill +fan.ceiling +fan.ceiling.fill +fan.circle +fan.circle.fill +fan.desk +fan.desk.fill +fan.fill +fan.floor +fan.floor.fill +fan.gauge.open +fan.oscillation +fan.oscillation.fill +fan.slash +fan.slash.fill +fanblades +fanblades.fill +fanblades.slash +fanblades.slash.fill +faxmachine +faxmachine.fill +ferry +ferry.fill +fiberchannel +fibrechannel +field.of.view.ultrawide +field.of.view.ultrawide.fill +field.of.view.wide +field.of.view.wide.fill +figure +figure.2 +figure.2.and.child.holdinghands +figure.2.arms.open +figure.2.circle +figure.2.circle.fill +figure.2.left.holdinghands +figure.2.right.holdinghands +figure.american.football +figure.american.football.circle +figure.american.football.circle.fill +figure.and.child.holdinghands +figure.archery +figure.archery.circle +figure.archery.circle.fill +figure.arms.open +figure.australian.football +figure.australian.football.circle +figure.australian.football.circle.fill +figure.badminton +figure.badminton.circle +figure.badminton.circle.fill +figure.barre +figure.barre.circle +figure.barre.circle.fill +figure.baseball +figure.baseball.circle +figure.baseball.circle.fill +figure.basketball +figure.basketball.circle +figure.basketball.circle.fill +figure.bowling +figure.bowling.circle +figure.bowling.circle.fill +figure.boxing +figure.boxing.circle +figure.boxing.circle.fill +figure.child +figure.child.and.lock +figure.child.and.lock.fill +figure.child.and.lock.open +figure.child.and.lock.open.fill +figure.child.circle +figure.child.circle.fill +figure.climbing +figure.climbing.circle +figure.climbing.circle.fill +figure.cooldown +figure.cooldown.circle +figure.cooldown.circle.fill +figure.core.training +figure.core.training.circle +figure.core.training.circle.fill +figure.cricket +figure.cricket.circle +figure.cricket.circle.fill +figure.cross.training +figure.cross.training.circle +figure.cross.training.circle.fill +figure.curling +figure.curling.circle +figure.curling.circle.fill +figure.dance +figure.dance.circle +figure.dance.circle.fill +figure.disc.sports +figure.disc.sports.circle +figure.disc.sports.circle.fill +figure.dress.line.vertical.figure +figure.elliptical +figure.elliptical.circle +figure.elliptical.circle.fill +figure.equestrian.sports +figure.equestrian.sports.circle +figure.equestrian.sports.circle.fill +figure.fall +figure.fall.circle +figure.fall.circle.fill +figure.fencing +figure.fencing.circle +figure.fencing.circle.fill +figure.field.hockey +figure.field.hockey.circle +figure.field.hockey.circle.fill +figure.fishing +figure.fishing.circle +figure.fishing.circle.fill +figure.flexibility +figure.flexibility.circle +figure.flexibility.circle.fill +figure.golf +figure.golf.circle +figure.golf.circle.fill +figure.gymnastics +figure.gymnastics.circle +figure.gymnastics.circle.fill +figure.hand.cycling +figure.hand.cycling.circle +figure.hand.cycling.circle.fill +figure.handball +figure.handball.circle +figure.handball.circle.fill +figure.highintensity.intervaltraining +figure.highintensity.intervaltraining.circle +figure.highintensity.intervaltraining.circle.fill +figure.hiking +figure.hiking.circle +figure.hiking.circle.fill +figure.hockey +figure.hockey.circle +figure.hockey.circle.fill +figure.hunting +figure.hunting.circle +figure.hunting.circle.fill +figure.ice.hockey +figure.ice.hockey.circle +figure.ice.hockey.circle.fill +figure.ice.skating +figure.ice.skating.circle +figure.ice.skating.circle.fill +figure.indoor.cycle +figure.indoor.cycle.circle +figure.indoor.cycle.circle.fill +figure.indoor.rowing +figure.indoor.rowing.circle +figure.indoor.rowing.circle.fill +figure.indoor.soccer +figure.indoor.soccer.circle +figure.indoor.soccer.circle.fill +figure.jumprope +figure.jumprope.circle +figure.jumprope.circle.fill +figure.kickboxing +figure.kickboxing.circle +figure.kickboxing.circle.fill +figure.lacrosse +figure.lacrosse.circle +figure.lacrosse.circle.fill +figure.martial.arts +figure.martial.arts.circle +figure.martial.arts.circle.fill +figure.mind.and.body +figure.mind.and.body.circle +figure.mind.and.body.circle.fill +figure.mixed.cardio +figure.mixed.cardio.circle +figure.mixed.cardio.circle.fill +figure.open.water.swim +figure.open.water.swim.circle +figure.open.water.swim.circle.fill +figure.outdoor.cycle +figure.outdoor.cycle.circle +figure.outdoor.cycle.circle.fill +figure.outdoor.rowing +figure.outdoor.rowing.circle +figure.outdoor.rowing.circle.fill +figure.outdoor.soccer +figure.outdoor.soccer.circle +figure.outdoor.soccer.circle.fill +figure.pickleball +figure.pickleball.circle +figure.pickleball.circle.fill +figure.pilates +figure.pilates.circle +figure.pilates.circle.fill +figure.play +figure.play.circle +figure.play.circle.fill +figure.pool.swim +figure.pool.swim.circle +figure.pool.swim.circle.fill +figure.racquetball +figure.racquetball.circle +figure.racquetball.circle.fill +figure.roll +figure.roll.circle +figure.roll.circle.fill +figure.roll.runningpace +figure.roll.runningpace.circle +figure.roll.runningpace.circle.fill +figure.rolling +figure.rolling.circle +figure.rolling.circle.fill +figure.rower +figure.rugby +figure.rugby.circle +figure.rugby.circle.fill +figure.run +figure.run.circle +figure.run.circle.fill +figure.run.square.stack +figure.run.square.stack.fill +figure.run.treadmill +figure.run.treadmill.circle +figure.run.treadmill.circle.fill +figure.sailing +figure.sailing.circle +figure.sailing.circle.fill +figure.seated.seatbelt +figure.seated.seatbelt.and.airbag.off +figure.seated.seatbelt.and.airbag.on +figure.seated.seatbelt.left.drive.seats.1 +figure.seated.seatbelt.left.drive.seats.1.1 +figure.seated.seatbelt.left.drive.seats.1.1.fill +figure.seated.seatbelt.left.drive.seats.1.2 +figure.seated.seatbelt.left.drive.seats.1.2.fill +figure.seated.seatbelt.left.drive.seats.1.fill +figure.seated.seatbelt.left.drive.seats.2 +figure.seated.seatbelt.left.drive.seats.2.2 +figure.seated.seatbelt.left.drive.seats.2.2.2 +figure.seated.seatbelt.left.drive.seats.2.2.2.fill +figure.seated.seatbelt.left.drive.seats.2.2.3 +figure.seated.seatbelt.left.drive.seats.2.2.3.fill +figure.seated.seatbelt.left.drive.seats.2.2.fill +figure.seated.seatbelt.left.drive.seats.2.3 +figure.seated.seatbelt.left.drive.seats.2.3.2 +figure.seated.seatbelt.left.drive.seats.2.3.2.fill +figure.seated.seatbelt.left.drive.seats.2.3.3 +figure.seated.seatbelt.left.drive.seats.2.3.3.fill +figure.seated.seatbelt.left.drive.seats.2.3.fill +figure.seated.seatbelt.left.drive.seats.2.fill +figure.seated.seatbelt.left.drive.seats.3 +figure.seated.seatbelt.left.drive.seats.3.3 +figure.seated.seatbelt.left.drive.seats.3.3.3 +figure.seated.seatbelt.left.drive.seats.3.3.3.fill +figure.seated.seatbelt.left.drive.seats.3.3.fill +figure.seated.seatbelt.left.drive.seats.3.fill +figure.seated.side +figure.seated.side.air.distribution.lower +figure.seated.side.air.distribution.middle +figure.seated.side.air.distribution.middle.and.lower +figure.seated.side.air.distribution.middle.and.lower.angled +figure.seated.side.air.distribution.upper +figure.seated.side.air.distribution.upper.angled.and.lower.angled +figure.seated.side.air.distribution.upper.angled.and.middle +figure.seated.side.air.distribution.upper.angled.and.middle.and.lower.angled +figure.seated.side.air.lower +figure.seated.side.air.upper +figure.seated.side.air.upper.and.lower +figure.seated.side.air.windshield +figure.seated.side.airbag.off +figure.seated.side.airbag.off.2 +figure.seated.side.airbag.on +figure.seated.side.airbag.on.2 +figure.seated.side.automatic +figure.seated.side.left +figure.seated.side.left.air.distribution.indirect +figure.seated.side.left.air.distribution.lower +figure.seated.side.left.air.distribution.lower.angled.and.upper.angled +figure.seated.side.left.air.distribution.middle +figure.seated.side.left.air.distribution.middle.and.lower +figure.seated.side.left.air.distribution.middle.and.lower.angled +figure.seated.side.left.air.distribution.upper +figure.seated.side.left.air.distribution.upper.and.middle.and.lower +figure.seated.side.left.air.distribution.upper.angled.and.dottedline.and.lower.angled +figure.seated.side.left.air.distribution.upper.angled.and.lower.angled +figure.seated.side.left.air.distribution.upper.angled.and.middle +figure.seated.side.left.air.distribution.upper.angled.and.middle.and.lower.angled +figure.seated.side.left.airbag.off +figure.seated.side.left.airbag.off.2 +figure.seated.side.left.airbag.on +figure.seated.side.left.airbag.on.2 +figure.seated.side.left.automatic +figure.seated.side.left.fan +figure.seated.side.left.steeringwheel +figure.seated.side.left.windshield.front.and.heat.waves +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.lower +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.middle +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.middle.and.lower +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.upper +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.upper.and.lower +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.upper.and.middle +figure.seated.side.left.windshield.front.and.heat.waves.air.distribution.upper.and.middle.and.lower +figure.seated.side.right +figure.seated.side.right.air.distribution.indirect +figure.seated.side.right.air.distribution.lower +figure.seated.side.right.air.distribution.lower.angled.and.upper.angled +figure.seated.side.right.air.distribution.middle +figure.seated.side.right.air.distribution.middle.and.lower +figure.seated.side.right.air.distribution.middle.and.lower.angled +figure.seated.side.right.air.distribution.upper +figure.seated.side.right.air.distribution.upper.and.middle.and.lower +figure.seated.side.right.air.distribution.upper.angled.and.dottedline.and.lower.angled +figure.seated.side.right.air.distribution.upper.angled.and.lower.angled +figure.seated.side.right.air.distribution.upper.angled.and.middle +figure.seated.side.right.air.distribution.upper.angled.and.middle.and.lower.angled +figure.seated.side.right.airbag.off +figure.seated.side.right.airbag.off.2 +figure.seated.side.right.airbag.on +figure.seated.side.right.airbag.on.2 +figure.seated.side.right.automatic +figure.seated.side.right.child.lap +figure.seated.side.right.fan +figure.seated.side.right.steeringwheel +figure.seated.side.right.windshield.front.and.heat.waves +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.lower +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.middle +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.middle.and.lower +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.upper +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.upper.and.lower +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.upper.and.middle +figure.seated.side.right.windshield.front.and.heat.waves.air.distribution.upper.and.middle.and.lower +figure.seated.side.windshield.front.and.heat.waves +figure.seated.side.windshield.front.and.heat.waves.air.distribution.lower +figure.seated.side.windshield.front.and.heat.waves.air.distribution.middle +figure.seated.side.windshield.front.and.heat.waves.air.distribution.middle.and.lower +figure.seated.side.windshield.front.and.heat.waves.air.distribution.upper +figure.seated.side.windshield.front.and.heat.waves.air.distribution.upper.and.lower +figure.seated.side.windshield.front.and.heat.waves.air.distribution.upper.and.middle +figure.seated.side.windshield.front.and.heat.waves.air.distribution.upper.and.middle.and.lower +figure.skateboarding +figure.skateboarding.circle +figure.skateboarding.circle.fill +figure.skating +figure.skiing.crosscountry +figure.skiing.crosscountry.circle +figure.skiing.crosscountry.circle.fill +figure.skiing.downhill +figure.skiing.downhill.circle +figure.skiing.downhill.circle.fill +figure.snowboarding +figure.snowboarding.circle +figure.snowboarding.circle.fill +figure.soccer +figure.socialdance +figure.socialdance.circle +figure.socialdance.circle.fill +figure.softball +figure.softball.circle +figure.softball.circle.fill +figure.squash +figure.squash.circle +figure.squash.circle.fill +figure.stair.stepper +figure.stair.stepper.circle +figure.stair.stepper.circle.fill +figure.stairs +figure.stairs.circle +figure.stairs.circle.fill +figure.stand +figure.stand.dress +figure.stand.dress.line.vertical.figure +figure.stand.line.dotted.figure.stand +figure.step.training +figure.step.training.circle +figure.step.training.circle.fill +figure.strengthtraining.functional +figure.strengthtraining.functional.circle +figure.strengthtraining.functional.circle.fill +figure.strengthtraining.traditional +figure.strengthtraining.traditional.circle +figure.strengthtraining.traditional.circle.fill +figure.surfing +figure.surfing.circle +figure.surfing.circle.fill +figure.table.tennis +figure.table.tennis.circle +figure.table.tennis.circle.fill +figure.taichi +figure.taichi.circle +figure.taichi.circle.fill +figure.tennis +figure.tennis.circle +figure.tennis.circle.fill +figure.track.and.field +figure.track.and.field.circle +figure.track.and.field.circle.fill +figure.volleyball +figure.volleyball.circle +figure.volleyball.circle.fill +figure.walk +figure.walk.arrival +figure.walk.circle +figure.walk.circle.fill +figure.walk.departure +figure.walk.diamond +figure.walk.diamond.fill +figure.walk.motion +figure.walk.motion.trianglebadge.exclamationmark +figure.walk.suitcase.rolling +figure.walk.suitcase.rolling.circle +figure.walk.suitcase.rolling.circle.fill +figure.walk.treadmill +figure.walk.treadmill.circle +figure.walk.treadmill.circle.fill +figure.walk.triangle +figure.walk.triangle.fill +figure.water.fitness +figure.water.fitness.circle +figure.water.fitness.circle.fill +figure.waterpolo +figure.waterpolo.circle +figure.waterpolo.circle.fill +figure.wave +figure.wave.circle +figure.wave.circle.fill +figure.wrestling +figure.wrestling.circle +figure.wrestling.circle.fill +figure.yoga +figure.yoga.circle +figure.yoga.circle.fill +filemenu.and.cursorarrow +filemenu.and.pointer.arrow +filemenu.and.selection +film +film.circle +film.circle.fill +film.fill +film.stack +film.stack.fill +finder +fire.extinguisher +fire.extinguisher.fill +fireplace +fireplace.fill +firewall +firewall.fill +fireworks +fish +fish.circle +fish.circle.fill +fish.fill +flag +flag.2.crossed +flag.2.crossed.circle +flag.2.crossed.circle.fill +flag.2.crossed.fill +flag.and.flag.filled.crossed +flag.badge.ellipsis +flag.badge.ellipsis.fill +flag.checkered +flag.checkered.2.crossed +flag.checkered.circle +flag.checkered.circle.fill +flag.circle +flag.circle.fill +flag.fill +flag.filled.and.flag.crossed +flag.pattern.checkered +flag.pattern.checkered.2.crossed +flag.pattern.checkered.circle +flag.pattern.checkered.circle.fill +flag.pattern.checkered.lc +flag.slash +flag.slash.circle +flag.slash.circle.fill +flag.slash.fill +flag.square +flag.square.fill +flame +flame.circle +flame.circle.fill +flame.fill +flame.gauge.open +flashlight.off.circle +flashlight.off.circle.fill +flashlight.off.fill +flashlight.on.circle +flashlight.on.circle.fill +flashlight.on.fill +flashlight.slash +flashlight.slash.circle +flashlight.slash.circle.fill +flask +flask.fill +fleuron +fleuron.fill +flip.horizontal +flip.horizontal.fill +flipphone +florinsign +florinsign.arrow.circlepath +florinsign.arrow.trianglehead.counterclockwise.rotate.90 +florinsign.bank.building +florinsign.bank.building.fill +florinsign.circle +florinsign.circle.fill +florinsign.gauge.chart.lefthalf.righthalf +florinsign.gauge.chart.leftthird.topthird.rightthird +florinsign.ring +florinsign.ring.dashed +florinsign.square +florinsign.square.fill +flowchart +flowchart.fill +fluid.batteryblock +fluid.brakesignal +fluid.coolant +fluid.transmission +fn +folder +folder.badge.gear +folder.badge.gearshape +folder.badge.minus +folder.badge.person.crop +folder.badge.plus +folder.badge.questionmark +folder.circle +folder.circle.fill +folder.fill +folder.fill.badge.gear +folder.fill.badge.gearshape +folder.fill.badge.minus +folder.fill.badge.person.crop +folder.fill.badge.plus +folder.fill.badge.questionmark +football +football.circle +football.circle.fill +football.fill +fork.knife +fork.knife.circle +fork.knife.circle.fill +formfitting.gamecontroller +formfitting.gamecontroller.fill +forward +forward.circle +forward.circle.fill +forward.end +forward.end.alt +forward.end.alt.fill +forward.end.circle +forward.end.circle.fill +forward.end.fill +forward.fill +forward.frame +forward.frame.fill +fossil.shell +fossil.shell.fill +francsign +francsign.arrow.circlepath +francsign.arrow.trianglehead.counterclockwise.rotate.90 +francsign.bank.building +francsign.bank.building.fill +francsign.circle +francsign.circle.fill +francsign.gauge.chart.lefthalf.righthalf +francsign.gauge.chart.leftthird.topthird.rightthird +francsign.ring +francsign.ring.dashed +francsign.square +francsign.square.fill +frying.pan +frying.pan.fill +fuel.filter.water +fuelpump +fuelpump.and.filter +fuelpump.arrowtriangle.left +fuelpump.arrowtriangle.left.fill +fuelpump.arrowtriangle.right +fuelpump.arrowtriangle.right.fill +fuelpump.circle +fuelpump.circle.fill +fuelpump.exclamationmark +fuelpump.exclamationmark.fill +fuelpump.fill +fuelpump.slash +fuelpump.slash.fill +fuelpump.thermometer +fuelpump.thermometer.fill +function +fx +g.circle +g.circle.fill +g.square +g.square.fill +gamecontroller +gamecontroller.circle +gamecontroller.circle.fill +gamecontroller.fill +gauge +gauge.badge.minus +gauge.badge.plus +gauge.chart.lefthalf.righthalf +gauge.chart.leftthird.topthird.rightthird +gauge.high +gauge.low +gauge.medium +gauge.medium.badge.minus +gauge.medium.badge.plus +gauge.open +gauge.open.righthalf.dotted.with.needle.and.arrow.trianglehead.backward +gauge.open.with.lines.needle.33percent +gauge.open.with.lines.needle.33percent.and.arrow.trianglehead.from.0percent.to.50percent +gauge.open.with.lines.needle.33percent.and.arrowtriangle +gauge.open.with.lines.needle.33percent.and.arrowtriangle.from.0percent.to.50percent +gauge.open.with.lines.needle.67percent.and.arrowtriangle +gauge.open.with.lines.needle.67percent.and.arrowtriangle.and.car +gauge.open.with.lines.needle.84percent.exclamation +gauge.with.dots.needle.0percent +gauge.with.dots.needle.100percent +gauge.with.dots.needle.33percent +gauge.with.dots.needle.50percent +gauge.with.dots.needle.67percent +gauge.with.dots.needle.bottom.0percent +gauge.with.dots.needle.bottom.100percent +gauge.with.dots.needle.bottom.50percent +gauge.with.dots.needle.bottom.50percent.badge.minus +gauge.with.dots.needle.bottom.50percent.badge.plus +gauge.with.needle +gauge.with.needle.fill +gear +gear.badge +gear.badge.checkmark +gear.badge.questionmark +gear.badge.xmark +gear.circle +gear.circle.fill +gearshape +gearshape.2 +gearshape.2.fill +gearshape.arrow.triangle.2.circlepath +gearshape.arrow.trianglehead.2.clockwise.rotate.90 +gearshape.circle +gearshape.circle.fill +gearshape.fill +gearshift.layout.sixspeed +gift +gift.circle +gift.circle.fill +gift.fill +giftcard +giftcard.fill +globe +globe.americas +globe.americas.fill +globe.asia.australia +globe.asia.australia.fill +globe.badge.chevron.backward +globe.badge.clock +globe.badge.clock.fill +globe.central.south.asia +globe.central.south.asia.fill +globe.desk +globe.desk.fill +globe.europe.africa +globe.europe.africa.fill +globe.fill +glowplug +gobackward +gobackward.10 +gobackward.15 +gobackward.30 +gobackward.45 +gobackward.5 +gobackward.60 +gobackward.75 +gobackward.90 +gobackward.minus +goforward +goforward.10 +goforward.15 +goforward.30 +goforward.45 +goforward.5 +goforward.60 +goforward.75 +goforward.90 +goforward.plus +graduationcap +graduationcap.circle +graduationcap.circle.fill +graduationcap.fill +graph.2d +graph.3d +greaterthan +greaterthan.circle +greaterthan.circle.fill +greaterthan.square +greaterthan.square.fill +greaterthanorequalto +greaterthanorequalto.circle +greaterthanorequalto.circle.fill +greaterthanorequalto.square +greaterthanorequalto.square.fill +greetingcard +greetingcard.fill +grid +grid.circle +grid.circle.fill +guaranisign +guaranisign.arrow.circlepath +guaranisign.arrow.trianglehead.counterclockwise.rotate.90 +guaranisign.bank.building +guaranisign.bank.building.fill +guaranisign.circle +guaranisign.circle.fill +guaranisign.gauge.chart.lefthalf.righthalf +guaranisign.gauge.chart.leftthird.topthird.rightthird +guaranisign.ring +guaranisign.ring.dashed +guaranisign.square +guaranisign.square.fill +guidepoint.horizontal +guidepoint.vertical +guidepoint.vertical.arrowtriangle.forward +guidepoint.vertical.numbers +guitars +guitars.fill +gym.bag +gym.bag.fill +gyroscope +h.circle +h.circle.fill +h.square +h.square.fill +h.square.fill.on.square.fill +h.square.on.square +h.square.on.square.fill +hammer +hammer.circle +hammer.circle.fill +hammer.fill +hand.app +hand.app.fill +hand.draw +hand.draw.badge.ellipsis +hand.draw.badge.ellipsis.fill +hand.draw.fill +hand.palm.facing +hand.palm.facing.fill +hand.pinch +hand.pinch.fill +hand.point.down +hand.point.down.fill +hand.point.left +hand.point.left.fill +hand.point.right +hand.point.right.fill +hand.point.up +hand.point.up.braille +hand.point.up.braille.badge.ellipsis +hand.point.up.braille.badge.ellipsis.fill +hand.point.up.braille.fill +hand.point.up.fill +hand.point.up.left +hand.point.up.left.and.text +hand.point.up.left.and.text.fill +hand.point.up.left.fill +hand.raised +hand.raised.app +hand.raised.app.fill +hand.raised.brakesignal +hand.raised.brakesignal.slash +hand.raised.circle +hand.raised.circle.fill +hand.raised.fill +hand.raised.fingers.spread +hand.raised.fingers.spread.fill +hand.raised.palm.facing +hand.raised.palm.facing.fill +hand.raised.slash +hand.raised.slash.fill +hand.raised.square +hand.raised.square.fill +hand.raised.square.on.square +hand.raised.square.on.square.fill +hand.rays +hand.rays.fill +hand.tap +hand.tap.fill +hand.thumbsdown +hand.thumbsdown.circle +hand.thumbsdown.circle.fill +hand.thumbsdown.fill +hand.thumbsdown.filled.hand.thumbsup +hand.thumbsdown.hand.thumbsup +hand.thumbsdown.hand.thumbsup.fill +hand.thumbsdown.hand.thumbsup.filled +hand.thumbsdown.slash +hand.thumbsdown.slash.fill +hand.thumbsup +hand.thumbsup.circle +hand.thumbsup.circle.fill +hand.thumbsup.fill +hand.thumbsup.slash +hand.thumbsup.slash.fill +hand.wave +hand.wave.fill +handbag +handbag.circle +handbag.circle.fill +handbag.fill +handbag.sensor.tag.radiowaves.left.and.right +handbag.sensor.tag.radiowaves.left.and.right.fill +hands.and.sparkles +hands.and.sparkles.fill +hands.clap +hands.clap.fill +hands.sparkles +hands.sparkles.fill +hanger +hare +hare.circle +hare.circle.fill +hare.fill +hat.cap +hat.cap.fill +hat.widebrim +hat.widebrim.fill +hazardsign +hazardsign.fill +head.profile.arrow.forward.and.vision.pro +head.profile.arrow.forward.and.visionpro +head.profile.vision.pro.remove +headlight.daytime +headlight.daytime.fill +headlight.fog +headlight.fog.fill +headlight.high.beam +headlight.high.beam.fill +headlight.low.beam +headlight.low.beam.fill +headphones +headphones.circle +headphones.circle.fill +headphones.dots +headphones.over.ear +headphones.sensor.tag.radiowaves.left.and.right +headphones.sensor.tag.radiowaves.left.and.right.fill +headphones.slash +headset +headset.circle +headset.circle.fill +hearingaid.ear +hearingdevice.and.signal.meter +hearingdevice.and.signal.meter.fill +hearingdevice.ear +hearingdevice.ear.fill +heart +heart.badge.bolt +heart.badge.bolt.fill +heart.badge.bolt.slash +heart.badge.bolt.slash.fill +heart.circle +heart.circle.fill +heart.fill +heart.gauge.open +heart.rectangle +heart.rectangle.fill +heart.slash +heart.slash.circle +heart.slash.circle.fill +heart.slash.fill +heart.square +heart.square.fill +heart.text.clipboard +heart.text.clipboard.fill +heart.text.square +heart.text.square.fill +heat.element.windshield +heat.waves +heat.waves.and.fan +heat.waves.circle +heat.waves.circle.fill +heat.waves.gauge.open +heater.vertical +heater.vertical.fill +helm +helmet +helmet.fill +hexagon +hexagon.bottomhalf.filled +hexagon.fill +hexagon.lefthalf.filled +hexagon.righthalf.filled +hexagon.tophalf.filled +hifireceiver +hifireceiver.fill +hifispeaker +hifispeaker.2 +hifispeaker.2.badge.minus +hifispeaker.2.badge.minus.fill +hifispeaker.2.badge.plus +hifispeaker.2.badge.plus.fill +hifispeaker.2.fill +hifispeaker.and.appletv +hifispeaker.and.appletv.fill +hifispeaker.and.homepod +hifispeaker.and.homepod.badge.minus +hifispeaker.and.homepod.badge.minus.fill +hifispeaker.and.homepod.badge.plus +hifispeaker.and.homepod.badge.plus.fill +hifispeaker.and.homepod.fill +hifispeaker.and.homepod.mini +hifispeaker.and.homepod.mini.badge.minus +hifispeaker.and.homepod.mini.badge.minus.fill +hifispeaker.and.homepod.mini.badge.plus +hifispeaker.and.homepod.mini.badge.plus.fill +hifispeaker.and.homepod.mini.fill +hifispeaker.and.homepodmini +hifispeaker.and.homepodmini.fill +hifispeaker.arrow.forward +hifispeaker.arrow.forward.fill +hifispeaker.badge.minus +hifispeaker.badge.minus.fill +hifispeaker.badge.plus +hifispeaker.badge.plus.fill +hifispeaker.fill +highlighter +highlighter.badge.ellipsis +hockey.puck +hockey.puck.circle +hockey.puck.circle.fill +hockey.puck.fill +hold.brakesignal +homekit +homepod +homepod.2 +homepod.2.badge.minus +homepod.2.badge.minus.fill +homepod.2.badge.plus +homepod.2.badge.plus.fill +homepod.2.fill +homepod.and.appletv +homepod.and.appletv.fill +homepod.and.homepod.mini +homepod.and.homepod.mini.badge.minus +homepod.and.homepod.mini.badge.minus.fill +homepod.and.homepod.mini.badge.plus +homepod.and.homepod.mini.badge.plus.fill +homepod.and.homepod.mini.fill +homepod.and.homepodmini +homepod.and.homepodmini.fill +homepod.arrow.forward +homepod.arrow.forward.fill +homepod.badge.checkmark +homepod.badge.checkmark.fill +homepod.badge.minus +homepod.badge.minus.fill +homepod.badge.plus +homepod.badge.plus.fill +homepod.fill +homepod.mini +homepod.mini.2 +homepod.mini.2.badge.minus +homepod.mini.2.badge.minus.fill +homepod.mini.2.badge.plus +homepod.mini.2.badge.plus.fill +homepod.mini.2.fill +homepod.mini.and.appletv +homepod.mini.and.appletv.fill +homepod.mini.arrow.forward +homepod.mini.arrow.forward.fill +homepod.mini.badge.checkmark +homepod.mini.badge.checkmark.fill +homepod.mini.badge.minus +homepod.mini.badge.minus.fill +homepod.mini.badge.plus +homepod.mini.badge.plus.fill +homepod.mini.fill +homepodmini +homepodmini.2 +homepodmini.2.fill +homepodmini.and.appletv +homepodmini.and.appletv.fill +homepodmini.fill +horn +horn.blast +horn.blast.fill +horn.fill +hourglass +hourglass.and.lock +hourglass.badge.eye +hourglass.badge.lock +hourglass.badge.plus +hourglass.bottomhalf.fill +hourglass.bottomhalf.filled +hourglass.circle +hourglass.circle.fill +hourglass.tophalf.fill +hourglass.tophalf.filled +house +house.and.flag +house.and.flag.circle +house.and.flag.circle.fill +house.and.flag.fill +house.badge.exclamationmark +house.badge.exclamationmark.fill +house.badge.wifi +house.badge.wifi.fill +house.circle +house.circle.fill +house.fill +house.lodge +house.lodge.circle +house.lodge.circle.fill +house.lodge.fill +house.slash +house.slash.fill +hryvniasign +hryvniasign.arrow.circlepath +hryvniasign.arrow.trianglehead.counterclockwise.rotate.90 +hryvniasign.bank.building +hryvniasign.bank.building.fill +hryvniasign.circle +hryvniasign.circle.fill +hryvniasign.gauge.chart.lefthalf.righthalf +hryvniasign.gauge.chart.leftthird.topthird.rightthird +hryvniasign.ring +hryvniasign.ring.dashed +hryvniasign.square +hryvniasign.square.fill +humidifier +humidifier.and.droplets +humidifier.and.droplets.fill +humidifier.and.ellipsis +humidifier.and.ellipsis.fill +humidifier.fill +humidity +humidity.fill +hurricane +hurricane.circle +hurricane.circle.fill +hydrogen +hydrogen.circle +hydrogen.circle.fill +hydrogen.square +hydrogen.square.fill +i.circle +i.circle.fill +i.square +i.square.fill +icloud +icloud.and.arrow.down +icloud.and.arrow.down.fill +icloud.and.arrow.up +icloud.and.arrow.up.fill +icloud.circle +icloud.circle.fill +icloud.dashed +icloud.fill +icloud.slash +icloud.slash.fill +icloud.square +icloud.square.fill +increase.indent +increase.quotelevel +indianrupeesign +indianrupeesign.arrow.circlepath +indianrupeesign.arrow.trianglehead.counterclockwise.rotate.90 +indianrupeesign.bank.building +indianrupeesign.bank.building.fill +indianrupeesign.circle +indianrupeesign.circle.fill +indianrupeesign.gauge.chart.lefthalf.righthalf +indianrupeesign.gauge.chart.leftthird.topthird.rightthird +indianrupeesign.ring +indianrupeesign.ring.dashed +indianrupeesign.square +indianrupeesign.square.fill +infinity +infinity.circle +infinity.circle.fill +info +info.bubble +info.bubble.fill +info.circle +info.circle.fill +info.circle.text.page +info.circle.text.page.fill +info.square +info.square.fill +info.triangle +info.triangle.fill +info.windshield +inhaler +inhaler.fill +inset.filled.applewatch.case +inset.filled.bottomhalf.rectangle +inset.filled.bottomhalf.rectangle.portrait +inset.filled.bottomhalf.tophalf.rectangle +inset.filled.bottomleading.bottomtrailing.rectangle +inset.filled.bottomleading.rectangle +inset.filled.bottomleading.rectangle.portrait +inset.filled.bottomleft.bottomright.rectangle +inset.filled.bottomleft.rectangle +inset.filled.bottomleft.rectangle.portrait +inset.filled.bottomright.rectangle +inset.filled.bottomright.rectangle.portrait +inset.filled.bottomthird.rectangle +inset.filled.bottomthird.rectangle.portrait +inset.filled.bottomthird.square +inset.filled.bottomtrailing.rectangle +inset.filled.bottomtrailing.rectangle.portrait +inset.filled.capsule +inset.filled.capsule.portrait +inset.filled.center.rectangle +inset.filled.center.rectangle.badge.plus +inset.filled.center.rectangle.portrait +inset.filled.circle +inset.filled.circle.dashed +inset.filled.circle.slash +inset.filled.diamond +inset.filled.leadinghalf.arrow.leading.rectangle +inset.filled.leadinghalf.rectangle +inset.filled.leadinghalf.rectangle.portrait +inset.filled.leadinghalf.toptrailing.bottomtrailing.rectangle +inset.filled.leadinghalf.trailinghalf.rectangle +inset.filled.leadingthird.rectangle +inset.filled.leadingthird.rectangle.portrait +inset.filled.leadingthird.square +inset.filled.lefthalf.arrow.left.rectangle +inset.filled.lefthalf.rectangle +inset.filled.lefthalf.rectangle.portrait +inset.filled.lefthalf.righthalf.rectangle +inset.filled.lefthalf.topright.bottomright.rectangle +inset.filled.leftthird.middlethird.rightthird.rectangle +inset.filled.leftthird.rectangle +inset.filled.leftthird.rectangle.portrait +inset.filled.leftthird.square +inset.filled.oval +inset.filled.oval.portrait +inset.filled.pano +inset.filled.rectangle +inset.filled.rectangle.and.cursorarrow +inset.filled.rectangle.and.person.filled +inset.filled.rectangle.and.person.filled.circle +inset.filled.rectangle.and.person.filled.circle.fill +inset.filled.rectangle.and.person.filled.slash +inset.filled.rectangle.and.pointer.arrow +inset.filled.rectangle.badge.record +inset.filled.rectangle.on.rectangle +inset.filled.rectangle.portrait +inset.filled.righthalf.arrow.right.rectangle +inset.filled.righthalf.lefthalf.rectangle +inset.filled.righthalf.rectangle +inset.filled.righthalf.rectangle.portrait +inset.filled.rightthird.rectangle +inset.filled.rightthird.rectangle.portrait +inset.filled.rightthird.square +inset.filled.square +inset.filled.square.dashed +inset.filled.tophalf.bottomhalf.rectangle +inset.filled.tophalf.bottomleft.bottomright.rectangle +inset.filled.tophalf.rectangle +inset.filled.tophalf.rectangle.portrait +inset.filled.topleading.bottomleading.trailinghalf.rectangle +inset.filled.topleading.rectangle +inset.filled.topleading.rectangle.portrait +inset.filled.topleft.bottomleft.righthalf.rectangle +inset.filled.topleft.rectangle +inset.filled.topleft.rectangle.portrait +inset.filled.topleft.topright.bottomhalf.rectangle +inset.filled.topleft.topright.bottomleft.bottomright.rectangle +inset.filled.topright.rectangle +inset.filled.topright.rectangle.portrait +inset.filled.topthird.middlethird.bottomthird.rectangle +inset.filled.topthird.rectangle +inset.filled.topthird.rectangle.portrait +inset.filled.topthird.square +inset.filled.toptrailing.rectangle +inset.filled.toptrailing.rectangle.portrait +inset.filled.trailinghalf.arrow.trailing.rectangle +inset.filled.trailinghalf.leadinghalf.rectangle +inset.filled.trailinghalf.rectangle +inset.filled.trailinghalf.rectangle.portrait +inset.filled.trailingthird.rectangle +inset.filled.trailingthird.rectangle.portrait +inset.filled.trailingthird.square +inset.filled.triangle +inset.filled.tv +internaldrive +internaldrive.fill +ipad +ipad.and.arrow.forward +ipad.and.iphone +ipad.and.iphone.slash +ipad.badge.checkmark +ipad.badge.exclamationmark +ipad.badge.location +ipad.badge.play +ipad.case +ipad.case.and.iphone.case +ipad.gen1 +ipad.gen1.badge.exclamationmark +ipad.gen1.badge.location +ipad.gen1.badge.play +ipad.gen1.crop.homebutton.circle +ipad.gen1.landscape +ipad.gen1.landscape.badge.exclamationmark +ipad.gen1.landscape.badge.location +ipad.gen1.landscape.badge.play +ipad.gen1.landscape.slash +ipad.gen1.sizes +ipad.gen1.slash +ipad.gen2 +ipad.gen2.badge.exclamationmark +ipad.gen2.badge.location +ipad.gen2.badge.play +ipad.gen2.landscape +ipad.gen2.landscape.badge.exclamationmark +ipad.gen2.landscape.badge.location +ipad.gen2.landscape.badge.play +ipad.gen2.landscape.slash +ipad.gen2.sizes +ipad.gen2.slash +ipad.homebutton +ipad.homebutton.badge.play +ipad.homebutton.landscape +ipad.homebutton.landscape.badge.play +ipad.landscape +ipad.landscape.and.applewatch +ipad.landscape.and.iphone +ipad.landscape.and.iphone.slash +ipad.landscape.and.ipod +ipad.landscape.badge.exclamationmark +ipad.landscape.badge.location +ipad.landscape.badge.play +ipad.rear.camera +ipad.sizes +iphone +iphone.and.arrow.forward +iphone.and.arrow.forward.inward +iphone.and.arrow.forward.outward +iphone.and.arrow.left.and.arrow.right +iphone.and.arrow.left.and.arrow.right.inward +iphone.and.arrow.right.inward +iphone.and.arrow.right.outward +iphone.and.ipod +iphone.and.vision.pro +iphone.app.switcher +iphone.badge.checkmark +iphone.badge.exclamationmark +iphone.badge.location +iphone.badge.play +iphone.case +iphone.circle +iphone.circle.fill +iphone.crop.circle +iphone.dock.motorized.viewfinder +iphone.gen1 +iphone.gen1.and.arrow.left +iphone.gen1.badge.exclamationmark +iphone.gen1.badge.location +iphone.gen1.badge.play +iphone.gen1.circle +iphone.gen1.circle.fill +iphone.gen1.crop.circle +iphone.gen1.crop.homebutton.circle +iphone.gen1.landscape +iphone.gen1.landscape.slash +iphone.gen1.motion +iphone.gen1.radiowaves.left.and.right +iphone.gen1.radiowaves.left.and.right.circle +iphone.gen1.radiowaves.left.and.right.circle.fill +iphone.gen1.sizes +iphone.gen1.slash +iphone.gen1.slash.circle +iphone.gen1.slash.circle.fill +iphone.gen2 +iphone.gen2.and.arrow.left.and.arrow.right.inward +iphone.gen2.badge.exclamationmark +iphone.gen2.badge.location +iphone.gen2.badge.play +iphone.gen2.circle +iphone.gen2.circle.fill +iphone.gen2.crop.circle +iphone.gen2.landscape +iphone.gen2.landscape.slash +iphone.gen2.motion +iphone.gen2.radiowaves.left.and.right +iphone.gen2.radiowaves.left.and.right.circle +iphone.gen2.radiowaves.left.and.right.circle.fill +iphone.gen2.sizes +iphone.gen2.slash +iphone.gen2.slash.circle +iphone.gen2.slash.circle.fill +iphone.gen3 +iphone.gen3.and.arrow.left.and.arrow.right.inward +iphone.gen3.badge.exclamationmark +iphone.gen3.badge.location +iphone.gen3.badge.play +iphone.gen3.circle +iphone.gen3.circle.fill +iphone.gen3.crop.circle +iphone.gen3.landscape +iphone.gen3.landscape.slash +iphone.gen3.motion +iphone.gen3.radiowaves.left.and.right +iphone.gen3.radiowaves.left.and.right.circle +iphone.gen3.radiowaves.left.and.right.circle.fill +iphone.gen3.sizes +iphone.gen3.slash +iphone.gen3.slash.circle +iphone.gen3.slash.circle.fill +iphone.homebutton +iphone.homebutton.badge.play +iphone.homebutton.circle +iphone.homebutton.circle.fill +iphone.homebutton.landscape +iphone.homebutton.radiowaves.left.and.right +iphone.homebutton.radiowaves.left.and.right.circle +iphone.homebutton.radiowaves.left.and.right.circle.fill +iphone.homebutton.slash +iphone.homebutton.slash.circle +iphone.homebutton.slash.circle.fill +iphone.landscape +iphone.motion +iphone.pattern.diagonalline +iphone.pattern.diagonalline.on.rectangle.portrait.dashed +iphone.radiowaves.left.and.right +iphone.radiowaves.left.and.right.circle +iphone.radiowaves.left.and.right.circle.fill +iphone.rear.camera +iphone.sizes +iphone.slash +iphone.slash.circle +iphone.slash.circle.fill +iphone.smartbatterycase.gen1 +iphone.smartbatterycase.gen2 +ipod +ipod.and.applewatch +ipod.and.vision.pro +ipod.shuffle.gen1 +ipod.shuffle.gen2 +ipod.shuffle.gen3 +ipod.shuffle.gen4 +ipod.touch +ipod.touch.landscape +ipod.touch.slash +ipodshuffle.gen1 +ipodshuffle.gen2 +ipodshuffle.gen3 +ipodshuffle.gen4 +ipodtouch +ipodtouch.landscape +ipodtouch.slash +italic +ivfluid.bag +ivfluid.bag.fill +j.circle +j.circle.fill +j.square +j.square.fill +j.square.fill.on.square.fill +j.square.on.square +j.square.on.square.fill +jacket +jacket.circle +jacket.circle.fill +jacket.fill +jacket.sensor.tag.radiowaves.left.and.right +jacket.sensor.tag.radiowaves.left.and.right.fill +k +k.circle +k.circle.fill +k.square +k.square.fill +kashida.arabic +key +key.2.on.ring +key.2.on.ring.fill +key.car.radiowaves.forward +key.car.radiowaves.forward.fill +key.car.side +key.car.side.fill +key.card +key.card.fill +key.circle +key.circle.fill +key.convertible.side +key.convertible.side.fill +key.fill +key.horizontal +key.horizontal.fill +key.icloud +key.icloud.fill +key.radiowaves.forward +key.radiowaves.forward.fill +key.radiowaves.forward.slash +key.radiowaves.forward.slash.fill +key.sensor.tag.radiowaves.left.and.right +key.sensor.tag.radiowaves.left.and.right.fill +key.shield +key.shield.fill +key.slash +key.slash.fill +key.suv.side +key.suv.side.fill +key.truck.pickup.side +key.truck.pickup.side.fill +key.viewfinder +keyboard +keyboard.badge.ellipsis +keyboard.badge.ellipsis.fill +keyboard.badge.eye +keyboard.badge.eye.fill +keyboard.chevron.compact.down +keyboard.chevron.compact.down.fill +keyboard.chevron.compact.left +keyboard.chevron.compact.left.fill +keyboard.fill +keyboard.macwindow +keyboard.onehanded.left +keyboard.onehanded.left.fill +keyboard.onehanded.right +keyboard.onehanded.right.fill +kipsign +kipsign.arrow.circlepath +kipsign.arrow.trianglehead.counterclockwise.rotate.90 +kipsign.bank.building +kipsign.bank.building.fill +kipsign.circle +kipsign.circle.fill +kipsign.gauge.chart.lefthalf.righthalf +kipsign.gauge.chart.leftthird.topthird.rightthird +kipsign.ring +kipsign.ring.dashed +kipsign.square +kipsign.square.fill +kph +kph.circle +kph.circle.fill +l.button.roundedbottom.horizontal +l.button.roundedbottom.horizontal.fill +l.circle +l.circle.fill +l.joystick +l.joystick.down +l.joystick.down.fill +l.joystick.fill +l.joystick.press.down +l.joystick.press.down.fill +l.joystick.tilt.down +l.joystick.tilt.down.fill +l.joystick.tilt.left +l.joystick.tilt.left.fill +l.joystick.tilt.right +l.joystick.tilt.right.fill +l.joystick.tilt.up +l.joystick.tilt.up.fill +l.rectangle.roundedbottom +l.rectangle.roundedbottom.fill +l.square +l.square.fill +l1.button.roundedbottom.horizontal +l1.button.roundedbottom.horizontal.fill +l1.circle +l1.circle.fill +l1.rectangle.roundedbottom +l1.rectangle.roundedbottom.fill +l2.button.angledtop.vertical.left +l2.button.angledtop.vertical.left.fill +l2.button.roundedtop.horizontal +l2.button.roundedtop.horizontal.fill +l2.circle +l2.circle.fill +l2.rectangle.roundedtop +l2.rectangle.roundedtop.fill +l3.button.angledbottom.horizontal.left +l3.button.angledbottom.horizontal.left.fill +l4.button.horizontal +l4.button.horizontal.fill +ladybug +ladybug.circle +ladybug.circle.fill +ladybug.fill +ladybug.slash +ladybug.slash.circle +ladybug.slash.circle.fill +ladybug.slash.fill +lamp.ceiling +lamp.ceiling.fill +lamp.ceiling.inverse +lamp.desk +lamp.desk.fill +lamp.floor +lamp.floor.fill +lamp.table +lamp.table.fill +lane +lanyardcard +lanyardcard.fill +laptopcomputer +laptopcomputer.and.arrow.down +laptopcomputer.and.ipad +laptopcomputer.and.iphone +laptopcomputer.badge.checkmark +laptopcomputer.slash +laptopcomputer.trianglebadge.exclamationmark +largecircle.fill.circle +larisign +larisign.arrow.circlepath +larisign.arrow.trianglehead.counterclockwise.rotate.90 +larisign.bank.building +larisign.bank.building.fill +larisign.circle +larisign.circle.fill +larisign.gauge.chart.lefthalf.righthalf +larisign.gauge.chart.leftthird.topthird.rightthird +larisign.ring +larisign.ring.dashed +larisign.square +larisign.square.fill +laser.burst +lasso +lasso.and.sparkles +lasso.badge.sparkles +lasso.sparkles +latch.2.case +latch.2.case.fill +laurel.leading +laurel.leading.laurel.trailing +laurel.trailing +lb.button.roundedbottom.horizontal +lb.button.roundedbottom.horizontal.fill +lb.circle +lb.circle.fill +lb.rectangle.roundedbottom +lb.rectangle.roundedbottom.fill +leaf +leaf.arrow.circlepath +leaf.arrow.triangle.circlepath +leaf.arrow.trianglehead.clockwise +leaf.circle +leaf.circle.fill +leaf.fill +left +left.circle +left.circle.fill +lessthan +lessthan.circle +lessthan.circle.fill +lessthan.square +lessthan.square.fill +lessthanorequalto +lessthanorequalto.circle +lessthanorequalto.circle.fill +lessthanorequalto.square +lessthanorequalto.square.fill +level +level.fill +licenseplate +licenseplate.fill +lifepreserver +lifepreserver.fill +light.beacon.max +light.beacon.max.fill +light.beacon.min +light.beacon.min.fill +light.cylindrical.ceiling +light.cylindrical.ceiling.fill +light.cylindrical.ceiling.inverse +light.max +light.min +light.overhead.left +light.overhead.left.fill +light.overhead.right +light.overhead.right.fill +light.panel +light.panel.fill +light.recessed +light.recessed.3 +light.recessed.3.fill +light.recessed.3.inverse +light.recessed.fill +light.recessed.inverse +light.ribbon +light.ribbon.fill +light.strip.2 +light.strip.2.fill +lightbulb +lightbulb.2 +lightbulb.2.fill +lightbulb.circle +lightbulb.circle.fill +lightbulb.fill +lightbulb.led +lightbulb.led.fill +lightbulb.led.wide +lightbulb.led.wide.fill +lightbulb.max +lightbulb.max.fill +lightbulb.min +lightbulb.min.badge.exclamationmark +lightbulb.min.badge.exclamationmark.fill +lightbulb.min.fill +lightbulb.slash +lightbulb.slash.fill +lightrail +lightrail.fill +lightspectrum.horizontal +lightswitch.off +lightswitch.off.fill +lightswitch.off.square +lightswitch.off.square.fill +lightswitch.on +lightswitch.on.fill +lightswitch.on.square +lightswitch.on.square.fill +line.2.horizontal.decrease.circle +line.2.horizontal.decrease.circle.fill +line.3.crossed.swirl.circle +line.3.crossed.swirl.circle.fill +line.3.horizontal +line.3.horizontal.button.angledtop.vertical.right +line.3.horizontal.button.angledtop.vertical.right.fill +line.3.horizontal.circle +line.3.horizontal.circle.fill +line.3.horizontal.decrease +line.3.horizontal.decrease.circle +line.3.horizontal.decrease.circle.fill +line.diagonal +line.diagonal.arrow +line.diagonal.trianglehead.up.right +line.diagonal.trianglehead.up.right.left.down +line.horizontal.2.decrease.circle +line.horizontal.2.decrease.circle.fill +line.horizontal.3 +line.horizontal.3.circle +line.horizontal.3.circle.fill +line.horizontal.3.decrease +line.horizontal.3.decrease.circle +line.horizontal.3.decrease.circle.fill +line.horizontal.star.fill.line.horizontal +lines.measurement.horizontal +lines.measurement.horizontal.aligned.bottom +lines.measurement.vertical +lineweight +link +link.badge.plus +link.circle +link.circle.fill +link.icloud +link.icloud.fill +lirasign +lirasign.arrow.circlepath +lirasign.arrow.trianglehead.counterclockwise.rotate.90 +lirasign.bank.building +lirasign.bank.building.fill +lirasign.circle +lirasign.circle.fill +lirasign.gauge.chart.lefthalf.righthalf +lirasign.gauge.chart.leftthird.topthird.rightthird +lirasign.ring +lirasign.ring.dashed +lirasign.square +lirasign.square.fill +list.and.film +list.bullet +list.bullet.badge.ellipsis +list.bullet.below.rectangle +list.bullet.circle +list.bullet.circle.fill +list.bullet.clipboard +list.bullet.clipboard.fill +list.bullet.indent +list.bullet.rectangle +list.bullet.rectangle.fill +list.bullet.rectangle.portrait +list.bullet.rectangle.portrait.fill +list.clipboard +list.clipboard.fill +list.dash +list.dash.badge.ellipsis +list.dash.header.rectangle +list.dash.header.rectangle.fill +list.number +list.number.badge.ellipsis +list.star +list.triangle +livephoto +livephoto.badge.a +livephoto.badge.automatic +livephoto.play +livephoto.slash +lizard +lizard.circle +lizard.circle.fill +lizard.fill +lm.button.horizontal +lm.button.horizontal.fill +location +location.app +location.app.fill +location.circle +location.circle.fill +location.fill +location.fill.viewfinder +location.magnifyingglass +location.north +location.north.circle +location.north.circle.fill +location.north.fill +location.north.line +location.north.line.fill +location.slash +location.slash.circle +location.slash.circle.fill +location.slash.fill +location.square +location.square.fill +location.viewfinder +lock +lock.app.dashed +lock.applewatch +lock.badge.checkmark +lock.badge.checkmark.fill +lock.badge.clock +lock.badge.clock.fill +lock.badge.xmark +lock.badge.xmark.fill +lock.circle +lock.circle.dotted +lock.circle.fill +lock.desktopcomputer +lock.display +lock.doc +lock.doc.fill +lock.document +lock.document.fill +lock.fill +lock.heart +lock.heart.fill +lock.icloud +lock.icloud.fill +lock.ipad +lock.iphone +lock.laptopcomputer +lock.open +lock.open.applewatch +lock.open.desktopcomputer +lock.open.display +lock.open.fill +lock.open.ipad +lock.open.iphone +lock.open.laptopcomputer +lock.open.rotation +lock.open.trianglebadge.exclamationmark +lock.open.trianglebadge.exclamationmark.fill +lock.rectangle +lock.rectangle.dashed +lock.rectangle.fill +lock.rectangle.on.rectangle +lock.rectangle.on.rectangle.dashed +lock.rectangle.on.rectangle.fill +lock.rectangle.stack +lock.rectangle.stack.fill +lock.rotation +lock.rotation.open +lock.shield +lock.shield.fill +lock.slash +lock.slash.fill +lock.square +lock.square.dashed +lock.square.fill +lock.square.stack +lock.square.stack.fill +lock.trianglebadge.exclamationmark +lock.trianglebadge.exclamationmark.fill +logo.playstation +logo.xbox +long.text.page.and.pencil +long.text.page.and.pencil.fill +loupe +lsb.button.angledbottom.horizontal.left +lsb.button.angledbottom.horizontal.left.fill +lt.button.roundedtop.horizontal +lt.button.roundedtop.horizontal.fill +lt.circle +lt.circle.fill +lt.rectangle.roundedtop +lt.rectangle.roundedtop.fill +lungs +lungs.fill +m.circle +m.circle.fill +m.square +m.square.fill +m1.button.horizontal +m1.button.horizontal.fill +m2.button.horizontal +m2.button.horizontal.fill +m3.button.horizontal +m3.button.horizontal.fill +m4.button.horizontal +m4.button.horizontal.fill +macbook +macbook.and.applewatch +macbook.and.ipad +macbook.and.iphone +macbook.and.ipod +macbook.and.vision.pro +macbook.and.visionpro +macbook.badge.checkmark +macbook.badge.exclamationmark +macbook.badge.shield.checkmark +macbook.gen1 +macbook.gen1.sizes +macbook.gen2 +macbook.gen2.sizes +macbook.sizes +macbook.slash +macbook.trianglebadge.exclamationmark +macmini +macmini.badge.checkmark +macmini.badge.checkmark.fill +macmini.fill +macmini.gen2 +macmini.gen2.fill +macmini.gen3 +macmini.gen3.fill +macpro.gen1 +macpro.gen1.fill +macpro.gen2 +macpro.gen2.fill +macpro.gen3 +macpro.gen3.badge.ckeckmark +macpro.gen3.badge.ckeckmark.fill +macpro.gen3.fill +macpro.gen3.server +macstudio +macstudio.badge.checkmark +macstudio.badge.checkmark.fill +macstudio.fill +macwindow +macwindow.and.cursorarrow +macwindow.and.pointer.arrow +macwindow.badge.plus +macwindow.on.rectangle +macwindow.stack +magazine +magazine.fill +magicmouse +magicmouse.fill +magnifyingglass +magnifyingglass.circle +magnifyingglass.circle.fill +magsafe.batterypack +magsafe.batterypack.fill +mail +mail.and.text.magnifyingglass +mail.fill +mail.stack +mail.stack.fill +malaysianringgitsign +malaysianringgitsign.arrow.trianglehead.counterclockwise.rotate.90 +malaysianringgitsign.bank.building +malaysianringgitsign.bank.building.fill +malaysianringgitsign.circle +malaysianringgitsign.circle.fill +malaysianringgitsign.gauge.chart.lefthalf.righthalf +malaysianringgitsign.gauge.chart.leftthird.topthird.rightthird +malaysianringgitsign.ring +malaysianringgitsign.ring.dashed +malaysianringgitsign.square +malaysianringgitsign.square.fill +manatsign +manatsign.arrow.circlepath +manatsign.arrow.trianglehead.counterclockwise.rotate.90 +manatsign.bank.building +manatsign.bank.building.fill +manatsign.circle +manatsign.circle.fill +manatsign.gauge.chart.lefthalf.righthalf +manatsign.gauge.chart.leftthird.topthird.rightthird +manatsign.ring +manatsign.ring.dashed +manatsign.square +manatsign.square.fill +map +map.circle +map.circle.fill +map.fill +mappin +mappin.and.ellipse +mappin.and.ellipse.circle +mappin.and.ellipse.circle.fill +mappin.circle +mappin.circle.fill +mappin.slash +mappin.slash.circle +mappin.slash.circle.fill +mappin.square +mappin.square.fill +matter.logo +mecca +medal +medal.fill +medal.star +medal.star.fill +mediastick +medical.thermometer +medical.thermometer.fill +megaphone +megaphone.fill +memories +memories.badge.checkmark +memories.badge.minus +memories.badge.plus +memories.badge.xmark +memories.slash +memorychip +memorychip.fill +menubar.arrow.down.rectangle +menubar.arrow.up.rectangle +menubar.dock.rectangle +menubar.dock.rectangle.badge.record +menubar.rectangle +menucard +menucard.fill +message +message.and.waveform +message.and.waveform.fill +message.badge +message.badge.circle +message.badge.circle.fill +message.badge.fill +message.badge.filled.fill +message.badge.waveform +message.badge.waveform.fill +message.circle +message.circle.fill +message.fill +metronome +metronome.fill +mic +mic.and.signal.meter +mic.and.signal.meter.fill +mic.badge.plus +mic.badge.xmark +mic.circle +mic.circle.fill +mic.fill +mic.fill.badge.plus +mic.fill.badge.xmark +mic.slash +mic.slash.circle +mic.slash.circle.fill +mic.slash.fill +mic.square +mic.square.fill +microbe +microbe.circle +microbe.circle.fill +microbe.fill +microphone +microphone.and.signal.meter +microphone.and.signal.meter.fill +microphone.badge.ellipsis +microphone.badge.ellipsis.fill +microphone.badge.plus +microphone.badge.plus.fill +microphone.badge.xmark +microphone.badge.xmark.fill +microphone.circle +microphone.circle.fill +microphone.fill +microphone.slash +microphone.slash.circle +microphone.slash.circle.fill +microphone.slash.fill +microphone.square +microphone.square.fill +microwave +microwave.fill +millsign +millsign.arrow.circlepath +millsign.arrow.trianglehead.counterclockwise.rotate.90 +millsign.bank.building +millsign.bank.building.fill +millsign.circle +millsign.circle.fill +millsign.gauge.chart.lefthalf.righthalf +millsign.gauge.chart.leftthird.topthird.rightthird +millsign.ring +millsign.ring.dashed +millsign.square +millsign.square.fill +minus +minus.arrow.trianglehead.clockwise +minus.arrow.trianglehead.counterclockwise +minus.circle +minus.circle.fill +minus.diamond +minus.diamond.fill +minus.forwardslash.plus +minus.magnifyingglass +minus.plus.and.fluid.batteryblock +minus.plus.batteryblock +minus.plus.batteryblock.exclamationmark +minus.plus.batteryblock.exclamationmark.fill +minus.plus.batteryblock.fill +minus.plus.batteryblock.slash +minus.plus.batteryblock.slash.fill +minus.plus.batteryblock.stack +minus.plus.batteryblock.stack.arrowtriangle.left +minus.plus.batteryblock.stack.arrowtriangle.left.fill +minus.plus.batteryblock.stack.arrowtriangle.right +minus.plus.batteryblock.stack.arrowtriangle.right.and.arrowtriangle.left +minus.plus.batteryblock.stack.arrowtriangle.right.and.arrowtriangle.left.fill +minus.plus.batteryblock.stack.arrowtriangle.right.fill +minus.plus.batteryblock.stack.exclamationmark +minus.plus.batteryblock.stack.exclamationmark.fill +minus.plus.batteryblock.stack.fill +minus.plus.lines.measurement.horizontal.aligned.bottom +minus.rectangle +minus.rectangle.fill +minus.rectangle.portrait +minus.rectangle.portrait.fill +minus.slash.plus +minus.square +minus.square.fill +mirror.side.left +mirror.side.left.and.arrow.turn.down.right +mirror.side.left.and.heat.waves +mirror.side.right +mirror.side.right.and.arrow.turn.down.left +mirror.side.right.and.heat.waves +moon +moon.circle +moon.circle.fill +moon.dust +moon.dust.circle +moon.dust.circle.fill +moon.dust.fill +moon.fill +moon.haze +moon.haze.circle +moon.haze.circle.fill +moon.haze.fill +moon.road.lanes +moon.stars +moon.stars.circle +moon.stars.circle.fill +moon.stars.fill +moon.zzz +moon.zzz.fill +moonphase.first.quarter +moonphase.first.quarter.inverse +moonphase.full.moon +moonphase.full.moon.inverse +moonphase.last.quarter +moonphase.last.quarter.inverse +moonphase.new.moon +moonphase.new.moon.inverse +moonphase.waning.crescent +moonphase.waning.crescent.inverse +moonphase.waning.gibbous +moonphase.waning.gibbous.inverse +moonphase.waxing.crescent +moonphase.waxing.crescent.inverse +moonphase.waxing.gibbous +moonphase.waxing.gibbous.inverse +moonrise +moonrise.circle +moonrise.circle.fill +moonrise.fill +moonset +moonset.circle +moonset.circle.fill +moonset.fill +moped +moped.fill +mosaic +mosaic.fill +motorcycle +motorcycle.fill +mount +mount.fill +mountain.2 +mountain.2.circle +mountain.2.circle.fill +mountain.2.fill +mouth +mouth.fill +move.3d +movieclapper +movieclapper.fill +mph +mph.circle +mph.circle.fill +mug +mug.fill +multiply +multiply.circle +multiply.circle.fill +multiply.square +multiply.square.fill +music.house +music.house.fill +music.mic +music.mic.circle +music.mic.circle.fill +music.microphone +music.microphone.circle +music.microphone.circle.fill +music.note +music.note.arrow.trianglehead.clockwise +music.note.house +music.note.house.fill +music.note.list +music.note.slash +music.note.square.stack +music.note.square.stack.fill +music.note.tv +music.note.tv.fill +music.pages +music.pages.fill +music.quarternote.3 +mustache +mustache.fill +n.circle +n.circle.fill +n.square +n.square.fill +nairasign +nairasign.arrow.circlepath +nairasign.arrow.trianglehead.counterclockwise.rotate.90 +nairasign.bank.building +nairasign.bank.building.fill +nairasign.circle +nairasign.circle.fill +nairasign.gauge.chart.lefthalf.righthalf +nairasign.gauge.chart.leftthird.topthird.rightthird +nairasign.ring +nairasign.ring.dashed +nairasign.square +nairasign.square.fill +network +network.badge.shield.half.filled +network.slash +newspaper +newspaper.circle +newspaper.circle.fill +newspaper.fill +norwegiankronesign +norwegiankronesign.arrow.circlepath +norwegiankronesign.arrow.trianglehead.counterclockwise.rotate.90 +norwegiankronesign.bank.building +norwegiankronesign.bank.building.fill +norwegiankronesign.circle +norwegiankronesign.circle.fill +norwegiankronesign.gauge.chart.lefthalf.righthalf +norwegiankronesign.gauge.chart.leftthird.topthird.rightthird +norwegiankronesign.ring +norwegiankronesign.ring.dashed +norwegiankronesign.square +norwegiankronesign.square.fill +nose +nose.fill +nosign +nosign.app +nosign.app.fill +nosign.badge.clock +note +note.text +note.text.badge.plus +notequal +notequal.circle +notequal.circle.fill +notequal.square +notequal.square.fill +number +number.circle +number.circle.fill +number.square +number.square.fill +numbers +numbers.bn +numbers.gu +numbers.km +numbers.kn +numbers.ml +numbers.mni +numbers.mr +numbers.my +numbers.or +numbers.pa +numbers.rectangle +numbers.rectangle.fill +numbers.sat +numbers.te +numbersign +o.circle +o.circle.fill +o.square +o.square.fill +oar.2.crossed +oar.2.crossed.circle +oar.2.crossed.circle.fill +octagon +octagon.bottomhalf.filled +octagon.fill +octagon.lefthalf.filled +octagon.righthalf.filled +octagon.tophalf.filled +oilcan +oilcan.and.thermometer +oilcan.and.thermometer.fill +oilcan.fill +opticaldisc +opticaldisc.fill +opticaldiscdrive +opticaldiscdrive.fill +opticid +opticid.fill +option +oval +oval.bottomhalf.filled +oval.fill +oval.inset.filled +oval.lefthalf.filled +oval.portrait +oval.portrait.bottomhalf.filled +oval.portrait.fill +oval.portrait.inset.filled +oval.portrait.lefthalf.filled +oval.portrait.righthalf.filled +oval.portrait.tophalf.filled +oval.righthalf.filled +oval.tophalf.filled +oven +oven.fill +p.circle +p.circle.fill +p.square +p.square.fill +p1.button.horizontal +p1.button.horizontal.fill +p2.button.horizontal +p2.button.horizontal.fill +p3.button.horizontal +p3.button.horizontal.fill +p4.button.horizontal +p4.button.horizontal.fill +pad.header +paddleshifter.left +paddleshifter.left.fill +paddleshifter.right +paddleshifter.right.fill +paint.bucket.classic +paintbrush +paintbrush.fill +paintbrush.pointed +paintbrush.pointed.fill +paintpalette +paintpalette.fill +pano +pano.badge.play +pano.badge.play.fill +pano.fill +paperclip +paperclip.badge.ellipsis +paperclip.circle +paperclip.circle.fill +paperplane +paperplane.circle +paperplane.circle.fill +paperplane.fill +paragraph +paragraphsign +parentheses +parkinglight +parkinglight.fill +parkingsign +parkingsign.brakesignal +parkingsign.brakesignal.slash +parkingsign.circle +parkingsign.circle.fill +parkingsign.radiowaves.down.right.off +parkingsign.radiowaves.left.and.right +parkingsign.radiowaves.left.and.right.slash +parkingsign.radiowaves.right.and.safetycone +parkingsign.square +parkingsign.square.fill +parkingsign.steeringwheel +party.popper +party.popper.fill +pause +pause.circle +pause.circle.fill +pause.fill +pause.rectangle +pause.rectangle.fill +pawprint +pawprint.circle +pawprint.circle.fill +pawprint.fill +pc +peacesign +pedal.accelerator +pedal.accelerator.fill +pedal.brake +pedal.brake.fill +pedal.clutch +pedal.clutch.fill +pedestrian.gate.closed +pedestrian.gate.closed.trianglebadge.exclamationmark +pedestrian.gate.open +pedestrian.gate.open.trianglebadge.exclamationmark +pencil +pencil.and.ellipsis.rectangle +pencil.and.list.clipboard +pencil.and.outline +pencil.and.ruler +pencil.and.ruler.fill +pencil.and.scribble +pencil.circle +pencil.circle.fill +pencil.line +pencil.slash +pencil.tip +pencil.tip.crop.circle +pencil.tip.crop.circle.badge.arrow.forward +pencil.tip.crop.circle.badge.arrow.forward.fill +pencil.tip.crop.circle.badge.minus +pencil.tip.crop.circle.badge.minus.fill +pencil.tip.crop.circle.badge.plus +pencil.tip.crop.circle.badge.plus.fill +pencil.tip.crop.circle.fill +pentagon +pentagon.bottomhalf.filled +pentagon.fill +pentagon.lefthalf.filled +pentagon.righthalf.filled +pentagon.tophalf.filled +percent +person +person.2 +person.2.arrow.trianglehead.counterclockwise +person.2.badge +person.2.badge.fill +person.2.badge.gearshape +person.2.badge.gearshape.fill +person.2.badge.key +person.2.badge.key.fill +person.2.badge.minus +person.2.badge.minus.fill +person.2.badge.plus +person.2.badge.plus.fill +person.2.circle +person.2.circle.fill +person.2.crop.square.stack +person.2.crop.square.stack.fill +person.2.fill +person.2.gobackward +person.2.shield +person.2.shield.fill +person.2.slash +person.2.slash.fill +person.2.square.stack +person.2.square.stack.fill +person.2.wave.2 +person.2.wave.2.fill +person.3 +person.3.fill +person.3.sequence +person.3.sequence.fill +person.and.arrow.left.and.arrow.right +person.and.arrow.left.and.arrow.right.outward +person.and.background.dotted +person.and.background.striped.horizontal +person.badge.clock +person.badge.clock.fill +person.badge.key +person.badge.key.fill +person.badge.minus +person.badge.minus.fill +person.badge.plus +person.badge.plus.fill +person.badge.shield.checkmark +person.badge.shield.checkmark.fill +person.badge.shield.exclamationmark +person.badge.shield.exclamationmark.fill +person.bubble +person.bubble.fill +person.bust +person.bust.circle +person.bust.circle.fill +person.bust.fill +person.checkmark.and.xmark +person.circle +person.circle.fill +person.crop.artframe +person.crop.badge.magnifyingglass +person.crop.badge.magnifyingglass.fill +person.crop.circle +person.crop.circle.badge +person.crop.circle.badge.checkmark +person.crop.circle.badge.clock +person.crop.circle.badge.clock.fill +person.crop.circle.badge.ellipsis +person.crop.circle.badge.ellipsis.fill +person.crop.circle.badge.exclam +person.crop.circle.badge.exclamationmark +person.crop.circle.badge.exclamationmark.fill +person.crop.circle.badge.fill +person.crop.circle.badge.minus +person.crop.circle.badge.moon +person.crop.circle.badge.moon.fill +person.crop.circle.badge.plus +person.crop.circle.badge.questionmark +person.crop.circle.badge.questionmark.fill +person.crop.circle.badge.xmark +person.crop.circle.dashed +person.crop.circle.dashed.circle +person.crop.circle.dashed.circle.fill +person.crop.circle.fill +person.crop.circle.fill.badge.checkmark +person.crop.circle.fill.badge.exclam +person.crop.circle.fill.badge.exclamationmark +person.crop.circle.fill.badge.minus +person.crop.circle.fill.badge.plus +person.crop.circle.fill.badge.questionmark +person.crop.circle.fill.badge.xmark +person.crop.rectangle +person.crop.rectangle.badge.plus +person.crop.rectangle.badge.plus.fill +person.crop.rectangle.fill +person.crop.rectangle.stack +person.crop.rectangle.stack.fill +person.crop.square +person.crop.square.badge.camera +person.crop.square.badge.camera.fill +person.crop.square.badge.video +person.crop.square.badge.video.fill +person.crop.square.fill +person.crop.square.fill.and.at.rectangle +person.crop.square.filled.and.at.rectangle +person.crop.square.filled.and.at.rectangle.fill +person.crop.square.on.square.angled +person.crop.square.on.square.angled.fill +person.fill +person.fill.and.arrow.left.and.arrow.right +person.fill.and.arrow.left.and.arrow.right.outward +person.fill.badge.minus +person.fill.badge.plus +person.fill.checkmark +person.fill.checkmark.and.xmark +person.fill.questionmark +person.fill.turn.down +person.fill.turn.left +person.fill.turn.right +person.fill.viewfinder +person.fill.xmark +person.icloud +person.icloud.fill +person.line.dotted.person +person.line.dotted.person.fill +person.slash +person.slash.fill +person.spatialaudio.3d.fill +person.spatialaudio.fill +person.spatialaudio.stereo.3d.fill +person.spatialaudio.stereo.fill +person.text.rectangle +person.text.rectangle.fill +person.text.rectangle.trianglebadge.exclamationmark +person.text.rectangle.trianglebadge.exclamationmark.fill +person.wave.2 +person.wave.2.fill +personalhotspot +personalhotspot.circle +personalhotspot.circle.fill +personalhotspot.slash +perspective +peruviansolessign +peruviansolessign.arrow.trianglehead.counterclockwise.rotate.90 +peruviansolessign.bank.building +peruviansolessign.bank.building.fill +peruviansolessign.circle +peruviansolessign.circle.fill +peruviansolessign.gauge.chart.lefthalf.righthalf +peruviansolessign.gauge.chart.leftthird.topthird.rightthird +peruviansolessign.ring +peruviansolessign.ring.dashed +peruviansolessign.square +peruviansolessign.square.fill +pesetasign +pesetasign.arrow.circlepath +pesetasign.arrow.trianglehead.counterclockwise.rotate.90 +pesetasign.bank.building +pesetasign.bank.building.fill +pesetasign.circle +pesetasign.circle.fill +pesetasign.gauge.chart.lefthalf.righthalf +pesetasign.gauge.chart.leftthird.topthird.rightthird +pesetasign.ring +pesetasign.ring.dashed +pesetasign.square +pesetasign.square.fill +pesosign +pesosign.arrow.circlepath +pesosign.arrow.trianglehead.counterclockwise.rotate.90 +pesosign.bank.building +pesosign.bank.building.fill +pesosign.circle +pesosign.circle.fill +pesosign.gauge.chart.lefthalf.righthalf +pesosign.gauge.chart.leftthird.topthird.rightthird +pesosign.ring +pesosign.ring.dashed +pesosign.square +pesosign.square.fill +pet.carrier +pet.carrier.circle +pet.carrier.circle.fill +pet.carrier.fill +phone +phone.and.waveform +phone.and.waveform.fill +phone.arrow.down.left +phone.arrow.down.left.fill +phone.arrow.right +phone.arrow.right.fill +phone.arrow.up.right +phone.arrow.up.right.circle +phone.arrow.up.right.circle.fill +phone.arrow.up.right.fill +phone.badge.checkmark +phone.badge.clock +phone.badge.clock.fill +phone.badge.plus +phone.badge.waveform +phone.badge.waveform.fill +phone.bubble +phone.bubble.fill +phone.bubble.left +phone.bubble.left.fill +phone.circle +phone.circle.fill +phone.connection +phone.connection.fill +phone.down +phone.down.circle +phone.down.circle.fill +phone.down.fill +phone.down.waves.left.and.right +phone.fill +phone.fill.arrow.down.left +phone.fill.arrow.right +phone.fill.arrow.up.right +phone.fill.badge.checkmark +phone.fill.badge.plus +phone.fill.connection +phone.pause +phone.pause.circle +phone.pause.circle.fill +phone.pause.fill +photo +photo.artframe +photo.artframe.circle +photo.artframe.circle.fill +photo.badge.arrow.down +photo.badge.arrow.down.fill +photo.badge.checkmark +photo.badge.checkmark.fill +photo.badge.exclamationmark +photo.badge.exclamationmark.fill +photo.badge.magnifyingglass +photo.badge.magnifyingglass.fill +photo.badge.plus +photo.badge.plus.fill +photo.badge.shield.exclamationmark +photo.badge.shield.exclamationmark.fill +photo.circle +photo.circle.fill +photo.fill +photo.fill.on.rectangle.fill +photo.on.rectangle +photo.on.rectangle.angled +photo.on.rectangle.angled.fill +photo.stack +photo.stack.fill +photo.trianglebadge.exclamationmark +photo.trianglebadge.exclamationmark.fill +photo.tv +pi +pi.circle +pi.circle.fill +pi.square +pi.square.fill +pianokeys +pianokeys.inverse +pill +pill.circle +pill.circle.fill +pill.fill +pills +pills.circle +pills.circle.fill +pills.fill +pin +pin.circle +pin.circle.fill +pin.fill +pin.slash +pin.slash.fill +pin.square +pin.square.fill +pip +pip.enter +pip.exit +pip.fill +pip.remove +pip.swap +pipe.and.drop +pipe.and.drop.fill +placeholdertext.fill +platter.2.filled.ipad +platter.2.filled.ipad.landscape +platter.2.filled.iphone +platter.2.filled.iphone.landscape +platter.bottom.applewatch.case +platter.filled.bottom.and.arrow.down.iphone +platter.filled.bottom.applewatch.case +platter.filled.bottom.iphone +platter.filled.top.and.arrow.up.iphone +platter.filled.top.applewatch.case +platter.filled.top.iphone +platter.top.applewatch.case +play +play.circle +play.circle.fill +play.desktopcomputer +play.diamond +play.diamond.fill +play.display +play.fill +play.house +play.house.fill +play.laptopcomputer +play.rectangle +play.rectangle.fill +play.rectangle.on.rectangle +play.rectangle.on.rectangle.circle +play.rectangle.on.rectangle.circle.fill +play.rectangle.on.rectangle.fill +play.slash +play.slash.fill +play.square +play.square.fill +play.square.stack +play.square.stack.fill +play.tv +play.tv.fill +playpause +playpause.circle +playpause.circle.fill +playpause.fill +playstation.logo +plus +plus.app +plus.app.fill +plus.arrow.trianglehead.clockwise +plus.arrow.trianglehead.counterclockwise +plus.bubble +plus.bubble.fill +plus.capsule +plus.capsule.fill +plus.circle +plus.circle.dashed +plus.circle.fill +plus.diamond +plus.diamond.fill +plus.forwardslash.minus +plus.magnifyingglass +plus.message +plus.message.fill +plus.minus.capsule +plus.minus.capsule.fill +plus.rectangle +plus.rectangle.fill +plus.rectangle.fill.on.folder.fill +plus.rectangle.fill.on.rectangle.fill +plus.rectangle.on.folder +plus.rectangle.on.folder.fill +plus.rectangle.on.rectangle +plus.rectangle.portrait +plus.rectangle.portrait.fill +plus.slash.minus +plus.square +plus.square.dashed +plus.square.fill +plus.square.fill.on.square.fill +plus.square.on.square +plus.viewfinder +plusminus +plusminus.circle +plusminus.circle.fill +point.3.connected.trianglepath.dotted +point.3.filled.connected.trianglepath.dotted +point.bottomleft.filled.forward.to.point.topright.scurvepath +point.bottomleft.forward.to.arrow.triangle.scurvepath +point.bottomleft.forward.to.arrow.triangle.scurvepath.fill +point.bottomleft.forward.to.arrow.triangle.uturn.scurvepath +point.bottomleft.forward.to.arrow.triangle.uturn.scurvepath.fill +point.bottomleft.forward.to.arrowtriangle.uturn.scurvepath +point.bottomleft.forward.to.arrowtriangle.uturn.scurvepath.fill +point.bottomleft.forward.to.point.topright.filled.scurvepath +point.bottomleft.forward.to.point.topright.scurvepath +point.bottomleft.forward.to.point.topright.scurvepath.fill +point.fill.topleft.down.curvedto.point.fill.bottomright.up +point.filled.topleft.down.curvedto.point.bottomright.up +point.forward.to.point.capsulepath +point.forward.to.point.capsulepath.fill +point.topleft.down.curvedto.point.bottomright.up +point.topleft.down.curvedto.point.bottomright.up.fill +point.topleft.down.curvedto.point.filled.bottomright.up +point.topleft.down.to.point.bottomright.curvepath +point.topleft.down.to.point.bottomright.curvepath.fill +point.topleft.down.to.point.bottomright.filled.curvepath +point.topleft.filled.down.to.point.bottomright.curvepath +point.topright.arrow.triangle.backward.to.point.bottomleft.filled.scurvepath +point.topright.arrow.triangle.backward.to.point.bottomleft.scurvepath +point.topright.arrow.triangle.backward.to.point.bottomleft.scurvepath.fill +point.topright.filled.arrow.triangle.backward.to.point.bottomleft.scurvepath +pointer.arrow +pointer.arrow.and.square.on.square.dashed +pointer.arrow.click +pointer.arrow.click.2 +pointer.arrow.click.badge.clock +pointer.arrow.ipad +pointer.arrow.ipad.and.square.on.square.dashed +pointer.arrow.ipad.rays +pointer.arrow.ipad.slash +pointer.arrow.ipad.slash.square +pointer.arrow.ipad.slash.square.fill +pointer.arrow.ipad.square +pointer.arrow.ipad.square.fill +pointer.arrow.motionlines +pointer.arrow.motionlines.click +pointer.arrow.rays +pointer.arrow.slash +pointer.arrow.slash.square +pointer.arrow.slash.square.fill +pointer.arrow.square +pointer.arrow.square.fill +polishzlotysign +polishzlotysign.arrow.circlepath +polishzlotysign.arrow.trianglehead.counterclockwise.rotate.90 +polishzlotysign.bank.building +polishzlotysign.bank.building.fill +polishzlotysign.circle +polishzlotysign.circle.fill +polishzlotysign.gauge.chart.lefthalf.righthalf +polishzlotysign.gauge.chart.leftthird.topthird.rightthird +polishzlotysign.ring +polishzlotysign.ring.dashed +polishzlotysign.square +polishzlotysign.square.fill +popcorn +popcorn.circle +popcorn.circle.fill +popcorn.fill +power +power.circle +power.circle.fill +power.dotted +powercord +powercord.fill +powermeter +poweroff +poweron +poweroutlet.strip +poweroutlet.strip.fill +poweroutlet.type.a +poweroutlet.type.a.fill +poweroutlet.type.a.square +poweroutlet.type.a.square.fill +poweroutlet.type.b +poweroutlet.type.b.fill +poweroutlet.type.b.square +poweroutlet.type.b.square.fill +poweroutlet.type.c +poweroutlet.type.c.fill +poweroutlet.type.c.square +poweroutlet.type.c.square.fill +poweroutlet.type.d +poweroutlet.type.d.fill +poweroutlet.type.d.square +poweroutlet.type.d.square.fill +poweroutlet.type.e +poweroutlet.type.e.fill +poweroutlet.type.e.square +poweroutlet.type.e.square.fill +poweroutlet.type.f +poweroutlet.type.f.fill +poweroutlet.type.f.square +poweroutlet.type.f.square.fill +poweroutlet.type.g +poweroutlet.type.g.fill +poweroutlet.type.g.square +poweroutlet.type.g.square.fill +poweroutlet.type.h +poweroutlet.type.h.fill +poweroutlet.type.h.square +poweroutlet.type.h.square.fill +poweroutlet.type.i +poweroutlet.type.i.fill +poweroutlet.type.i.square +poweroutlet.type.i.square.fill +poweroutlet.type.j +poweroutlet.type.j.fill +poweroutlet.type.j.square +poweroutlet.type.j.square.fill +poweroutlet.type.k +poweroutlet.type.k.fill +poweroutlet.type.k.square +poweroutlet.type.k.square.fill +poweroutlet.type.l +poweroutlet.type.l.fill +poweroutlet.type.l.square +poweroutlet.type.l.square.fill +poweroutlet.type.m +poweroutlet.type.m.fill +poweroutlet.type.m.square +poweroutlet.type.m.square.fill +poweroutlet.type.n +poweroutlet.type.n.fill +poweroutlet.type.n.square +poweroutlet.type.n.square.fill +poweroutlet.type.o +poweroutlet.type.o.fill +poweroutlet.type.o.square +poweroutlet.type.o.square.fill +powerplug +powerplug.fill +powerplug.portrait +powerplug.portrait.fill +powersleep +printer +printer.dotmatrix +printer.dotmatrix.fill +printer.dotmatrix.fill.and.paper.fill +printer.dotmatrix.filled.and.paper +printer.dotmatrix.filled.and.paper.inverse +printer.dotmatrix.inverse +printer.fill +printer.fill.and.paper.fill +printer.filled.and.paper +printer.filled.and.paper.inverse +printer.inverse +progress.indicator +projective +purchased +purchased.circle +purchased.circle.fill +puzzlepiece +puzzlepiece.extension +puzzlepiece.extension.fill +puzzlepiece.fill +pyramid +pyramid.fill +q.circle +q.circle.fill +q.square +q.square.fill +qrcode +qrcode.viewfinder +questionmark +questionmark.app +questionmark.app.dashed +questionmark.app.fill +questionmark.bubble +questionmark.bubble.fill +questionmark.circle +questionmark.circle.dashed +questionmark.circle.fill +questionmark.diamond +questionmark.diamond.fill +questionmark.folder +questionmark.folder.fill +questionmark.key.filled +questionmark.message +questionmark.message.fill +questionmark.square +questionmark.square.dashed +questionmark.square.fill +questionmark.text.page +questionmark.text.page.fill +questionmark.video +questionmark.video.fill +quote.bubble +quote.bubble.fill +quote.closing +quote.opening +quotelevel +r.button.roundedbottom.horizontal +r.button.roundedbottom.horizontal.fill +r.circle +r.circle.fill +r.joystick +r.joystick.down +r.joystick.down.fill +r.joystick.fill +r.joystick.press.down +r.joystick.press.down.fill +r.joystick.tilt.down +r.joystick.tilt.down.fill +r.joystick.tilt.left +r.joystick.tilt.left.fill +r.joystick.tilt.right +r.joystick.tilt.right.fill +r.joystick.tilt.up +r.joystick.tilt.up.fill +r.rectangle.roundedbottom +r.rectangle.roundedbottom.fill +r.square +r.square.fill +r.square.fill.on.square.fill +r.square.on.square +r.square.on.square.fill +r1.button.roundedbottom.horizontal +r1.button.roundedbottom.horizontal.fill +r1.circle +r1.circle.fill +r1.rectangle.roundedbottom +r1.rectangle.roundedbottom.fill +r2.button.angledtop.vertical.right +r2.button.angledtop.vertical.right.fill +r2.button.roundedtop.horizontal +r2.button.roundedtop.horizontal.fill +r2.circle +r2.circle.fill +r2.rectangle.roundedtop +r2.rectangle.roundedtop.fill +r3.button.angledbottom.horizontal.right +r3.button.angledbottom.horizontal.right.fill +r4.button.horizontal +r4.button.horizontal.fill +radicand.squareroot +radio +radio.fill +radiowaves.left +radiowaves.right +rainbow +rays +rb.button.roundedbottom.horizontal +rb.button.roundedbottom.horizontal.fill +rb.circle +rb.circle.fill +rb.rectangle.roundedbottom +rb.rectangle.roundedbottom.fill +receipt +receipt.fill +record.circle +record.circle.fill +recordingtape +recordingtape.circle +recordingtape.circle.fill +rectangle +rectangle.2.swap +rectangle.3.group +rectangle.3.group.bubble +rectangle.3.group.bubble.fill +rectangle.3.group.bubble.left +rectangle.3.group.bubble.left.fill +rectangle.3.group.dashed +rectangle.3.group.fill +rectangle.3.offgrid +rectangle.3.offgrid.bubble.left +rectangle.3.offgrid.bubble.left.fill +rectangle.3.offgrid.fill +rectangle.and.arrow.up.right.and.arrow.down.left +rectangle.and.arrow.up.right.and.arrow.down.left.slash +rectangle.and.hand.point.up.left +rectangle.and.hand.point.up.left.fill +rectangle.and.hand.point.up.left.filled +rectangle.and.paperclip +rectangle.and.pencil.and.ellipsis +rectangle.and.text.magnifyingglass +rectangle.arrowtriangle.2.inward +rectangle.arrowtriangle.2.outward +rectangle.badge.checkmark +rectangle.badge.minus +rectangle.badge.person.crop +rectangle.badge.plus +rectangle.badge.sparkles +rectangle.badge.sparkles.fill +rectangle.badge.xmark +rectangle.bottomhalf.filled +rectangle.bottomhalf.inset.filled +rectangle.bottomthird.inset.fill +rectangle.bottomthird.inset.filled +rectangle.center.inset.fill +rectangle.center.inset.filled +rectangle.center.inset.filled.badge.plus +rectangle.checkered +rectangle.compress.vertical +rectangle.connected.to.line.below +rectangle.dashed +rectangle.dashed.and.paperclip +rectangle.dashed.badge.record +rectangle.dock +rectangle.expand.diagonal +rectangle.expand.vertical +rectangle.fill +rectangle.fill.badge.checkmark +rectangle.fill.badge.minus +rectangle.fill.badge.person.crop +rectangle.fill.badge.plus +rectangle.fill.badge.xmark +rectangle.fill.on.rectangle.angled.fill +rectangle.fill.on.rectangle.fill +rectangle.fill.on.rectangle.fill.circle +rectangle.fill.on.rectangle.fill.circle.fill +rectangle.fill.on.rectangle.fill.slash.fill +rectangle.filled.and.hand.point.up.left +rectangle.grid.1x2 +rectangle.grid.1x2.fill +rectangle.grid.1x3 +rectangle.grid.1x3.fill +rectangle.grid.2x2 +rectangle.grid.2x2.fill +rectangle.grid.3x1 +rectangle.grid.3x1.fill +rectangle.grid.3x2 +rectangle.grid.3x2.fill +rectangle.grid.3x3 +rectangle.grid.3x3.fill +rectangle.inset.bottomleading.filled +rectangle.inset.bottomleft.fill +rectangle.inset.bottomleft.filled +rectangle.inset.bottomright.fill +rectangle.inset.bottomright.filled +rectangle.inset.bottomtrailing.filled +rectangle.inset.fill +rectangle.inset.filled +rectangle.inset.filled.and.cursorarrow +rectangle.inset.filled.and.person.filled +rectangle.inset.filled.badge.record +rectangle.inset.filled.on.rectangle +rectangle.inset.topleading.filled +rectangle.inset.topleft.fill +rectangle.inset.topleft.filled +rectangle.inset.topright.fill +rectangle.inset.topright.filled +rectangle.inset.toptrailing.filled +rectangle.landscape.rotate +rectangle.landscape.rotate.slash +rectangle.leadinghalf.filled +rectangle.leadinghalf.inset.filled +rectangle.leadinghalf.inset.filled.arrow.leading +rectangle.leadingthird.inset.filled +rectangle.lefthalf.fill +rectangle.lefthalf.filled +rectangle.lefthalf.inset.fill +rectangle.lefthalf.inset.fill.arrow.left +rectangle.lefthalf.inset.filled +rectangle.lefthalf.inset.filled.arrow.left +rectangle.leftthird.inset.fill +rectangle.leftthird.inset.filled +rectangle.on.rectangle +rectangle.on.rectangle.angled +rectangle.on.rectangle.badge.gearshape +rectangle.on.rectangle.button.angledtop.vertical.left +rectangle.on.rectangle.button.angledtop.vertical.left.fill +rectangle.on.rectangle.circle +rectangle.on.rectangle.circle.fill +rectangle.on.rectangle.dashed +rectangle.on.rectangle.slash +rectangle.on.rectangle.slash.circle +rectangle.on.rectangle.slash.circle.fill +rectangle.on.rectangle.slash.fill +rectangle.on.rectangle.square +rectangle.on.rectangle.square.fill +rectangle.pattern.checkered +rectangle.portrait +rectangle.portrait.and.arrow.forward +rectangle.portrait.and.arrow.forward.fill +rectangle.portrait.and.arrow.right +rectangle.portrait.and.arrow.right.fill +rectangle.portrait.arrowtriangle.2.inward +rectangle.portrait.arrowtriangle.2.outward +rectangle.portrait.badge.plus +rectangle.portrait.badge.plus.fill +rectangle.portrait.bottomhalf.filled +rectangle.portrait.bottomhalf.inset.filled +rectangle.portrait.bottomleading.inset.filled +rectangle.portrait.bottomleft.inset.filled +rectangle.portrait.bottomright.inset.filled +rectangle.portrait.bottomthird.inset.filled +rectangle.portrait.bottomtrailing.inset.filled +rectangle.portrait.center.inset.filled +rectangle.portrait.fill +rectangle.portrait.inset.filled +rectangle.portrait.leadinghalf.inset.filled +rectangle.portrait.leadingthird.inset.filled +rectangle.portrait.lefthalf.filled +rectangle.portrait.lefthalf.inset.filled +rectangle.portrait.leftthird.inset.filled +rectangle.portrait.on.rectangle.portrait +rectangle.portrait.on.rectangle.portrait.angled +rectangle.portrait.on.rectangle.portrait.angled.fill +rectangle.portrait.on.rectangle.portrait.fill +rectangle.portrait.on.rectangle.portrait.slash +rectangle.portrait.on.rectangle.portrait.slash.fill +rectangle.portrait.righthalf.filled +rectangle.portrait.righthalf.inset.filled +rectangle.portrait.rightthird.inset.filled +rectangle.portrait.rotate +rectangle.portrait.rotate.slash +rectangle.portrait.slash +rectangle.portrait.slash.fill +rectangle.portrait.split.2x1 +rectangle.portrait.split.2x1.fill +rectangle.portrait.split.2x1.slash +rectangle.portrait.split.2x1.slash.fill +rectangle.portrait.tophalf.filled +rectangle.portrait.tophalf.inset.filled +rectangle.portrait.topleading.inset.filled +rectangle.portrait.topleft.inset.filled +rectangle.portrait.topright.inset.filled +rectangle.portrait.topthird.inset.filled +rectangle.portrait.toptrailing.inset.filled +rectangle.portrait.trailinghalf.inset.filled +rectangle.portrait.trailingthird.inset.filled +rectangle.ratio.16.to.9 +rectangle.ratio.16.to.9.fill +rectangle.ratio.3.to.4 +rectangle.ratio.3.to.4.fill +rectangle.ratio.4.to.3 +rectangle.ratio.4.to.3.fill +rectangle.ratio.9.to.16 +rectangle.ratio.9.to.16.fill +rectangle.righthalf.fill +rectangle.righthalf.filled +rectangle.righthalf.inset.fill +rectangle.righthalf.inset.fill.arrow.right +rectangle.righthalf.inset.filled +rectangle.righthalf.inset.filled.arrow.right +rectangle.rightthird.inset.fill +rectangle.rightthird.inset.filled +rectangle.roundedbottom +rectangle.roundedbottom.fill +rectangle.roundedtop +rectangle.roundedtop.fill +rectangle.slash +rectangle.slash.fill +rectangle.split.1x2 +rectangle.split.1x2.fill +rectangle.split.2x1 +rectangle.split.2x1.fill +rectangle.split.2x1.slash +rectangle.split.2x1.slash.fill +rectangle.split.2x2 +rectangle.split.2x2.fill +rectangle.split.3x1 +rectangle.split.3x1.fill +rectangle.split.3x3 +rectangle.split.3x3.fill +rectangle.stack +rectangle.stack.badge.minus +rectangle.stack.badge.person.crop +rectangle.stack.badge.person.crop.fill +rectangle.stack.badge.play +rectangle.stack.badge.play.fill +rectangle.stack.badge.plus +rectangle.stack.fill +rectangle.stack.fill.badge.minus +rectangle.stack.fill.badge.person.crop +rectangle.stack.fill.badge.plus +rectangle.stack.person.crop +rectangle.stack.person.crop.fill +rectangle.stack.slash +rectangle.stack.slash.fill +rectangle.tophalf.filled +rectangle.tophalf.inset.filled +rectangle.topthird.inset +rectangle.topthird.inset.fill +rectangle.topthird.inset.filled +rectangle.trailinghalf.filled +rectangle.trailinghalf.inset.filled +rectangle.trailinghalf.inset.filled.arrow.trailing +rectangle.trailingthird.inset.filled +refrigerator +refrigerator.fill +repeat +repeat.1 +repeat.1.circle +repeat.1.circle.fill +repeat.badge.xmark +repeat.badge.xmark.circle.fill +repeat.circle +repeat.circle.fill +restart +restart.circle +restart.circle.fill +retarder.brakesignal +retarder.brakesignal.and.exclamationmark +retarder.brakesignal.slash +return +return.left +return.right +rhombus +rhombus.fill +richtext.page +richtext.page.fill +right +right.circle +right.circle.fill +righttriangle +righttriangle.fill +righttriangle.split.diagonal +righttriangle.split.diagonal.fill +ring +ring.dashed +rm.button.horizontal +rm.button.horizontal.fill +road.lane.arrowtriangle.2.inward +road.lane.arrowtriangle.2.outward +road.lanes +road.lanes.curved.left +road.lanes.curved.right +robotic.vacuum +robotic.vacuum.and.arrowtriangle.up +robotic.vacuum.and.arrowtriangle.up.fill +robotic.vacuum.and.ellipsis +robotic.vacuum.and.ellipsis.fill +robotic.vacuum.fill +roller.shade.closed +roller.shade.open +roman.shade.closed +roman.shade.open +rosette +rotate.3d +rotate.3d.circle +rotate.3d.circle.fill +rotate.3d.fill +rotate.left +rotate.left.fill +rotate.right +rotate.right.fill +rsb.button.angledbottom.horizontal.right +rsb.button.angledbottom.horizontal.right.fill +rt.button.roundedtop.horizontal +rt.button.roundedtop.horizontal.fill +rt.circle +rt.circle.fill +rt.rectangle.roundedtop +rt.rectangle.roundedtop.fill +rublesign +rublesign.arrow.circlepath +rublesign.arrow.trianglehead.counterclockwise.rotate.90 +rublesign.bank.building +rublesign.bank.building.fill +rublesign.circle +rublesign.circle.fill +rublesign.gauge.chart.lefthalf.righthalf +rublesign.gauge.chart.leftthird.topthird.rightthird +rublesign.ring +rublesign.ring.dashed +rublesign.square +rublesign.square.fill +rugbyball +rugbyball.circle +rugbyball.circle.fill +rugbyball.fill +ruler +ruler.fill +rupeesign +rupeesign.arrow.circlepath +rupeesign.arrow.trianglehead.counterclockwise.rotate.90 +rupeesign.bank.building +rupeesign.bank.building.fill +rupeesign.circle +rupeesign.circle.fill +rupeesign.gauge.chart.lefthalf.righthalf +rupeesign.gauge.chart.leftthird.topthird.rightthird +rupeesign.ring +rupeesign.ring.dashed +rupeesign.square +rupeesign.square.fill +s.circle +s.circle.fill +s.square +s.square.fill +safari +safari.fill +sailboat +sailboat.circle +sailboat.circle.fill +sailboat.fill +scale.3d +scalemass +scalemass.fill +scanner +scanner.fill +scissors +scissors.badge.ellipsis +scissors.circle +scissors.circle.fill +scooter +scope +screwdriver +screwdriver.fill +scribble +scribble.variable +scroll +scroll.fill +sdcard +sdcard.fill +seal +seal.fill +selection.pin.in.out +sensor +sensor.fill +sensor.radiowaves.left.and.right +sensor.radiowaves.left.and.right.fill +sensor.tag.radiowaves.forward +sensor.tag.radiowaves.forward.fill +server.rack +service.dog +service.dog.fill +shadow +shared.with.you +shared.with.you.circle +shared.with.you.circle.fill +shared.with.you.slash +sharedwithyou +sharedwithyou.circle +sharedwithyou.circle.fill +sharedwithyou.slash +shareplay +shareplay.slash +shazam.logo +shazam.logo.fill +shekelsign +shekelsign.arrow.circlepath +shekelsign.arrow.trianglehead.counterclockwise.rotate.90 +shekelsign.bank.building +shekelsign.bank.building.fill +shekelsign.circle +shekelsign.circle.fill +shekelsign.gauge.chart.lefthalf.righthalf +shekelsign.gauge.chart.leftthird.topthird.rightthird +shekelsign.ring +shekelsign.ring.dashed +shekelsign.square +shekelsign.square.fill +sheqelsign.circle +sheqelsign.circle.fill +sheqelsign.square +sheqelsign.square.fill +shield +shield.checkerboard +shield.checkered +shield.fill +shield.lefthalf.fill +shield.lefthalf.fill.slash +shield.lefthalf.filled +shield.lefthalf.filled.badge.checkmark +shield.lefthalf.filled.slash +shield.lefthalf.filled.trianglebadge.exclamationmark +shield.pattern.checkered +shield.righthalf.filled +shield.slash +shield.slash.fill +shift +shift.fill +shippingbox +shippingbox.and.arrow.backward +shippingbox.and.arrow.backward.fill +shippingbox.circle +shippingbox.circle.fill +shippingbox.fill +shoe +shoe.2 +shoe.2.fill +shoe.arrow.trianglehead.up.and.down +shoe.arrow.trianglehead.up.and.down.fill +shoe.arrow.trianglehead.up.right +shoe.arrow.trianglehead.up.right.circle +shoe.arrow.trianglehead.up.right.circle.fill +shoe.arrow.trianglehead.up.right.fill +shoe.circle +shoe.circle.fill +shoe.fill +shoeprints.fill +shower +shower.fill +shower.handheld +shower.handheld.fill +shower.sidejet +shower.sidejet.fill +shuffle +shuffle.circle +shuffle.circle.fill +sidebar.leading +sidebar.left +sidebar.right +sidebar.squares.leading +sidebar.squares.left +sidebar.squares.right +sidebar.squares.trailing +sidebar.trailing +signature +signpost.and.arrowtriangle.up +signpost.and.arrowtriangle.up.circle +signpost.and.arrowtriangle.up.circle.fill +signpost.and.arrowtriangle.up.fill +signpost.left +signpost.left.circle +signpost.left.circle.fill +signpost.left.fill +signpost.right +signpost.right.and.left +signpost.right.and.left.circle +signpost.right.and.left.circle.fill +signpost.right.and.left.fill +signpost.right.circle +signpost.right.circle.fill +signpost.right.fill +simcard +simcard.2 +simcard.2.fill +simcard.fill +singaporedollarsign +singaporedollarsign.arrow.trianglehead.counterclockwise.rotate.90 +singaporedollarsign.bank.building +singaporedollarsign.bank.building.fill +singaporedollarsign.circle +singaporedollarsign.circle.fill +singaporedollarsign.gauge.chart.lefthalf.righthalf +singaporedollarsign.gauge.chart.leftthird.topthird.rightthird +singaporedollarsign.ring +singaporedollarsign.ring.dashed +singaporedollarsign.square +singaporedollarsign.square.fill +sink +sink.fill +siri +skateboard +skateboard.fill +skew +skis +skis.fill +slash.circle +slash.circle.fill +sleep +sleep.circle +sleep.circle.fill +slider.horizontal.2.arrow.trianglehead.counterclockwise +slider.horizontal.2.gobackward +slider.horizontal.2.rectangle.and.arrow.triangle.2.circlepath +slider.horizontal.2.rectangle.and.arrow.trianglehead.2.clockwise.rotate.90 +slider.horizontal.2.square +slider.horizontal.2.square.badge.arrow.down +slider.horizontal.2.square.on.square +slider.horizontal.3 +slider.horizontal.below.circle.lefthalf.filled +slider.horizontal.below.circle.lefthalf.filled.inverse +slider.horizontal.below.circle.righthalf.filled +slider.horizontal.below.circle.righthalf.filled.inverse +slider.horizontal.below.rectangle +slider.horizontal.below.square.and.square.filled +slider.horizontal.below.square.fill.and.square +slider.horizontal.below.square.filled.and.square +slider.horizontal.below.sun.max +slider.horizontal.below.sun.min +slider.vertical.3 +slowmo +smallcircle.circle +smallcircle.circle.fill +smallcircle.fill.circle +smallcircle.fill.circle.fill +smallcircle.filled.circle +smallcircle.filled.circle.fill +smartphone +smiley +smiley.fill +smoke +smoke.circle +smoke.circle.fill +smoke.fill +snow +snowboard +snowboard.fill +snowflake +snowflake.circle +snowflake.circle.fill +snowflake.road.lane +snowflake.road.lane.dashed +snowflake.slash +soccerball +soccerball.circle +soccerball.circle.fill +soccerball.circle.fill.inverse +soccerball.circle.inverse +soccerball.inverse +sofa +sofa.fill +sos +sos.circle +sos.circle.fill +space +sparkle +sparkle.magnifyingglass +sparkle.text.clipboard +sparkle.text.clipboard.fill +sparkles +sparkles.2 +sparkles.rectangle.stack +sparkles.rectangle.stack.fill +sparkles.square.fill.on.square +sparkles.square.filled.on.square +sparkles.tv +sparkles.tv.fill +spatial.capture +spatial.capture.fill +spatial.capture.on.hexagon +spatial.capture.on.hexagon.fill +spatial.capture.slash +spatial.capture.slash.fill +speaker +speaker.1 +speaker.1.fill +speaker.2 +speaker.2.fill +speaker.3 +speaker.3.fill +speaker.badge.exclamationmark +speaker.badge.exclamationmark.fill +speaker.circle +speaker.circle.fill +speaker.fill +speaker.minus +speaker.minus.fill +speaker.plus +speaker.plus.fill +speaker.slash +speaker.slash.circle +speaker.slash.circle.fill +speaker.slash.fill +speaker.square +speaker.square.fill +speaker.trianglebadge.exclamationmark +speaker.trianglebadge.exclamationmark.fill +speaker.wave.1 +speaker.wave.1.arrowtriangles.up.right.down.left +speaker.wave.1.fill +speaker.wave.2 +speaker.wave.2.bubble +speaker.wave.2.bubble.fill +speaker.wave.2.bubble.left +speaker.wave.2.bubble.left.fill +speaker.wave.2.circle +speaker.wave.2.circle.fill +speaker.wave.2.fill +speaker.wave.3 +speaker.wave.3.fill +speaker.zzz +speaker.zzz.fill +speedometer +spigot +spigot.fill +spoon.serving +sportscourt +sportscourt.circle +sportscourt.circle.fill +sportscourt.fill +sprinkler +sprinkler.and.droplets +sprinkler.and.droplets.fill +sprinkler.fill +square +square.2.layers.3d +square.2.layers.3d.bottom.filled +square.2.layers.3d.fill +square.2.layers.3d.top.filled +square.2.stack.3d +square.2.stack.3d.bottom.fill +square.2.stack.3d.bottom.filled +square.2.stack.3d.top.fill +square.2.stack.3d.top.filled +square.3.layers.3d +square.3.layers.3d.bottom.filled +square.3.layers.3d.down.backward +square.3.layers.3d.down.forward +square.3.layers.3d.down.left +square.3.layers.3d.down.left.slash +square.3.layers.3d.down.right +square.3.layers.3d.down.right.slash +square.3.layers.3d.middle.filled +square.3.layers.3d.slash +square.3.layers.3d.top.filled +square.3.stack.3d +square.3.stack.3d.bottom.fill +square.3.stack.3d.bottom.filled +square.3.stack.3d.middle.fill +square.3.stack.3d.middle.filled +square.3.stack.3d.slash +square.3.stack.3d.top.fill +square.3.stack.3d.top.filled +square.and.arrow.down +square.and.arrow.down.badge.checkmark +square.and.arrow.down.badge.checkmark.fill +square.and.arrow.down.badge.clock +square.and.arrow.down.badge.clock.fill +square.and.arrow.down.badge.xmark +square.and.arrow.down.badge.xmark.fill +square.and.arrow.down.fill +square.and.arrow.down.on.square +square.and.arrow.down.on.square.fill +square.and.arrow.up +square.and.arrow.up.badge.checkmark +square.and.arrow.up.badge.checkmark.fill +square.and.arrow.up.badge.clock +square.and.arrow.up.badge.clock.fill +square.and.arrow.up.circle +square.and.arrow.up.circle.fill +square.and.arrow.up.fill +square.and.arrow.up.on.square +square.and.arrow.up.on.square.fill +square.and.arrow.up.trianglebadge.exclamationmark +square.and.arrow.up.trianglebadge.exclamationmark.fill +square.and.at.rectangle +square.and.at.rectangle.fill +square.and.line.vertical.and.square +square.and.line.vertical.and.square.fill +square.and.line.vertical.and.square.filled +square.and.pencil +square.and.pencil.circle +square.and.pencil.circle.fill +square.arrowtriangle.4.outward +square.badge.plus +square.badge.plus.fill +square.bottomhalf.fill +square.bottomhalf.filled +square.bottomthird.inset.filled +square.circle +square.circle.fill +square.dashed +square.dashed.inset.fill +square.dashed.inset.filled +square.dotted +square.fill +square.fill.and.line.vertical.and.square +square.fill.and.line.vertical.and.square.fill +square.fill.and.line.vertical.square.fill +square.fill.on.circle.fill +square.fill.on.square +square.fill.on.square.fill +square.fill.text.grid.1x2 +square.filled.and.line.vertical.and.square +square.filled.on.square +square.grid.2x2 +square.grid.2x2.fill +square.grid.3x1.below.line.grid.1x2 +square.grid.3x1.below.line.grid.1x2.fill +square.grid.3x1.fill.below.line.grid.1x2 +square.grid.3x1.folder.badge.plus +square.grid.3x1.folder.fill.badge.plus +square.grid.3x2 +square.grid.3x2.fill +square.grid.3x3 +square.grid.3x3.bottomleft.fill +square.grid.3x3.bottomleft.filled +square.grid.3x3.bottommiddle.fill +square.grid.3x3.bottommiddle.filled +square.grid.3x3.bottomright.fill +square.grid.3x3.bottomright.filled +square.grid.3x3.fill +square.grid.3x3.fill.square +square.grid.3x3.middle.fill +square.grid.3x3.middle.filled +square.grid.3x3.middleleft.fill +square.grid.3x3.middleleft.filled +square.grid.3x3.middleright.fill +square.grid.3x3.middleright.filled +square.grid.3x3.square +square.grid.3x3.square.badge.ellipsis +square.grid.3x3.topleft.fill +square.grid.3x3.topleft.filled +square.grid.3x3.topmiddle.fill +square.grid.3x3.topmiddle.filled +square.grid.3x3.topright.fill +square.grid.3x3.topright.filled +square.grid.4x3.fill +square.inset.filled +square.leadingthird.inset.filled +square.lefthalf.fill +square.lefthalf.filled +square.leftthird.inset.filled +square.on.circle +square.on.square +square.on.square.badge.person.crop +square.on.square.badge.person.crop.fill +square.on.square.dashed +square.on.square.intersection.dashed +square.on.square.squareshape.controlhandles +square.resize +square.resize.down +square.resize.up +square.righthalf.fill +square.righthalf.filled +square.rightthird.inset.filled +square.slash +square.slash.fill +square.split.1x2 +square.split.1x2.fill +square.split.2x1 +square.split.2x1.fill +square.split.2x2 +square.split.2x2.fill +square.split.bottomrightquarter +square.split.bottomrightquarter.fill +square.split.diagonal +square.split.diagonal.2x2 +square.split.diagonal.2x2.fill +square.split.diagonal.fill +square.stack +square.stack.3d.down.dottedline +square.stack.3d.down.forward +square.stack.3d.down.forward.fill +square.stack.3d.down.right +square.stack.3d.down.right.fill +square.stack.3d.forward.dottedline +square.stack.3d.forward.dottedline.fill +square.stack.3d.up +square.stack.3d.up.badge.a +square.stack.3d.up.badge.a.fill +square.stack.3d.up.badge.automatic +square.stack.3d.up.badge.automatic.fill +square.stack.3d.up.fill +square.stack.3d.up.slash +square.stack.3d.up.slash.fill +square.stack.3d.up.trianglebadge.exclamationmark +square.stack.3d.up.trianglebadge.exclamationmark.fill +square.stack.fill +square.text.square +square.text.square.fill +square.tophalf.fill +square.tophalf.filled +square.topthird.inset.filled +square.trailingthird.inset.filled +squareroot +squares.below.rectangle +squares.leading.rectangle +squares.leading.rectangle.fill +squareshape +squareshape.controlhandles.on.squareshape.controlhandles +squareshape.dashed.squareshape +squareshape.dotted.split.2x2 +squareshape.dotted.squareshape +squareshape.fill +squareshape.split.2x2 +squareshape.split.2x2.dotted +squareshape.split.2x2.dotted.inside +squareshape.split.2x2.dotted.inside.and.outside +squareshape.split.2x2.dotted.outside +squareshape.split.3x3 +squareshape.squareshape.dashed +squareshape.squareshape.dotted +stairs +star +star.bubble +star.bubble.fill +star.circle +star.circle.fill +star.fill +star.hexagon +star.hexagon.fill +star.leadinghalf.fill +star.leadinghalf.filled +star.lefthalf.fill +star.rectangle +star.rectangle.fill +star.slash +star.slash.fill +star.square +star.square.fill +star.square.on.square +star.square.on.square.fill +staroflife +staroflife.circle +staroflife.circle.fill +staroflife.fill +staroflife.shield +staroflife.shield.fill +steeringwheel +steeringwheel.and.hands +steeringwheel.and.heat.waves +steeringwheel.and.key +steeringwheel.and.liquid.wave +steeringwheel.and.lock +steeringwheel.arrow.trianglehead.counterclockwise.and.clockwise +steeringwheel.arrowtriangle.left +steeringwheel.arrowtriangle.right +steeringwheel.badge.exclamationmark +steeringwheel.badge.lock +steeringwheel.circle +steeringwheel.circle.fill +steeringwheel.exclamationmark +steeringwheel.road.lane +steeringwheel.road.lane.dashed +steeringwheel.slash +sterlingsign +sterlingsign.arrow.circlepath +sterlingsign.arrow.trianglehead.counterclockwise.rotate.90 +sterlingsign.bank.building +sterlingsign.bank.building.fill +sterlingsign.circle +sterlingsign.circle.fill +sterlingsign.gauge.chart.lefthalf.righthalf +sterlingsign.gauge.chart.leftthird.topthird.rightthird +sterlingsign.ring +sterlingsign.ring.dashed +sterlingsign.square +sterlingsign.square.fill +stethoscope +stethoscope.circle +stethoscope.circle.fill +stop +stop.circle +stop.circle.fill +stop.fill +stopwatch +stopwatch.fill +storefront +storefront.circle +storefront.circle.fill +storefront.fill +stove +stove.fill +strikethrough +strikethrough.double +stroke.line.diagonal +stroke.line.diagonal.slash +stroller +stroller.fill +studentdesk +suit.club +suit.club.fill +suit.diamond +suit.diamond.fill +suit.heart +suit.heart.fill +suit.spade +suit.spade.fill +suitcase +suitcase.cart +suitcase.cart.fill +suitcase.circle +suitcase.circle.fill +suitcase.fill +suitcase.rolling +suitcase.rolling.and.film +suitcase.rolling.and.film.circle +suitcase.rolling.and.film.circle.fill +suitcase.rolling.and.film.fill +suitcase.rolling.and.suitcase +suitcase.rolling.and.suitcase.circle +suitcase.rolling.and.suitcase.circle.fill +suitcase.rolling.and.suitcase.fill +suitcase.rolling.circle +suitcase.rolling.circle.fill +suitcase.rolling.fill +sum +sun.and.horizon +sun.and.horizon.circle +sun.and.horizon.circle.fill +sun.and.horizon.fill +sun.dust +sun.dust.circle +sun.dust.circle.fill +sun.dust.fill +sun.haze +sun.haze.circle +sun.haze.circle.fill +sun.haze.fill +sun.horizon +sun.horizon.circle +sun.horizon.circle.fill +sun.horizon.fill +sun.lefthalf.filled +sun.max +sun.max.circle +sun.max.circle.fill +sun.max.fill +sun.max.trianglebadge.exclamationmark +sun.max.trianglebadge.exclamationmark.fill +sun.min +sun.min.fill +sun.rain +sun.rain.circle +sun.rain.circle.fill +sun.rain.fill +sun.righthalf.filled +sun.snow +sun.snow.circle +sun.snow.circle.fill +sun.snow.fill +sunglasses +sunglasses.fill +sunrise +sunrise.circle +sunrise.circle.fill +sunrise.fill +sunset +sunset.circle +sunset.circle.fill +sunset.fill +surfboard +surfboard.fill +suspension.shock +suv.side +suv.side.air.circulate +suv.side.air.circulate.fill +suv.side.air.fresh +suv.side.air.fresh.fill +suv.side.and.exclamationmark +suv.side.and.exclamationmark.fill +suv.side.arrow.left.and.right +suv.side.arrow.left.and.right.fill +suv.side.arrowtriangle.down +suv.side.arrowtriangle.down.fill +suv.side.arrowtriangle.up +suv.side.arrowtriangle.up.arrowtriangle.down +suv.side.arrowtriangle.up.arrowtriangle.down.fill +suv.side.arrowtriangle.up.fill +suv.side.fill +suv.side.front.open +suv.side.front.open.crop +suv.side.front.open.crop.fill +suv.side.front.open.fill +suv.side.hill.descent.control +suv.side.hill.descent.control.fill +suv.side.hill.down +suv.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle +suv.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle.fill +suv.side.hill.down.fill +suv.side.hill.up +suv.side.hill.up.fill +suv.side.lock +suv.side.lock.fill +suv.side.lock.open +suv.side.lock.open.fill +suv.side.rear.open +suv.side.rear.open.crop +suv.side.rear.open.crop.fill +suv.side.rear.open.fill +suv.side.roof.cargo.carrier +suv.side.roof.cargo.carrier.fill +suv.side.roof.cargo.carrier.slash +suv.side.roof.cargo.carrier.slash.fill +swatchpalette +swatchpalette.fill +swedishkronasign +swedishkronasign.arrow.circlepath +swedishkronasign.arrow.trianglehead.counterclockwise.rotate.90 +swedishkronasign.bank.building +swedishkronasign.bank.building.fill +swedishkronasign.circle +swedishkronasign.circle.fill +swedishkronasign.gauge.chart.lefthalf.righthalf +swedishkronasign.gauge.chart.leftthird.topthird.rightthird +swedishkronasign.ring +swedishkronasign.ring.dashed +swedishkronasign.square +swedishkronasign.square.fill +swift +swiftdata +swirl.circle.righthalf.filled +swirl.circle.righthalf.filled.inverse +switch.2 +switch.programmable +switch.programmable.fill +switch.programmable.square +switch.programmable.square.fill +syringe +syringe.fill +t.bubble +t.bubble.fill +t.circle +t.circle.fill +t.square +t.square.fill +table +table.badge.more +table.badge.more.fill +table.fill +table.furniture +table.furniture.fill +tablecells +tablecells.badge.ellipsis +tablecells.badge.ellipsis.fill +tablecells.fill +tablecells.fill.badge.ellipsis +tachometer +tag +tag.circle +tag.circle.fill +tag.fill +tag.slash +tag.slash.fill +tag.square +tag.square.fill +taillight.fog +taillight.fog.fill +takeoutbag.and.cup.and.straw +takeoutbag.and.cup.and.straw.fill +target +teddybear +teddybear.fill +teletype +teletype.answer +teletype.answer.circle +teletype.answer.circle.fill +teletype.circle +teletype.circle.fill +tengesign +tengesign.arrow.circlepath +tengesign.arrow.trianglehead.counterclockwise.rotate.90 +tengesign.bank.building +tengesign.bank.building.fill +tengesign.circle +tengesign.circle.fill +tengesign.gauge.chart.lefthalf.righthalf +tengesign.gauge.chart.leftthird.topthird.rightthird +tengesign.ring +tengesign.ring.dashed +tengesign.square +tengesign.square.fill +tennis.racket +tennis.racket.circle +tennis.racket.circle.fill +tennisball +tennisball.circle +tennisball.circle.fill +tennisball.fill +tent +tent.2 +tent.2.circle +tent.2.circle.fill +tent.2.fill +tent.circle +tent.circle.fill +tent.fill +terminal +terminal.fill +testtube.2 +text.aligncenter +text.alignleft +text.alignright +text.and.command.macwindow +text.append +text.badge.checkmark +text.badge.minus +text.badge.plus +text.badge.star +text.badge.xmark +text.below.folder +text.below.folder.fill +text.below.photo +text.below.photo.fill +text.book.closed +text.book.closed.fill +text.bubble +text.bubble.badge.clock +text.bubble.badge.clock.fill +text.bubble.fill +text.cursor +text.document +text.document.fill +text.insert +text.justify +text.justify.leading +text.justify.left +text.justify.right +text.justify.trailing +text.justifyleft +text.justifyright +text.line.2.summary +text.line.2.summary.badge.xmark +text.line.3.summary +text.line.first.and.arrowtriangle.forward +text.line.last.and.arrowtriangle.forward +text.line.magnify +text.magnifyingglass +text.pad.header +text.pad.header.badge.clock +text.pad.header.badge.plus +text.page +text.page.badge.magnifyingglass +text.page.fill +text.page.slash +text.page.slash.fill +text.quote +text.rectangle +text.rectangle.fill +text.rectangle.page +text.rectangle.page.fill +text.redaction +text.square.filled +text.viewfinder +text.word.spacing +textbox +textformat +textformat.12 +textformat.12.km +textformat.12.my +textformat.123 +textformat.abc +textformat.abc.dottedunderline +textformat.alt +textformat.alt.bn +textformat.alt.gu +textformat.alt.kn +textformat.alt.ml +textformat.alt.mni +textformat.alt.mr +textformat.alt.or +textformat.alt.pa +textformat.alt.sat +textformat.alt.si +textformat.alt.ta +textformat.alt.te +textformat.bn +textformat.characters +textformat.characters.arrow.left.and.right +textformat.characters.arrow.left.and.right.bn +textformat.characters.arrow.left.and.right.gu +textformat.characters.arrow.left.and.right.kn +textformat.characters.arrow.left.and.right.ml +textformat.characters.arrow.left.and.right.mni +textformat.characters.arrow.left.and.right.mr +textformat.characters.arrow.left.and.right.or +textformat.characters.arrow.left.and.right.pa +textformat.characters.arrow.left.and.right.ru +textformat.characters.arrow.left.and.right.sat +textformat.characters.arrow.left.and.right.si +textformat.characters.arrow.left.and.right.ta +textformat.characters.arrow.left.and.right.te +textformat.characters.bn +textformat.characters.dottedunderline +textformat.characters.dottedunderline.bn +textformat.characters.dottedunderline.gu +textformat.characters.dottedunderline.kn +textformat.characters.dottedunderline.ml +textformat.characters.dottedunderline.mni +textformat.characters.dottedunderline.mr +textformat.characters.dottedunderline.or +textformat.characters.dottedunderline.pa +textformat.characters.dottedunderline.ru +textformat.characters.dottedunderline.sat +textformat.characters.dottedunderline.si +textformat.characters.dottedunderline.ta +textformat.characters.dottedunderline.te +textformat.characters.gu +textformat.characters.kn +textformat.characters.ml +textformat.characters.mni +textformat.characters.mr +textformat.characters.or +textformat.characters.pa +textformat.characters.ru +textformat.characters.sat +textformat.characters.si +textformat.characters.ta +textformat.characters.te +textformat.gu +textformat.kn +textformat.ml +textformat.mni +textformat.mr +textformat.numbers +textformat.numbers.bn +textformat.numbers.gu +textformat.numbers.km +textformat.numbers.kn +textformat.numbers.ml +textformat.numbers.mni +textformat.numbers.mr +textformat.numbers.my +textformat.numbers.or +textformat.numbers.pa +textformat.numbers.sat +textformat.numbers.te +textformat.or +textformat.pa +textformat.sat +textformat.si +textformat.size +textformat.size.bn +textformat.size.gu +textformat.size.kn +textformat.size.larger +textformat.size.larger.bn +textformat.size.larger.gu +textformat.size.larger.kn +textformat.size.larger.ml +textformat.size.larger.mni +textformat.size.larger.mr +textformat.size.larger.or +textformat.size.larger.pa +textformat.size.larger.sat +textformat.size.larger.si +textformat.size.larger.ta +textformat.size.larger.te +textformat.size.ml +textformat.size.mni +textformat.size.mr +textformat.size.or +textformat.size.pa +textformat.size.sat +textformat.size.si +textformat.size.smaller +textformat.size.smaller.bn +textformat.size.smaller.gu +textformat.size.smaller.kn +textformat.size.smaller.ml +textformat.size.smaller.mni +textformat.size.smaller.mr +textformat.size.smaller.or +textformat.size.smaller.pa +textformat.size.smaller.sat +textformat.size.smaller.si +textformat.size.smaller.ta +textformat.size.smaller.te +textformat.size.ta +textformat.size.te +textformat.subscript +textformat.subscript.bn +textformat.subscript.gu +textformat.subscript.kn +textformat.subscript.ml +textformat.subscript.mni +textformat.subscript.mr +textformat.subscript.or +textformat.subscript.pa +textformat.subscript.sat +textformat.subscript.si +textformat.subscript.ta +textformat.subscript.te +textformat.superscript +textformat.superscript.bn +textformat.superscript.gu +textformat.superscript.kn +textformat.superscript.ml +textformat.superscript.mni +textformat.superscript.mr +textformat.superscript.or +textformat.superscript.pa +textformat.superscript.sat +textformat.superscript.si +textformat.superscript.ta +textformat.superscript.te +textformat.ta +textformat.te +theatermask.and.paintbrush +theatermask.and.paintbrush.fill +theatermasks +theatermasks.circle +theatermasks.circle.fill +theatermasks.fill +thermometer +thermometer.and.ellipsis +thermometer.and.liquid.waves +thermometer.and.liquid.waves.snowflake +thermometer.and.liquid.waves.trianglebadge.exclamationmark +thermometer.brakesignal +thermometer.gauge.open +thermometer.high +thermometer.low +thermometer.medium +thermometer.medium.slash +thermometer.snowflake +thermometer.snowflake.circle +thermometer.snowflake.circle.fill +thermometer.sun +thermometer.sun.circle +thermometer.sun.circle.fill +thermometer.sun.fill +thermometer.tirepressure +thermometer.transmission +thermometer.variable +thermometer.variable.and.figure +thermometer.variable.and.figure.circle +thermometer.variable.and.figure.circle.fill +thermometer.variable.badge.clock +thermometer.variable.badge.play +ticket +ticket.circle +ticket.circle.fill +ticket.fill +timelapse +timeline.selection +timer +timer.circle +timer.circle.fill +timer.square +tire +tire.badge.snowflake +tirepressure +togglepower +toilet +toilet.circle +toilet.circle.fill +toilet.fill +tornado +tornado.circle +tornado.circle.fill +tortoise +tortoise.circle +tortoise.circle.fill +tortoise.fill +torus +touchid +tow.hitch +tow.hitch.exclamationmark +tow.hitch.exclamationmark.fill +tow.hitch.fill +traction.control.tirepressure +traction.control.tirepressure.exclamationmark +traction.control.tirepressure.slash +train.side.front.car +train.side.middle.car +train.side.rear.car +tram +tram.card +tram.card.fill +tram.circle +tram.circle.fill +tram.fill +tram.fill.tunnel +tram.tunnel.fill +translate +transmission +trapezoid.and.line.horizontal +trapezoid.and.line.horizontal.fill +trapezoid.and.line.vertical +trapezoid.and.line.vertical.fill +trash +trash.circle +trash.circle.fill +trash.fill +trash.slash +trash.slash.circle +trash.slash.circle.fill +trash.slash.fill +trash.slash.square +trash.slash.square.fill +trash.square +trash.square.fill +tray +tray.2 +tray.2.fill +tray.and.arrow.down +tray.and.arrow.down.fill +tray.and.arrow.up +tray.and.arrow.up.fill +tray.badge +tray.badge.fill +tray.circle +tray.circle.fill +tray.fill +tray.full +tray.full.fill +tree +tree.circle +tree.circle.fill +tree.fill +triangle +triangle.bottomhalf.filled +triangle.circle +triangle.circle.fill +triangle.fill +triangle.inset.filled +triangle.lefthalf.fill +triangle.lefthalf.filled +triangle.righthalf.fill +triangle.righthalf.filled +triangle.tophalf.filled +triangleshape +triangleshape.fill +trophy +trophy.circle +trophy.circle.fill +trophy.fill +tropicalstorm +tropicalstorm.circle +tropicalstorm.circle.fill +truck.box +truck.box.badge.clock +truck.box.badge.clock.fill +truck.box.fill +truck.pickup.side +truck.pickup.side.air.circulate +truck.pickup.side.air.circulate.fill +truck.pickup.side.air.fresh +truck.pickup.side.air.fresh.fill +truck.pickup.side.and.exclamationmark +truck.pickup.side.and.exclamationmark.fill +truck.pickup.side.arrow.left.and.right +truck.pickup.side.arrow.left.and.right.fill +truck.pickup.side.arrowtriangle.down +truck.pickup.side.arrowtriangle.down.fill +truck.pickup.side.arrowtriangle.up +truck.pickup.side.arrowtriangle.up.arrowtriangle.down +truck.pickup.side.arrowtriangle.up.arrowtriangle.down.fill +truck.pickup.side.arrowtriangle.up.fill +truck.pickup.side.fill +truck.pickup.side.front.open +truck.pickup.side.front.open.crop +truck.pickup.side.front.open.crop.fill +truck.pickup.side.front.open.fill +truck.pickup.side.hill.down +truck.pickup.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle +truck.pickup.side.hill.down.and.gauge.open.with.lines.needle.25percent.and.arrowtriangle.fill +truck.pickup.side.hill.down.fill +truck.pickup.side.hill.up +truck.pickup.side.hill.up.fill +truck.pickup.side.lock +truck.pickup.side.lock.fill +truck.pickup.side.lock.open +truck.pickup.side.lock.open.fill +truck.side.hill.descent.control +truck.side.hill.descent.control.fill +truck.side.roof.cargo.carrier +truck.side.roof.cargo.carrier.fill +truck.side.roof.cargo.carrier.slash +truck.side.roof.cargo.carrier.slash.fill +tsa +tsa.circle +tsa.circle.fill +tsa.slash +tshirt +tshirt.circle +tshirt.circle.fill +tshirt.fill +tugriksign +tugriksign.arrow.circlepath +tugriksign.arrow.trianglehead.counterclockwise.rotate.90 +tugriksign.bank.building +tugriksign.bank.building.fill +tugriksign.circle +tugriksign.circle.fill +tugriksign.gauge.chart.lefthalf.righthalf +tugriksign.gauge.chart.leftthird.topthird.rightthird +tugriksign.ring +tugriksign.ring.dashed +tugriksign.square +tugriksign.square.fill +tuningfork +turkishlirasign +turkishlirasign.arrow.circlepath +turkishlirasign.arrow.trianglehead.counterclockwise.rotate.90 +turkishlirasign.bank.building +turkishlirasign.bank.building.fill +turkishlirasign.circle +turkishlirasign.circle.fill +turkishlirasign.gauge.chart.lefthalf.righthalf +turkishlirasign.gauge.chart.leftthird.topthird.rightthird +turkishlirasign.ring +turkishlirasign.ring.dashed +turkishlirasign.square +turkishlirasign.square.fill +tv +tv.and.hifispeaker.fill +tv.and.mediabox +tv.and.mediabox.fill +tv.badge.wifi +tv.badge.wifi.fill +tv.circle +tv.circle.fill +tv.fill +tv.inset.filled +tv.music.note +tv.music.note.fill +tv.slash +tv.slash.fill +u.circle +u.circle.fill +u.square +u.square.fill +uiwindow.split.2x1 +umbrella +umbrella.circle +umbrella.circle.fill +umbrella.fill +umbrella.gauge.open +umbrella.percent +umbrella.percent.fill +umbrella.sensor.tag.radiowaves.left.and.right +umbrella.sensor.tag.radiowaves.left.and.right.fill +underline +underline.double +v.circle +v.circle.fill +v.square +v.square.fill +vent.heat.waves.upward +vial.viewfinder +video +video.and.waveform +video.and.waveform.fill +video.badge.checkmark +video.badge.ellipsis +video.badge.plus +video.badge.plus.fill +video.badge.waveform +video.badge.waveform.fill +video.bubble +video.bubble.fill +video.bubble.left +video.bubble.left.fill +video.circle +video.circle.fill +video.doorbell +video.doorbell.fill +video.fill +video.fill.badge.checkmark +video.fill.badge.ellipsis +video.fill.badge.plus +video.slash +video.slash.circle +video.slash.circle.fill +video.slash.fill +video.square +video.square.fill +videoprojector +videoprojector.fill +view.2d +view.3d +viewfinder +viewfinder.circle +viewfinder.circle.fill +viewfinder.rectangular +viewfinder.trianglebadge.exclamationmark +vision.pro +vision.pro.and.arrow.forward +vision.pro.and.arrow.forward.fill +vision.pro.badge.checkmark +vision.pro.badge.checkmark.fill +vision.pro.badge.exclamationmark +vision.pro.badge.exclamationmark.fill +vision.pro.badge.play +vision.pro.badge.play.fill +vision.pro.circle +vision.pro.circle.fill +vision.pro.fill +vision.pro.slash +vision.pro.slash.circle +vision.pro.slash.circle.fill +vision.pro.slash.fill +vision.pro.trianglebadge.exclamationmark +vision.pro.trianglebadge.exclamationmark.fill +visionpro +visionpro.and.arrow.forward +visionpro.and.arrow.forward.fill +visionpro.badge.exclamationmark +visionpro.badge.exclamationmark.fill +visionpro.badge.play +visionpro.badge.play.fill +visionpro.circle +visionpro.circle.fill +visionpro.fill +visionpro.slash +visionpro.slash.circle +visionpro.slash.circle.fill +visionpro.slash.fill +voiceover +volleyball +volleyball.circle +volleyball.circle.fill +volleyball.fill +w.circle +w.circle.fill +w.square +w.square.fill +wake +wake.circle +wake.circle.fill +wallet.bifold +wallet.bifold.fill +wallet.pass +wallet.pass.fill +wallet.sensor.tag.radiowaves.left.and.right +wallet.sensor.tag.radiowaves.left.and.right.fill +wand.and.outline +wand.and.outline.inverse +wand.and.rays +wand.and.rays.inverse +wand.and.sparkles +wand.and.sparkles.inverse +wand.and.stars +wand.and.stars.inverse +warninglight +warninglight.fill +washer +washer.circle +washer.circle.fill +washer.fill +watch.analog +watchface.applewatch.case +water.waves +water.waves.and.arrow.down +water.waves.and.arrow.down.trianglebadge.exclamationmark +water.waves.and.arrow.trianglehead.down +water.waves.and.arrow.trianglehead.down.trianglebadge.exclamationmark +water.waves.and.arrow.trianglehead.up +water.waves.and.arrow.up +water.waves.slash +waterbottle +waterbottle.fill +wave.3.backward +wave.3.backward.circle +wave.3.backward.circle.fill +wave.3.down +wave.3.down.car.side +wave.3.down.car.side.fill +wave.3.down.circle +wave.3.down.circle.fill +wave.3.down.convertible.side +wave.3.down.convertible.side.fill +wave.3.down.pickup.side +wave.3.down.pickup.side.fill +wave.3.down.suv.side +wave.3.down.suv.side.fill +wave.3.forward +wave.3.forward.circle +wave.3.forward.circle.fill +wave.3.left +wave.3.left.circle +wave.3.left.circle.fill +wave.3.right +wave.3.right.circle +wave.3.right.circle.fill +wave.3.up +wave.3.up.circle +wave.3.up.circle.fill +waveform +waveform.and.magnifyingglass +waveform.and.mic +waveform.and.person.filled +waveform.badge.checkmark +waveform.badge.exclamationmark +waveform.badge.magnifyingglass +waveform.badge.mic +waveform.badge.microphone +waveform.badge.minus +waveform.badge.plus +waveform.badge.xmark +waveform.circle +waveform.circle.fill +waveform.low +waveform.mid +waveform.path +waveform.path.badge.minus +waveform.path.badge.plus +waveform.path.ecg +waveform.path.ecg.magnifyingglass +waveform.path.ecg.rectangle +waveform.path.ecg.rectangle.fill +waveform.path.ecg.text +waveform.path.ecg.text.clipboard +waveform.path.ecg.text.clipboard.fill +waveform.path.ecg.text.page +waveform.path.ecg.text.page.fill +waveform.slash +web.camera +web.camera.fill +wheelchair +widget.extralarge +widget.extralarge.badge.plus +widget.large +widget.large.badge.plus +widget.medium +widget.medium.badge.plus +widget.small +widget.small.badge.plus +wifi +wifi.badge.lock +wifi.circle +wifi.circle.fill +wifi.exclamationmark +wifi.exclamationmark.circle +wifi.exclamationmark.circle.fill +wifi.router +wifi.router.fill +wifi.slash +wifi.square +wifi.square.fill +wind +wind.circle +wind.circle.fill +wind.snow +wind.snow.circle +wind.snow.circle.fill +window.awning +window.awning.closed +window.casement +window.casement.closed +window.ceiling +window.ceiling.closed +window.horizontal +window.horizontal.closed +window.shade.closed +window.shade.open +window.vertical.closed +window.vertical.open +windshield.front.and.fluid +windshield.front.and.fluid.and.spray +windshield.front.and.heat.waves +windshield.front.and.spray +windshield.front.and.wiper +windshield.front.and.wiper.and.drop +windshield.front.and.wiper.and.spray +windshield.front.and.wiper.exclamationmark +windshield.front.and.wiper.intermittent +windshield.rear.and.fluid +windshield.rear.and.fluid.and.spray +windshield.rear.and.heat.waves +windshield.rear.and.spray +windshield.rear.and.wiper +windshield.rear.and.wiper.and.drop +windshield.rear.and.wiper.and.spray +windshield.rear.and.wiper.exclamationmark +windshield.rear.and.wiper.intermittent +wineglass +wineglass.fill +wonsign +wonsign.arrow.circlepath +wonsign.arrow.trianglehead.counterclockwise.rotate.90 +wonsign.bank.building +wonsign.bank.building.fill +wonsign.circle +wonsign.circle.fill +wonsign.gauge.chart.lefthalf.righthalf +wonsign.gauge.chart.leftthird.topthird.rightthird +wonsign.ring +wonsign.ring.dashed +wonsign.square +wonsign.square.fill +wrench +wrench.adjustable +wrench.adjustable.fill +wrench.and.screwdriver +wrench.and.screwdriver.fill +wrench.fill +wrongwaysign +wrongwaysign.fill +x.circle +x.circle.fill +x.square +x.square.fill +x.squareroot +xbox.logo +xmark +xmark.app +xmark.app.fill +xmark.bin +xmark.bin.circle +xmark.bin.circle.fill +xmark.bin.fill +xmark.circle +xmark.circle.badge.airplane +xmark.circle.badge.airplane.fill +xmark.circle.fill +xmark.diamond +xmark.diamond.fill +xmark.icloud +xmark.icloud.fill +xmark.octagon +xmark.octagon.fill +xmark.rectangle +xmark.rectangle.fill +xmark.rectangle.portrait +xmark.rectangle.portrait.fill +xmark.seal +xmark.seal.fill +xmark.shield +xmark.shield.fill +xmark.square +xmark.square.fill +xmark.triangle.circle.square +xmark.triangle.circle.square.fill +xserve +xserve.raid +y.circle +y.circle.fill +y.square +y.square.fill +yensign +yensign.arrow.circlepath +yensign.arrow.trianglehead.counterclockwise.rotate.90 +yensign.bank.building +yensign.bank.building.fill +yensign.circle +yensign.circle.fill +yensign.gauge.chart.lefthalf.righthalf +yensign.gauge.chart.leftthird.topthird.rightthird +yensign.ring +yensign.ring.dashed +yensign.square +yensign.square.fill +yieldsign +yieldsign.fill +z.circle +z.circle.fill +z.square +z.square.fill +zipper.page +zl.button.roundedtop.horizontal +zl.button.roundedtop.horizontal.fill +zl.rectangle.roundedtop +zl.rectangle.roundedtop.fill +zr.button.roundedtop.horizontal +zr.button.roundedtop.horizontal.fill +zr.rectangle.roundedtop +zr.rectangle.roundedtop.fill +zzz +""" +} + +private struct SkillSymbolPicker: View { + @Binding var selection: String + @Environment(\.dismiss) private var dismiss + @Environment(\.themePalette) private var palette + @State private var query = "" + + private let columns = [ + GridItem(.adaptive(minimum: 44), spacing: 6), + ] + + private var filtered: [String] { + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return SFSymbolCatalog.names } + return SFSymbolCatalog.names.filter { $0.localizedCaseInsensitiveContains(trimmed) } + } + + var body: some View { + NavigationStack { + ScrollView { + LazyVGrid(columns: columns, spacing: 6) { + ForEach(filtered, id: \.self) { symbol in + Button { + selection = symbol + dismiss() + } label: { + Image(systemName: symbol) + .font(.system(size: 18, weight: .medium)) + .foregroundStyle( + symbol == selection ? palette.textOnAccent : palette.textPrimary + ) + .frame(width: 44, height: 44) + .background( + symbol == selection ? palette.accent : palette.surfaceElevated, + in: RoundedRectangle(cornerRadius: 8, style: .continuous) + ) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(symbol)) + } + } + .padding(.horizontal, Spacing.md) + .padding(.vertical, Spacing.sm) + } + .background(palette.background) + .navigationTitle("skills.editor.icon") + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $query, prompt: Text("skills.editor.iconSearch")) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("common.done") { dismiss() } + } + } + } + .presentationDetents([.large]) + } +} diff --git a/OSGKeyboard/Views/AIClipboardSkillLayoutDemoView.swift b/OSGKeyboard/Views/AIClipboardSkillLayoutDemoView.swift new file mode 100644 index 0000000..40f0e05 --- /dev/null +++ b/OSGKeyboard/Views/AIClipboardSkillLayoutDemoView.swift @@ -0,0 +1,136 @@ +// AIClipboardSkillLayoutDemoView.swift +// OSGKeyboard · Main App (DEBUG-only) +// +// Interactive layout preview of AI Agent clipboard skills on the real +// `AIKeyboardView`. Launch with `--ai-skills-demo` and optional +// `--skills-count=N` (1...8). + +#if DEBUG +import SwiftUI +import OSGKeyboardShared + +struct AIClipboardSkillLayoutDemoView: View { + @StateObject private var state = KeyboardState() + @StateObject private var typing = TypingSessionController() + @State private var count: Int = Self.initialCount + + var body: some View { + VStack(spacing: 0) { + controls + Spacer(minLength: 0) + AIKeyboardView( + state: state, + typing: typing, + onInsert: { _ in } + ) + .background(Palette.light.background) + } + .background(Color(red: 0.06, green: 0.06, blue: 0.07).ignoresSafeArea()) + .environment(\.locale, Locale(identifier: "zh-Hans")) + .preferredColorScheme(.light) + .onAppear { apply(count) } + .onChange(of: count) { _, newValue in apply(newValue) } + } + + private var controls: some View { + VStack(spacing: 10) { + Text("技能布局预览") + .font(.headline) + .foregroundStyle(.white) + HStack(spacing: 16) { + Button { + count = max(1, count - 1) + } label: { + Image(systemName: "minus.circle.fill") + .font(.system(size: 28)) + } + Text("\(count) 个") + .font(.title2.monospacedDigit().weight(.semibold)) + .foregroundStyle(.white) + .frame(minWidth: 72) + Button { + count = min(8, count + 1) + } label: { + Image(systemName: "plus.circle.fill") + .font(.system(size: 28)) + } + } + .foregroundStyle(.green) + HStack(spacing: 8) { + ForEach([3, 5, 8], id: \.self) { n in + Button("\(n)") { + count = n + } + .buttonStyle(.borderedProminent) + .tint(count == n ? .green : .gray) + } + } + } + .padding(.top, 56) + .padding(.bottom, 16) + } + + private func apply(_ count: Int) { + let clamped = min(max(count, 1), 8) + AIKeyboardView.debugPreviewSkills = Self.previewSkills(count: clamped) + state.surface = .ai + state.aiServiceAvailable = true + state.micDisabled = false + state.layoutWidth = 390 + state.usesIPadLayoutMetrics = false + state.clipboardHistoryEnabled = true + state.enabledClipboardSkillIDs = Array( + AIClipboardSkillCatalog.catalog.map(\.id).prefix(clamped) + ) + state.aiSession.enter() + } + + private static var initialCount: Int { + let prefix = "--skills-count=" + if let arg = ProcessInfo.processInfo.arguments.first(where: { $0.hasPrefix(prefix) }), + let value = Int(arg.dropFirst(prefix.count)), + (1...8).contains(value) { + return value + } + return 5 + } + + private static func previewSkills(count: Int) -> [AIClipboardSkill] { + let catalog = AIClipboardSkillCatalog.catalog + if count <= catalog.count { + return Array(catalog.prefix(count)) + } + var extras: [AIClipboardSkill] = [ + AIClipboardSkill( + id: "preview-polish", + systemImage: "wand.and.stars", + titleKey: "keyboard.ai.skill.previewPolish", + cardTitleKey: "skills.reply.name", + descriptionKey: "skills.reply.description", + kind: .transform, + isDefault: false + ), + AIClipboardSkill( + id: "preview-ideas", + systemImage: "lightbulb.fill", + titleKey: "keyboard.ai.skill.previewIdeas", + cardTitleKey: "skills.summarize.name", + descriptionKey: "skills.summarize.description", + kind: .transform, + isDefault: false + ), + AIClipboardSkill( + id: "preview-tone", + systemImage: "theatermasks.fill", + titleKey: "keyboard.ai.skill.previewTone", + cardTitleKey: "skills.translate.name", + descriptionKey: "skills.translate.description", + kind: .transform, + isDefault: false + ), + ] + extras = Array(extras.prefix(count - catalog.count)) + return catalog + extras + } +} +#endif diff --git a/OSGKeyboard/Views/CatalogCardChrome.swift b/OSGKeyboard/Views/CatalogCardChrome.swift new file mode 100644 index 0000000..38dc16c --- /dev/null +++ b/OSGKeyboard/Views/CatalogCardChrome.swift @@ -0,0 +1,31 @@ +// CatalogCardChrome.swift +// OSGKeyboard · Main App +// +// Shared edit / selected badges for style and skill cards. + +import SwiftUI +import OSGKeyboardShared + +enum CatalogCardChrome { + static let badgeSize: CGFloat = 30 + static let editIconSize: CGFloat = 15 + static let checkIconSize: CGFloat = 21 + static let editHitSize: CGFloat = 44 + + static func editIcon(palette: ThemePalette) -> some View { + Image(systemName: "pencil") + .font(.system(size: editIconSize, weight: .semibold)) + .foregroundStyle(palette.textSecondary) + .frame(width: badgeSize, height: badgeSize) + .background(palette.background.opacity(0.75), in: Circle()) + } + + static func checkIcon(palette: ThemePalette) -> some View { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: checkIconSize, weight: .semibold)) + .foregroundStyle(palette.accent) + .padding(Spacing.sm) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + .allowsHitTesting(false) + } +} diff --git a/OSGKeyboard/Views/PolishStylesView.swift b/OSGKeyboard/Views/PolishStylesView.swift index 73dd189..74fd730 100644 --- a/OSGKeyboard/Views/PolishStylesView.swift +++ b/OSGKeyboard/Views/PolishStylesView.swift @@ -142,26 +142,14 @@ struct PolishStylesView: View { editingPack = pack } } label: { - Image(systemName: pack.kind == .builtin ? "eye" : "pencil") - .font(.system(size: 13, weight: .semibold)) - .foregroundStyle(palette.textSecondary) - .frame(width: 30, height: 30) - .background(palette.background.opacity(0.75), in: Circle()) + CatalogCardChrome.editIcon(palette: palette) } .padding(Spacing.sm) .buttonStyle(.plain) - .accessibilityLabel( - Text(pack.kind == .builtin ? "polishStyles.viewPrompt" : "polishStyles.edit") - ) + .accessibilityLabel(Text("polishStyles.edit")) if isSelected { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 21, weight: .semibold)) - .foregroundStyle(palette.accent) - .background(Color.white, in: Circle()) - .padding(Spacing.sm) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) - .allowsHitTesting(false) + CatalogCardChrome.checkIcon(palette: palette) } } .background( diff --git a/OSGKeyboard/en.lproj/Localizable.strings b/OSGKeyboard/en.lproj/Localizable.strings index efaeca5..cb7d991 100644 --- a/OSGKeyboard/en.lproj/Localizable.strings +++ b/OSGKeyboard/en.lproj/Localizable.strings @@ -435,7 +435,10 @@ "skills.enabled.section" = "In use (%d/%d)"; "skills.enabled.empty" = "No skills on the keyboard. Turn one on from the list below."; "skills.available.section" = "Available"; -"skills.clipboardHistory.banner" = "Turn on Clipboard History in Settings to use these skills after you copy text."; +"skills.clipboard.guide.title" = "Clipboard access needed"; +"skills.clipboard.guide.body" = "Turn on Clipboard History in the app, then allow paste access in iOS Settings, so these skills can run after you copy text."; +"skills.clipboard.guide.openAppSettings" = "Open Clipboard settings"; +"skills.clipboard.guide.openSystemSettings" = "Open iOS Settings"; "skills.detail.title" = "Skill"; "skills.badge.default" = "Default skill"; "skills.action.turnOff" = "Turn off"; @@ -452,9 +455,43 @@ "skills.translate.description" = "Translate the copied text using your keyboard language setting."; "skills.extractTodos.name" = "Extract tasks"; "skills.extractTodos.description" = "Extract to-dos from copied text into Reminders."; +"skills.extractEvents.name" = "Extract events"; +"skills.extractEvents.description" = "Extract events from copied text into Calendar."; +"skills.navigate.name" = "Navigate"; +"skills.navigate.description" = "Find an address in the copied text and start driving directions. Uses Amap if installed, then Baidu Maps, then Apple Maps."; "skills.install.lead" = "Add it, then confirm. Don’t rename it."; "skills.install.openShortcuts" = "Add Shortcut"; "skills.install.confirmAdded" = "I've added it"; +"skills.add" = "Add skill"; +"skills.edit" = "Edit skill"; +"skills.custom.description" = "Custom clipboard skill"; +"skills.editor.name" = "Name"; +"skills.editor.namePlaceholder" = "Skill name"; +"skills.editor.summary" = "About"; +"skills.editor.summaryPlaceholder" = "Shown on the skill card"; +"skills.editor.icon" = "Icon"; +"skills.editor.iconChoose" = "Choose SF Symbol"; +"skills.editor.iconSearch" = "Search symbols"; +"skills.editor.prompt" = "Processing prompt"; +"skills.editor.thinking" = "Thinking"; +"skills.editor.thinkingHint" = "Off by default. Turn on only when you want slower, deeper reasoning for this skill."; +"skills.editor.shortcut" = "Shortcut"; +"skills.editor.linkPlaceholder" = "https://www.icloud.com/shortcuts/…"; +"skills.editor.shortcutNamePlaceholder" = "Shortcut name (can differ from the skill name)"; +"skills.editor.shortcutHint" = "Paste an iCloud share link. The published Shortcut name is filled in automatically and you can change it. Don’t rename it in the Shortcuts app after adding."; +"skills.editor.lookingUp" = "Looking up Shortcut name…"; +"skills.editor.resolvedName" = "Will run: %@"; +"skills.editor.lookupFailed" = "Couldn’t read the Shortcut name. Check the link, or type the name yourself."; +"skills.delete.title" = "Delete this skill?"; +"skills.delete.message" = "It will be removed from this device. The Shortcut in the Shortcuts app is not deleted."; +"skills.error.title" = "Couldn’t save skill"; +"skills.error.emptyName" = "Enter a skill name."; +"skills.error.emptyPrompt" = "The prompt cannot be empty."; +"skills.error.emptyShortcutName" = "Enter the Shortcut name used to run it."; +"skills.error.invalidLink" = "Enter a valid iCloud Shortcuts link."; +"skills.error.emptyIcon" = "Choose an icon."; +"skills.error.promptTooLong" = "The prompt can contain up to 6,000 characters."; +"skills.error.generic" = "Try again."; /* Polish style packs */ "polishStyles.title" = "Polish styles"; diff --git a/OSGKeyboard/zh-Hans.lproj/Localizable.strings b/OSGKeyboard/zh-Hans.lproj/Localizable.strings index 891f7b8..87a21f0 100644 --- a/OSGKeyboard/zh-Hans.lproj/Localizable.strings +++ b/OSGKeyboard/zh-Hans.lproj/Localizable.strings @@ -434,7 +434,10 @@ "skills.enabled.section" = "使用中(%d/%d)"; "skills.enabled.empty" = "键盘上还没有技能。从下方列表打开一个即可。"; "skills.available.section" = "可添加"; -"skills.clipboardHistory.banner" = "请先在设置中打开剪贴板历史,复制文字后才能在键盘上使用这些技能。"; +"skills.clipboard.guide.title" = "需要剪贴板权限"; +"skills.clipboard.guide.body" = "请先在 App 里打开剪贴板历史,再到系统设置中允许粘贴授权,复制后才能使用这些技能。"; +"skills.clipboard.guide.openAppSettings" = "打开剪贴板设置"; +"skills.clipboard.guide.openSystemSettings" = "打开系统设置"; "skills.detail.title" = "技能"; "skills.badge.default" = "默认技能"; "skills.action.turnOff" = "关闭"; @@ -451,9 +454,43 @@ "skills.translate.description" = "按键盘目标语言翻译复制的内容。"; "skills.extractTodos.name" = "提取待办"; "skills.extractTodos.description" = "从复制内容提取待办,写入提醒事项。"; +"skills.extractEvents.name" = "提取日程"; +"skills.extractEvents.description" = "从复制内容提取日程,写入日历。"; +"skills.navigate.name" = "导航"; +"skills.navigate.description" = "从复制内容识别地址并开始驾车导航。优先高德,其次百度,最后 Apple 地图。"; "skills.install.lead" = "添加后点「我已添加」,请勿改名。"; "skills.install.openShortcuts" = "添加捷径"; "skills.install.confirmAdded" = "我已添加"; +"skills.add" = "添加技能"; +"skills.edit" = "编辑技能"; +"skills.custom.description" = "自定义剪贴板技能"; +"skills.editor.name" = "名称"; +"skills.editor.namePlaceholder" = "技能名称"; +"skills.editor.summary" = "介绍"; +"skills.editor.summaryPlaceholder" = "显示在技能卡片上"; +"skills.editor.icon" = "图标"; +"skills.editor.iconChoose" = "选择 SF Symbol"; +"skills.editor.iconSearch" = "搜索符号"; +"skills.editor.prompt" = "文本处理提示词"; +"skills.editor.thinking" = "思考"; +"skills.editor.thinkingHint" = "默认关闭。仅在需要该技能更慢、更深的推理时开启。"; +"skills.editor.shortcut" = "捷径"; +"skills.editor.linkPlaceholder" = "https://www.icloud.com/shortcuts/…"; +"skills.editor.shortcutNamePlaceholder" = "捷径名称(可与技能名称不同)"; +"skills.editor.shortcutHint" = "粘贴 iCloud 分享链接。发布名称会自动填入,也可以自行修改。添加到「快捷指令」后请勿改名。"; +"skills.editor.lookingUp" = "正在读取捷径名称…"; +"skills.editor.resolvedName" = "将运行:%@"; +"skills.editor.lookupFailed" = "无法读取捷径名称。请检查链接,或手动填写名称。"; +"skills.delete.title" = "删除此技能?"; +"skills.delete.message" = "会从本机移除。快捷指令 App 里的捷径不会被删除。"; +"skills.error.title" = "无法保存技能"; +"skills.error.emptyName" = "请输入技能名称。"; +"skills.error.emptyPrompt" = "提示词不能为空。"; +"skills.error.emptyShortcutName" = "请填写用于运行的捷径名称。"; +"skills.error.invalidLink" = "请输入有效的 iCloud 捷径链接。"; +"skills.error.emptyIcon" = "请选择图标。"; +"skills.error.promptTooLong" = "提示词最多 6,000 字。"; +"skills.error.generic" = "请再试一次。"; /* 润色风格包 */ "polishStyles.title" = "润色风格"; diff --git a/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift b/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift index 6cbe42f..b0ea8e9 100644 --- a/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift +++ b/OSGKeyboardExt/Services/AIKeyboardCoordinator.swift @@ -90,7 +90,7 @@ final class AIKeyboardCoordinator { if case .materialUnavailable = resolution { AIAgentShortcutRun.trace("keyboard.submit rejected clipboardUnavailable skill=\(skill.id)") } - submitResolvedPrompt(resolution) + submitResolvedPrompt(resolution, thinkingEnabled: skill.thinkingEnabled) } /// Tap an idle hint card: resolve its material, skip the mic, ask the host. @@ -114,7 +114,10 @@ final class AIKeyboardCoordinator { } } - private func submitResolvedPrompt(_ resolution: AIClipboardPrompt.Resolution) { + private func submitResolvedPrompt( + _ resolution: AIClipboardPrompt.Resolution, + thinkingEnabled: Bool? = nil + ) { guard case .ready(let prompt) = resolution else { // The clipboard window closed between rendering and this tap. state.pendingClipboardSkillID = nil @@ -130,7 +133,8 @@ final class AIKeyboardCoordinator { } let disposition = flow.submitAIQuestion( text: prompt, - conversationID: conversationID + conversationID: conversationID, + thinkingEnabled: thinkingEnabled ) if case .rejected(let rejection) = disposition { state.pendingClipboardSkillID = nil @@ -236,14 +240,37 @@ final class AIKeyboardCoordinator { private var isPendingExportSkill: Bool { guard let id = state.pendingClipboardSkillID else { return false } - return AIClipboardSkillCatalog.skill(id: id)?.kind == .export + return resolvedSkill(id: id)?.kind == .export } - /// Parse extract-todos. Empty → in-keyboard tip, stay in the host app. - /// Titles → hand off to the host to run the companion Shortcut. + private func resolvedSkill(id: String) -> AIClipboardSkill? { + AIClipboardSkillCatalog.skill( + id: id, + userCatalog: AppGroupStore().agentUserSkillCatalog + ) + } + + /// Parse an export skill. Empty → in-keyboard tip, stay in the host app. + /// Lines → hand off to the host to run the companion Shortcut. private func finishExportSkill(answer: String) { let source = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text - let items = AITodoExtraction.items(from: answer, sourceClipboard: source) + let skillID = state.pendingClipboardSkillID + let items: [String] + let emptyTipKey: String + switch skillID { + case AIClipboardSkillCatalog.extractEventsID: + items = AIEventExtraction.lines(from: answer, sourceClipboard: source) + emptyTipKey = "keyboard.ai.skill.noEvents" + case AIClipboardSkillCatalog.extractTodosID: + items = AITodoExtraction.items(from: answer, sourceClipboard: source) + emptyTipKey = "keyboard.ai.skill.noTodos" + case AIClipboardSkillCatalog.navigateID: + items = AIAddressExtraction.lines(from: answer, sourceClipboard: source) + emptyTipKey = "keyboard.ai.skill.noAddress" + default: + items = AIGenericSkillExport.items(from: answer) + emptyTipKey = "keyboard.ai.skill.noExportItems" + } AIAgentShortcutRun.traceBody("keyboard.llmRaw", answer) AIAgentShortcutRun.trace( "keyboard.parse items=\(items.count) clipboardChars=\(source?.count ?? 0)" @@ -253,7 +280,6 @@ final class AIKeyboardCoordinator { AIAgentShortcutRun.traceBody("keyboard.parsedTitles", items.joined(separator: "\n")) } #endif - let skillID = state.pendingClipboardSkillID state.pendingClipboardSkillID = nil if state.aiSession.isBusy { state.aiSession.cancelCurrentWork() @@ -263,11 +289,11 @@ final class AIKeyboardCoordinator { } guard !items.isEmpty else { AIAgentShortcutRun.trace("keyboard.parse empty — skip Shortcuts") - state.skillTipText = ExtL10n.string("keyboard.ai.skill.noTodos") + state.skillTipText = ExtL10n.string(emptyTipKey) return } guard let skillID, - AIClipboardSkillCatalog.skill(id: skillID)?.shortcutName != nil else { + resolvedSkill(id: skillID)?.shortcutName != nil else { AIAgentShortcutRun.trace("keyboard.parse missingShortcut skill=\(skillID ?? "nil")") state.skillTipText = ExtL10n.string("keyboard.ai.skill.shortcutMissing") return diff --git a/OSGKeyboardExt/Services/KeyboardFlowCoordinator.swift b/OSGKeyboardExt/Services/KeyboardFlowCoordinator.swift index 261305f..0e52882 100644 --- a/OSGKeyboardExt/Services/KeyboardFlowCoordinator.swift +++ b/OSGKeyboardExt/Services/KeyboardFlowCoordinator.swift @@ -670,12 +670,17 @@ final class KeyboardFlowCoordinator { func submitAIQuestion( text: String, - conversationID: UUID + conversationID: UUID, + thinkingEnabled: Bool? = nil ) -> FlowUtteranceStartDisposition { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return .rejected(.pipelineBusy) } return startUtterance( - .aiQuestion(conversationID: conversationID, prefilledQuestion: trimmed) + .aiQuestion( + conversationID: conversationID, + prefilledQuestion: trimmed, + thinkingEnabled: thinkingEnabled + ) ) } @@ -1868,6 +1873,7 @@ final class KeyboardFlowCoordinator { utteranceMode: .aiQuestion, aiConversationID: currentUtteranceRequest?.aiConversationID, aiQuestionText: text, + aiThinkingEnabled: currentUtteranceRequest?.aiThinkingEnabled, startDeadlineAt: currentStartDeadlineAt ) FlowSessionBridge.writeCommand(command) diff --git a/OSGKeyboardExt/Views/AIKeyboardView.swift b/OSGKeyboardExt/Views/AIKeyboardView.swift index 16bb6e9..6faa324 100644 --- a/OSGKeyboardExt/Views/AIKeyboardView.swift +++ b/OSGKeyboardExt/Views/AIKeyboardView.swift @@ -15,9 +15,18 @@ struct AIKeyboardView: View { static let actionButtonMaxWidth: CGFloat = 150 static let statusHeight: CGFloat = 20 static let carouselInterval: TimeInterval = 4 - static let skillButtonSize: CGFloat = 52 + static let skillButtonSize: CGFloat = 48 + static let skillsPerRow = 4 + static let skillRowSpacing: CGFloat = 8 + static let skillCellSpacing: CGFloat = 16 + static let skillCellWidth: CGFloat = 64 } + #if DEBUG + /// Layout preview for `--ai-skills-demo`. Nil keeps production clipboard-window gating. + static var debugPreviewSkills: [AIClipboardSkill]? + #endif + @Environment(\.colorScheme) private var colorScheme @Environment(\.accessibilityReduceMotion) private var reduceMotion @ObservedObject var state: KeyboardState @@ -236,40 +245,52 @@ struct AIKeyboardView: View { } private var clipboardSkillRow: some View { - let skills = visibleClipboardSkills - let row = HStack(spacing: Spacing.lg) { - ForEach(skills) { skill in - Button { - state.submitAIClipboardSkill(skill) - } label: { - VStack(spacing: 6) { - Image(systemName: skill.systemImage) - .font(.system(size: 18, weight: .semibold)) - .foregroundStyle(palette.textPrimary.opacity(0.85)) - .frame(width: Layout.skillButtonSize, height: Layout.skillButtonSize) - .glassEffect(.regular.interactive(), in: Circle()) - Text(clipboardSkillTitle(skill)) - .font(TypeStyle.caption2) - .foregroundStyle(palette.textSecondary) + let rows = skillRows(from: visibleClipboardSkills) + return VStack(spacing: 0) { + Spacer(minLength: 0) + VStack(spacing: Layout.skillRowSpacing) { + ForEach(Array(rows.enumerated()), id: \.offset) { _, rowSkills in + HStack(spacing: Layout.skillCellSpacing) { + ForEach(rowSkills) { skill in + skillChip(skill) + } } } - .buttonStyle(.plain) - .disabled(!state.aiServiceAvailable || state.aiSession.isBusy) - .accessibilityLabel(Text(clipboardSkillTitle(skill))) - } - } - return Group { - if skills.count > 4 { - ScrollView(.horizontal, showsIndicators: false) { - row.padding(.horizontal, Spacing.md) - } - } else { - row } + Spacer(minLength: 0) } .frame(maxWidth: .infinity, maxHeight: .infinity) } + private func skillRows(from skills: [AIClipboardSkill]) -> [[AIClipboardSkill]] { + stride(from: 0, to: skills.count, by: Layout.skillsPerRow).map { start in + Array(skills[start ..< min(start + Layout.skillsPerRow, skills.count)]) + } + } + + private func skillChip(_ skill: AIClipboardSkill) -> some View { + Button { + state.submitAIClipboardSkill(skill) + } label: { + VStack(spacing: 6) { + Image(systemName: skill.systemImage) + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(palette.textPrimary.opacity(0.85)) + .frame(width: Layout.skillButtonSize, height: Layout.skillButtonSize) + .glassEffect(.regular.interactive(), in: Circle()) + Text(clipboardSkillTitle(skill)) + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) + .lineLimit(1) + .minimumScaleFactor(0.8) + } + .frame(width: Layout.skillCellWidth) + } + .buttonStyle(.plain) + .disabled(!state.aiServiceAvailable || state.aiSession.isBusy) + .accessibilityLabel(Text(clipboardSkillTitle(skill))) + } + /// Translate follows the keyboard target; Reply / Summarize stay static. private func clipboardSkillTitle(_ skill: AIClipboardSkill) -> String { if skill.id == AIClipboardSkillCatalog.translateID { @@ -278,11 +299,20 @@ struct AIKeyboardView: View { uiLanguage: AppGroupStore().uiLanguage ) } + if let custom = skill.customName?.trimmingCharacters(in: .whitespacesAndNewlines), + !custom.isEmpty { + return custom + } return ExtL10n.string(skill.titleKey) } /// Copy-then-30s window: skill chips replace the rotating hint. private var showsClipboardSkills: Bool { + #if DEBUG + if let preview = Self.debugPreviewSkills, !preview.isEmpty { + return showsPlaceholder + } + #endif guard showsPlaceholder, state.clipboardHistoryEnabled else { return false } guard !visibleClipboardSkills.isEmpty else { return false } return AIHintPool.isClipboardSkillWindowActive( @@ -292,7 +322,15 @@ struct AIKeyboardView: View { } private var visibleClipboardSkills: [AIClipboardSkill] { - AIClipboardSkillCatalog.visible(enabledIDs: state.enabledClipboardSkillIDs) + #if DEBUG + if let preview = Self.debugPreviewSkills, !preview.isEmpty { + return preview + } + #endif + return AIClipboardSkillCatalog.visible( + enabledIDs: state.enabledClipboardSkillIDs, + userCatalog: AppGroupStore().agentUserSkillCatalog + ) } /// No draft/answer yet — show the centered hint carousel instead of a scroll body. @@ -474,6 +512,12 @@ struct AIKeyboardView: View { if state.pendingClipboardSkillID == AIClipboardSkillCatalog.extractTodosID { return ExtL10n.string("keyboard.ai.skill.extracting") } + if state.pendingClipboardSkillID == AIClipboardSkillCatalog.extractEventsID { + return ExtL10n.string("keyboard.ai.skill.extractingEvents") + } + if state.pendingClipboardSkillID == AIClipboardSkillCatalog.navigateID { + return ExtL10n.string("keyboard.ai.skill.extractingAddress") + } if let draft = state.aiSession.draftAnswerText, !draft.isEmpty { return ExtL10n.string("keyboard.ai.generating") } diff --git a/OSGKeyboardExt/en.lproj/Keyboard.strings b/OSGKeyboardExt/en.lproj/Keyboard.strings index 46f10dc..75129b8 100644 --- a/OSGKeyboardExt/en.lproj/Keyboard.strings +++ b/OSGKeyboardExt/en.lproj/Keyboard.strings @@ -298,8 +298,18 @@ "keyboard.ai.skill.summarize" = "Summarize"; "keyboard.ai.skill.translate" = "Translate"; "keyboard.ai.skill.extractTodos" = "Tasks"; +"keyboard.ai.skill.extractEvents" = "Events"; +"keyboard.ai.skill.navigate" = "Navigate"; +"keyboard.ai.skill.previewPolish" = "Polish"; +"keyboard.ai.skill.previewIdeas" = "Ideas"; +"keyboard.ai.skill.previewTone" = "Tone"; "keyboard.ai.skill.noTodos" = "No tasks in the clipboard"; +"keyboard.ai.skill.noEvents" = "No date or time in the clipboard"; +"keyboard.ai.skill.noAddress" = "No address in the clipboard"; +"keyboard.ai.skill.noExportItems" = "Nothing to send to the Shortcut"; "keyboard.ai.skill.extracting" = "Finding tasks…"; +"keyboard.ai.skill.extractingEvents" = "Finding events…"; +"keyboard.ai.skill.extractingAddress" = "Finding an address…"; "keyboard.ai.skill.runningShortcut" = "Running Shortcut…"; "keyboard.ai.skill.shortcutMissing" = "Companion Shortcut not found. Reinstall it in Skills"; "keyboard.ai.skill.handoffFailed" = "Couldn’t open the app to run the Shortcut"; diff --git a/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings b/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings index 3280535..f40f409 100644 --- a/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings +++ b/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings @@ -298,8 +298,18 @@ "keyboard.ai.skill.summarize" = "总结"; "keyboard.ai.skill.translate" = "翻译"; "keyboard.ai.skill.extractTodos" = "待办"; +"keyboard.ai.skill.extractEvents" = "日程"; +"keyboard.ai.skill.navigate" = "导航"; +"keyboard.ai.skill.previewPolish" = "润色"; +"keyboard.ai.skill.previewIdeas" = "点子"; +"keyboard.ai.skill.previewTone" = "语气"; "keyboard.ai.skill.noTodos" = "剪贴板内容没有待办事项"; +"keyboard.ai.skill.noEvents" = "剪贴板内容没有日期或时间"; +"keyboard.ai.skill.noAddress" = "剪贴板内容没有可导航的地址"; +"keyboard.ai.skill.noExportItems" = "没有可发给捷径的内容"; "keyboard.ai.skill.extracting" = "正在提取待办…"; +"keyboard.ai.skill.extractingEvents" = "正在提取日程…"; +"keyboard.ai.skill.extractingAddress" = "正在识别地址…"; "keyboard.ai.skill.runningShortcut" = "正在运行捷径…"; "keyboard.ai.skill.shortcutMissing" = "找不到配套捷径,请在技能页重新安装"; "keyboard.ai.skill.handoffFailed" = "无法打开 App 运行捷径"; diff --git a/OSGKeyboardMac/MacPolishStylesView.swift b/OSGKeyboardMac/MacPolishStylesView.swift index 33e34bb..71c196c 100644 --- a/OSGKeyboardMac/MacPolishStylesView.swift +++ b/OSGKeyboardMac/MacPolishStylesView.swift @@ -246,28 +246,22 @@ private struct MacPolishStyleCard: View { } .buttonStyle(.plain) - // Builtin: eye → view prompt; custom: pencil → edit. + // Pencil opens the prompt (built-in, read-only) or the editor (custom). Button(action: primaryAction) { - Image(systemName: isUserStyle ? "pencil" : "eye") - .font(.system(size: 12, weight: .semibold)) + Image(systemName: "pencil") + .font(.system(size: 15, weight: .semibold)) .foregroundStyle(palette.textSecondary) - .frame(width: 28, height: 28) + .frame(width: 30, height: 30) .background(palette.background.opacity(isHovering ? 0.9 : 0.72), in: Circle()) } .padding(Spacing.sm) .buttonStyle(.plain) - .accessibilityLabel( - MacL10n.string( - isUserStyle ? "mac.styles.edit" : "mac.styles.viewPrompt", - language: language - ) - ) + .accessibilityLabel(MacL10n.string("mac.styles.edit", language: language)) if isSelected { Image(systemName: "checkmark.circle.fill") .font(.system(size: 21, weight: .semibold)) .foregroundStyle(palette.accent) - .background(palette.surface, in: Circle()) .padding(Spacing.sm) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) .allowsHitTesting(false) diff --git a/OSGKeyboardShared/Models/AIAgentSkillLayout.swift b/OSGKeyboardShared/Models/AIAgentSkillLayout.swift index 2194185..a242804 100644 --- a/OSGKeyboardShared/Models/AIAgentSkillLayout.swift +++ b/OSGKeyboardShared/Models/AIAgentSkillLayout.swift @@ -44,8 +44,10 @@ public struct AIAgentSkillLayout: Codable, Equatable, Sendable { } /// Drops unknown IDs, unconfirmed export skills, and duplicates; caps at 8. - public func sanitized() -> AIAgentSkillLayout { - let known = Dictionary(uniqueKeysWithValues: AIClipboardSkillCatalog.catalog.map { ($0.id, $0) }) + public func sanitized( + catalog: [AIClipboardSkill] = AIClipboardSkillCatalog.catalog + ) -> AIAgentSkillLayout { + let known = Dictionary(uniqueKeysWithValues: catalog.map { ($0.id, $0) }) var seenEnabled = Set() let enabled = enabledIDs.filter { id in guard let skill = known[id], seenEnabled.insert(id).inserted else { return false } diff --git a/OSGKeyboardShared/Models/AIUserSkill.swift b/OSGKeyboardShared/Models/AIUserSkill.swift new file mode 100644 index 0000000..0562542 --- /dev/null +++ b/OSGKeyboardShared/Models/AIUserSkill.swift @@ -0,0 +1,177 @@ +// AIUserSkill.swift +// OSGKeyboard · Shared +// +// User-created clipboard skills. Built-in skills stay in +// `AIClipboardSkillCatalog`; this catalog is persisted in App Group so the +// keyboard can resolve custom names, prompts, and Shortcut run names. + +import Foundation + +public struct AIUserSkill: Codable, Equatable, Identifiable, Sendable { + public let id: String + public var name: String + /// Card subtitle. Empty is allowed; the UI shows a generic fallback. + public var summary: String + public var systemImage: String + public var prompt: String + public var shortcutICloudURL: URL + /// Name used by `shortcuts://run-shortcut?name=`. Independent of `name`. + public var shortcutName: String + /// Per-skill reasoning. Built-in skills are always off; custom defaults off. + public var thinkingEnabled: Bool + public let createdAt: Date + public var updatedAt: Date + + public init( + id: String = "user.\(UUID().uuidString.lowercased())", + name: String, + summary: String = "", + systemImage: String = AIUserSkillLimits.defaultSystemImage, + prompt: String, + shortcutICloudURL: URL, + shortcutName: String, + thinkingEnabled: Bool = false, + createdAt: Date = Date(), + updatedAt: Date? = nil + ) { + self.id = id + self.name = name + self.summary = summary + self.systemImage = systemImage + self.prompt = prompt + self.shortcutICloudURL = shortcutICloudURL + self.shortcutName = shortcutName + self.thinkingEnabled = thinkingEnabled + self.createdAt = createdAt + self.updatedAt = updatedAt ?? createdAt + } + + public var isUserCreated: Bool { id.hasPrefix("user.") } + + public func asClipboardSkill() -> AIClipboardSkill { + AIClipboardSkill( + id: id, + systemImage: systemImage, + titleKey: "", + cardTitleKey: "", + descriptionKey: "", + kind: .export, + isDefault: false, + shortcutName: shortcutName, + shortcutICloudURL: shortcutICloudURL, + customName: name, + customSummary: summary, + customPrompt: prompt, + thinkingEnabled: thinkingEnabled + ) + } +} + +public enum AIUserSkillLimits { + public static let defaultSystemImage = "sparkles" + public static let maximumPromptCharacters = 6_000 + public static let maximumNameCharacters = 40 + public static let maximumSummaryCharacters = 200 + public static let newPromptTemplate = """ + 请根据剪贴板内容完成以下操作。 + 只输出结果,不要解释或客套。 + """ + + /// Curated SF Symbols for the skill-icon picker. + public static let symbolChoices: [String] = [ + "sparkles", + "wand.and.stars", + "text.badge.checkmark", + "checklist", + "calendar", + "envelope.fill", + "bubble.left.and.bubble.right.fill", + "character.bubble.fill", + "doc.text.magnifyingglass", + "arrowshape.turn.up.left.fill", + "lightbulb.fill", + "star.fill", + "heart.fill", + "flag.fill", + "tag.fill", + "folder.fill", + "list.bullet", + "square.and.pencil", + "scissors", + "globe", + "paperplane.fill", + "clock.fill", + "bell.fill", + "bookmark.fill", + "person.fill", + "link", + "number", + "at", + "tray.fill", + "quote.bubble.fill", + ] +} + +public enum AIUserSkillValidationError: Error, Equatable, Sendable { + case emptyName + case emptyPrompt + case emptyShortcutName + case invalidShortcutLink + case emptyIcon + case promptTooLong(maximum: Int) +} + +public struct AIUserSkillCatalog: Codable, Equatable, Sendable { + public var entries: [AIUserSkill] + + public init(entries: [AIUserSkill] = []) { + self.entries = entries.filter(\.isUserCreated) + } + + public static let empty = AIUserSkillCatalog() + + public func skill(id: String) -> AIUserSkill? { + entries.first { $0.id == id } + } + + public mutating func upsert(_ skill: AIUserSkill, at date: Date = Date()) throws { + let name = skill.name.trimmingCharacters(in: .whitespacesAndNewlines) + let summary = skill.summary.trimmingCharacters(in: .whitespacesAndNewlines) + let prompt = skill.prompt.trimmingCharacters(in: .whitespacesAndNewlines) + let shortcutName = skill.shortcutName.trimmingCharacters(in: .whitespacesAndNewlines) + let icon = skill.systemImage.trimmingCharacters(in: .whitespacesAndNewlines) + + guard skill.isUserCreated else { throw AIUserSkillValidationError.emptyName } + guard !name.isEmpty else { throw AIUserSkillValidationError.emptyName } + guard !prompt.isEmpty else { throw AIUserSkillValidationError.emptyPrompt } + guard prompt.count <= AIUserSkillLimits.maximumPromptCharacters else { + throw AIUserSkillValidationError.promptTooLong( + maximum: AIUserSkillLimits.maximumPromptCharacters + ) + } + guard !shortcutName.isEmpty else { throw AIUserSkillValidationError.emptyShortcutName } + guard AIShortcutShareLink.isValid(skill.shortcutICloudURL) else { + throw AIUserSkillValidationError.invalidShortcutLink + } + guard !icon.isEmpty else { throw AIUserSkillValidationError.emptyIcon } + + var saved = skill + saved.name = String(name.prefix(AIUserSkillLimits.maximumNameCharacters)) + saved.summary = String(summary.prefix(AIUserSkillLimits.maximumSummaryCharacters)) + saved.prompt = prompt + saved.shortcutName = shortcutName + saved.systemImage = icon + saved.thinkingEnabled = skill.thinkingEnabled + saved.updatedAt = date + + if let index = entries.firstIndex(where: { $0.id == skill.id }) { + entries[index] = saved + } else { + entries.append(saved) + } + } + + public mutating func remove(id: String) { + entries.removeAll { $0.id == id } + } +} diff --git a/OSGKeyboardShared/Models/AppGroupConfiguration.swift b/OSGKeyboardShared/Models/AppGroupConfiguration.swift index 044dd28..19c858f 100644 --- a/OSGKeyboardShared/Models/AppGroupConfiguration.swift +++ b/OSGKeyboardShared/Models/AppGroupConfiguration.swift @@ -71,6 +71,8 @@ public struct AppGroupConfiguration: Sendable, Equatable { public static let localASRCustomLanguageModelEnabled = "config.localASR.customLanguageModelEnabled" /// Enabled AI Agent skill IDs (order) + confirmed companion Shortcuts. public static let agentSkillLayout = "config.aiAgentSkills.layout.v1" + /// User-created clipboard skills (no cloud sync; App Group only). + public static let agentUserSkillCatalog = "config.aiAgentSkills.userCatalog.v1" } // MARK: - Stored fields diff --git a/OSGKeyboardShared/Models/FlowSession/FlowCommand.swift b/OSGKeyboardShared/Models/FlowSession/FlowCommand.swift index f0c3c4e..f58ae86 100644 --- a/OSGKeyboardShared/Models/FlowSession/FlowCommand.swift +++ b/OSGKeyboardShared/Models/FlowSession/FlowCommand.swift @@ -41,6 +41,8 @@ public struct FlowCommand: Codable, Equatable, Sendable { public let aiConversationID: UUID? /// Prefilled question used only by `.submitAIQuestion`. public let aiQuestionText: String? + /// Clipboard-skill thinking override. Nil keeps AI-mode default (on). + public let aiThinkingEnabled: Bool? /// Absolute wall-clock deadlines survive extension reconstruction. public let startDeadlineAt: TimeInterval? public let processingDeadlineAt: TimeInterval? @@ -60,6 +62,7 @@ public struct FlowCommand: Codable, Equatable, Sendable { sourceHistoryEntryRevision: Int64? = nil, aiConversationID: UUID? = nil, aiQuestionText: String? = nil, + aiThinkingEnabled: Bool? = nil, startDeadlineAt: TimeInterval? = nil, processingDeadlineAt: TimeInterval? = nil ) { @@ -77,6 +80,7 @@ public struct FlowCommand: Codable, Equatable, Sendable { self.sourceHistoryEntryRevision = sourceHistoryEntryRevision self.aiConversationID = aiConversationID self.aiQuestionText = aiQuestionText + self.aiThinkingEnabled = aiThinkingEnabled self.startDeadlineAt = startDeadlineAt self.processingDeadlineAt = processingDeadlineAt } diff --git a/OSGKeyboardShared/Models/FlowUtteranceRequest.swift b/OSGKeyboardShared/Models/FlowUtteranceRequest.swift index b4ee119..be5e79a 100644 --- a/OSGKeyboardShared/Models/FlowUtteranceRequest.swift +++ b/OSGKeyboardShared/Models/FlowUtteranceRequest.swift @@ -13,6 +13,8 @@ public struct FlowUtteranceRequest: Equatable, Sendable { public let aiConversationID: UUID? /// When set with `.aiQuestion`, host skips ASR and answers this text. public let aiQuestionText: String? + /// Clipboard-skill thinking override. Nil keeps AI-mode default (on). + public let aiThinkingEnabled: Bool? public static let dictation = FlowUtteranceRequest(mode: .dictation) @@ -22,7 +24,8 @@ public struct FlowUtteranceRequest: Equatable, Sendable { sourceHistoryEntryID: UUID? = nil, sourceHistoryEntryRevision: Int64? = nil, aiConversationID: UUID? = nil, - aiQuestionText: String? = nil + aiQuestionText: String? = nil, + aiThinkingEnabled: Bool? = nil ) { self.mode = mode self.editSourceText = editSourceText @@ -30,6 +33,7 @@ public struct FlowUtteranceRequest: Equatable, Sendable { self.sourceHistoryEntryRevision = sourceHistoryEntryRevision self.aiConversationID = aiConversationID self.aiQuestionText = aiQuestionText + self.aiThinkingEnabled = aiThinkingEnabled } public static func editLastInput( @@ -48,12 +52,14 @@ public struct FlowUtteranceRequest: Equatable, Sendable { public static func aiQuestion( conversationID: UUID, - prefilledQuestion: String? = nil + prefilledQuestion: String? = nil, + thinkingEnabled: Bool? = nil ) -> FlowUtteranceRequest { FlowUtteranceRequest( mode: .aiQuestion, aiConversationID: conversationID, - aiQuestionText: prefilledQuestion + aiQuestionText: prefilledQuestion, + aiThinkingEnabled: thinkingEnabled ) } } diff --git a/OSGKeyboardShared/Services/AIAddressExtraction.swift b/OSGKeyboardShared/Services/AIAddressExtraction.swift new file mode 100644 index 0000000..7d10f17 --- /dev/null +++ b/OSGKeyboardShared/Services/AIAddressExtraction.swift @@ -0,0 +1,148 @@ +// AIAddressExtraction.swift +// OSGKeyboard · Shared +// +// Parses the LLM's navigate-skill reply into one origin|destination line. +// Fail closed: empty / NONE / no destination never become a run. Only the +// first valid line is kept — opening several map apps is not useful. + +import Foundation + +public struct AIMapRoute: Equatable, Sendable { + /// Nil means "current location" in the map app. + public let origin: String? + public let destination: String + + public init(origin: String?, destination: String) { + self.origin = origin + self.destination = destination + } +} + +public enum AIAddressExtraction: Sendable { + /// Canonical line: origin|destination. Empty origin keeps the pipe. + public static let fieldSeparator: Character = "|" + + private static let emptyTokens: Set = [ + "none", "no", "n/a", "na", "nil", "null", + "无", "没有", "没有地址", "没有地点", "无地址", "无地点", + "没有可导航的地点", "没有可导航的地址", + "no address", "no addresses", "no location", "no locations", + "no place", "no places", "no destination", + ] + + /// Lines to send to the host. Empty → do not run the companion Shortcut. + public static func lines( + from raw: String, + sourceClipboard: String? = nil + ) -> [String] { + guard let route = route(from: raw, sourceClipboard: sourceClipboard) else { + return [] + } + return [encode(route)] + } + + public static func route( + from raw: String, + sourceClipboard: String? = nil + ) -> AIMapRoute? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if isEmptyToken(trimmed) { return nil } + + for line in trimmed.components(separatedBy: .newlines) { + guard let route = parseLine(line) else { continue } + if isWholeClipboardEcho(route, source: sourceClipboard) { + return nil + } + return route + } + return nil + } + + public static func encode(_ route: AIMapRoute) -> String { + "\(route.origin ?? "")|\(route.destination)" + } + + // MARK: - Line + + private static func parseLine(_ line: String) -> AIMapRoute? { + let stripped = stripBullet(line) + guard !stripped.isEmpty, !isEmptyToken(stripped) else { return nil } + + let parts = stripped + .split(separator: fieldSeparator, omittingEmptySubsequences: false) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + + let originRaw: String + let destinationRaw: String + if parts.count == 1 { + originRaw = "" + destinationRaw = parts[0] + } else { + originRaw = parts[0] + var destParts = Array(parts[1...]) + while destParts.last?.isEmpty == true { + destParts.removeLast() + } + destinationRaw = destParts + .joined(separator: String(fieldSeparator)) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + guard let destination = sanitizedPlace(destinationRaw) else { return nil } + let origin = sanitizedPlace(originRaw) + if let origin, origin.caseInsensitiveCompare(destination) == .orderedSame { + return AIMapRoute(origin: nil, destination: destination) + } + return AIMapRoute(origin: origin, destination: destination) + } + + /// Drop URLs and empty / NONE tokens. Place names stay as-is. + private static func sanitizedPlace(_ raw: String) -> String? { + let text = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty, !isEmptyToken(text) else { return nil } + if text.contains("://") { return nil } + return text + } + + private static func isEmptyToken(_ text: String) -> Bool { + let folded = text + .trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "。.!!")) + .lowercased() + return emptyTokens.contains(folded) + } + + private static func stripBullet(_ line: String) -> String { + var text = line.trimmingCharacters(in: .whitespacesAndNewlines) + let prefixes = ["- ", "* ", "• ", "、"] + for prefix in prefixes where text.hasPrefix(prefix) { + text = String(text.dropFirst(prefix.count)) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + if let dotted = text.range(of: #"^\d+[\.\)、]\s*"#, options: .regularExpression) { + text = String(text[dotted.upperBound...]) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + return text + } + + /// One long destination that is essentially the clipboard body is not a place. + private static func isWholeClipboardEcho(_ route: AIMapRoute, source: String?) -> Bool { + guard let source, source.count > 80, route.destination.count > 80 else { + return false + } + let a = collapse(route.destination) + let b = collapse(source) + guard !a.isEmpty, !b.isEmpty else { return false } + if a == b { return true } + return a.contains(b) || b.contains(a) + } + + private static func collapse(_ text: String) -> String { + text.components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + .joined(separator: " ") + .lowercased() + } +} diff --git a/OSGKeyboardShared/Services/AIAgentSkillLayoutStore.swift b/OSGKeyboardShared/Services/AIAgentSkillLayoutStore.swift index be6d884..32b9a2b 100644 --- a/OSGKeyboardShared/Services/AIAgentSkillLayoutStore.swift +++ b/OSGKeyboardShared/Services/AIAgentSkillLayoutStore.swift @@ -12,46 +12,65 @@ public final class AIAgentSkillLayoutStore: ObservableObject { public static let shared = AIAgentSkillLayoutStore() @Published public private(set) var layout: AIAgentSkillLayout + @Published public private(set) var userCatalog: AIUserSkillCatalog - private let defaults: UserDefaults? - private let persist: (AIAgentSkillLayout) -> Void - private let load: () -> AIAgentSkillLayout + private let persistLayout: (AIAgentSkillLayout) -> Void + private let persistUserCatalog: (AIUserSkillCatalog) -> Void + private let loadLayout: () -> AIAgentSkillLayout + private let loadUserCatalog: () -> AIUserSkillCatalog public init(defaults: UserDefaults? = nil) { if let defaults { - self.defaults = defaults - self.load = { AppGroupStore(defaults: defaults).agentSkillLayout } - self.persist = { AppGroupStore(defaults: defaults).setAgentSkillLayout($0) } + self.loadUserCatalog = { AppGroupStore(defaults: defaults).agentUserSkillCatalog } + self.persistUserCatalog = { AppGroupStore(defaults: defaults).setAgentUserSkillCatalog($0) } + self.loadLayout = { AppGroupStore(defaults: defaults).agentSkillLayout } + self.persistLayout = { AppGroupStore(defaults: defaults).setAgentSkillLayout($0) } } else { - self.defaults = nil - self.load = { AppGroupStore().agentSkillLayout } - self.persist = { AppGroupStore().setAgentSkillLayout($0) } + self.loadUserCatalog = { AppGroupStore().agentUserSkillCatalog } + self.persistUserCatalog = { AppGroupStore().setAgentUserSkillCatalog($0) } + self.loadLayout = { AppGroupStore().agentSkillLayout } + self.persistLayout = { AppGroupStore().setAgentSkillLayout($0) } } - self.layout = self.load() + self.userCatalog = self.loadUserCatalog() + self.layout = self.loadLayout() } public func reload() { - layout = load() + userCatalog = loadUserCatalog() + layout = loadLayout() + } + + public var mergedCatalog: [AIClipboardSkill] { + AIClipboardSkillCatalog.all(userCatalog: userCatalog) } public var enabledSkills: [AIClipboardSkill] { - AIClipboardSkillCatalog.visible(enabledIDs: layout.enabledIDs) + AIClipboardSkillCatalog.visible( + enabledIDs: layout.enabledIDs, + userCatalog: userCatalog + ) } public var availableSkills: [AIClipboardSkill] { - AIClipboardSkillCatalog.catalog.filter { !layout.isEnabled($0.id) } + mergedCatalog.filter { !layout.isEnabled($0.id) } + } + + public func userSkill(id: String) -> AIUserSkill? { + userCatalog.skill(id: id) } @discardableResult public func enable(_ id: String) -> AIAgentSkillEnableResult { - let current = layout.sanitized() - guard let skill = AIClipboardSkillCatalog.skill(id: id) else { return .unknown } + let current = layout.sanitized(catalog: mergedCatalog) + guard let skill = AIClipboardSkillCatalog.skill(id: id, userCatalog: userCatalog) else { + return .unknown + } if current.isEnabled(id) { return .alreadyEnabled } if skill.requiresShortcut, !current.hasConfirmedShortcut(id) { return .needsShortcut } if current.isFull { return .full } - commit( + commitLayout( AIAgentSkillLayout( enabledIDs: current.enabledIDs + [id], confirmedShortcutIDs: current.confirmedShortcutIDs @@ -63,8 +82,8 @@ public final class AIAgentSkillLayoutStore: ObservableObject { /// Drops the keyboard slot only. Companion Shortcuts stay installed; /// the user deletes them in the Shortcuts app if they want them gone. public func disable(_ id: String) { - let current = layout.sanitized() - commit( + let current = layout.sanitized(catalog: mergedCatalog) + commitLayout( AIAgentSkillLayout( enabledIDs: current.enabledIDs.filter { $0 != id }, confirmedShortcutIDs: current.confirmedShortcutIDs @@ -75,27 +94,34 @@ public final class AIAgentSkillLayoutStore: ObservableObject { /// Marks the companion Shortcut as added, then tries to occupy a slot. @discardableResult public func confirmShortcutAndEnable(_ id: String) -> AIAgentSkillEnableResult { - guard let skill = AIClipboardSkillCatalog.skill(id: id), skill.requiresShortcut else { + guard let skill = AIClipboardSkillCatalog.skill(id: id, userCatalog: userCatalog), + skill.requiresShortcut else { return .unknown } - var current = layout.sanitized() + var current = layout.sanitized(catalog: mergedCatalog) if !current.confirmedShortcutIDs.contains(id) { current.confirmedShortcutIDs.append(id) } - commit(current) + commitLayout(current) return enable(id) } public func moveEnabled(id draggedID: String, onto targetID: String) { - var ids = layout.sanitized().enabledIDs - guard let from = ids.firstIndex(of: draggedID), - let to = ids.firstIndex(of: targetID), - from != to else { return } + let ids = layout.sanitized(catalog: mergedCatalog).enabledIDs + guard let to = ids.firstIndex(of: targetID) else { return } + moveEnabled(id: draggedID, toIndex: to) + } + + public func moveEnabled(id draggedID: String, toIndex: Int) { + var ids = layout.sanitized(catalog: mergedCatalog).enabledIDs + guard let from = ids.firstIndex(of: draggedID), !ids.isEmpty else { return } + let to = min(max(toIndex, 0), ids.count - 1) + guard from != to else { return } ids.move( fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to ) - commit( + commitLayout( AIAgentSkillLayout( enabledIDs: ids, confirmedShortcutIDs: layout.confirmedShortcutIDs @@ -103,8 +129,47 @@ public final class AIAgentSkillLayoutStore: ObservableObject { ) } - private func commit(_ layout: AIAgentSkillLayout) { - persist(layout) - self.layout = load() + public func saveUserSkill(_ skill: AIUserSkill) throws { + let previousURL = userCatalog.skill(id: skill.id)?.shortcutICloudURL + var catalog = userCatalog + try catalog.upsert(skill) + commitUserCatalog(catalog) + if previousURL != nil, previousURL != skill.shortcutICloudURL { + dropShortcutConfirmation(for: skill.id) + } + } + + public func deleteUserSkill(id: String) { + var catalog = userCatalog + catalog.remove(id: id) + commitUserCatalog(catalog) + let current = layout.sanitized(catalog: mergedCatalog) + commitLayout( + AIAgentSkillLayout( + enabledIDs: current.enabledIDs.filter { $0 != id }, + confirmedShortcutIDs: current.confirmedShortcutIDs.filter { $0 != id } + ) + ) + } + + private func dropShortcutConfirmation(for id: String) { + let current = layout.sanitized(catalog: mergedCatalog) + commitLayout( + AIAgentSkillLayout( + enabledIDs: current.enabledIDs.filter { $0 != id }, + confirmedShortcutIDs: current.confirmedShortcutIDs.filter { $0 != id } + ) + ) + } + + private func commitLayout(_ layout: AIAgentSkillLayout) { + persistLayout(layout) + self.layout = loadLayout() + } + + private func commitUserCatalog(_ catalog: AIUserSkillCatalog) { + persistUserCatalog(catalog) + userCatalog = loadUserCatalog() + layout = loadLayout() } } diff --git a/OSGKeyboardShared/Services/AIClipboardSkill.swift b/OSGKeyboardShared/Services/AIClipboardSkill.swift index 3001289..bbf1764 100644 --- a/OSGKeyboardShared/Services/AIClipboardSkill.swift +++ b/OSGKeyboardShared/Services/AIClipboardSkill.swift @@ -30,8 +30,17 @@ public struct AIClipboardSkill: Identifiable, Equatable, Sendable { public let shortcutName: String? /// Optional `icloud.com/shortcuts/` share URL. Nil → open the bundled file. public let shortcutICloudURL: URL? + /// Bundled `.shortcut` resource name without extension. Nil → no file fallback. + public let shortcutResourceName: String? + /// User-created skills store display copy here instead of localization keys. + public let customName: String? + public let customSummary: String? + public let customPrompt: String? + /// Built-in skills are always false. Custom skills default off. + public let thinkingEnabled: Bool public var requiresShortcut: Bool { kind == .export } + public var isUserCreated: Bool { id.hasPrefix("user.") } public init( id: String, @@ -42,7 +51,12 @@ public struct AIClipboardSkill: Identifiable, Equatable, Sendable { kind: AIClipboardSkillKind, isDefault: Bool, shortcutName: String? = nil, - shortcutICloudURL: URL? = nil + shortcutICloudURL: URL? = nil, + shortcutResourceName: String? = nil, + customName: String? = nil, + customSummary: String? = nil, + customPrompt: String? = nil, + thinkingEnabled: Bool = false ) { self.id = id self.systemImage = systemImage @@ -53,6 +67,11 @@ public struct AIClipboardSkill: Identifiable, Equatable, Sendable { self.isDefault = isDefault self.shortcutName = shortcutName self.shortcutICloudURL = shortcutICloudURL + self.shortcutResourceName = shortcutResourceName + self.customName = customName + self.customSummary = customSummary + self.customPrompt = customPrompt + self.thinkingEnabled = id.hasPrefix("user.") ? thinkingEnabled : false } } @@ -63,8 +82,20 @@ public enum AIClipboardSkillCatalog: Sendable { public static let extractTodosID = "extractTodos" public static let extractTodosShortcutName = "OSG · 提取待办" public static let extractTodosShortcutICloudURL = URL( - string: "https://www.icloud.com/shortcuts/520317da7ae74759b64d5fb069c71f81" + string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44" )! + public static let extractTodosResourceName = "OSGExtractTodos" + + public static let extractEventsID = "extractEvents" + public static let extractEventsShortcutName = "OSG · 提取日程" + public static let extractEventsShortcutICloudURL = URL( + string: "https://www.icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf" + )! + public static let extractEventsResourceName = "OSGExtractEvents" + + public static let navigateID = "navigate" + public static let navigateShortcutName = "OSG · 导航" + public static let navigateResourceName = "OSGNavigate" /// Full built-in catalog, in a stable display order for the Skills tab. public static let catalog: [AIClipboardSkill] = [ @@ -104,35 +135,76 @@ public enum AIClipboardSkillCatalog: Sendable { kind: .export, isDefault: false, shortcutName: extractTodosShortcutName, - shortcutICloudURL: extractTodosShortcutICloudURL + shortcutICloudURL: extractTodosShortcutICloudURL, + shortcutResourceName: extractTodosResourceName + ), + AIClipboardSkill( + id: extractEventsID, + systemImage: "calendar", + titleKey: "keyboard.ai.skill.extractEvents", + cardTitleKey: "skills.extractEvents.name", + descriptionKey: "skills.extractEvents.description", + kind: .export, + isDefault: false, + shortcutName: extractEventsShortcutName, + shortcutICloudURL: extractEventsShortcutICloudURL, + shortcutResourceName: extractEventsResourceName + ), + AIClipboardSkill( + id: navigateID, + systemImage: "arrow.triangle.turn.up.right.diamond.fill", + titleKey: "keyboard.ai.skill.navigate", + cardTitleKey: "skills.navigate.name", + descriptionKey: "skills.navigate.description", + kind: .export, + isDefault: false, + shortcutName: navigateShortcutName, + shortcutResourceName: navigateResourceName ), ] /// Legacy alias: the three default transform skills used to be the whole list. public static let builtIn: [AIClipboardSkill] = catalog - public static func skill(id: String) -> AIClipboardSkill? { - catalog.first { $0.id == id } + public static func all(userCatalog: AIUserSkillCatalog = .empty) -> [AIClipboardSkill] { + catalog + userCatalog.entries.map { $0.asClipboardSkill() } + } + + public static func skill( + id: String, + userCatalog: AIUserSkillCatalog = .empty + ) -> AIClipboardSkill? { + catalog.first { $0.id == id } ?? userCatalog.skill(id: id)?.asClipboardSkill() } /// `enabledIDs` is the Skills-tab order. `nil` keeps the default three. /// An explicit empty array shows no chips (carousel fallback). - public static func visible(enabledIDs: [String]? = nil) -> [AIClipboardSkill] { + public static func visible( + enabledIDs: [String]? = nil, + userCatalog: AIUserSkillCatalog = .empty + ) -> [AIClipboardSkill] { let ids = enabledIDs ?? AIAgentSkillLayout.defaultEnabledIDs guard !ids.isEmpty else { return [] } - let byID = Dictionary(uniqueKeysWithValues: catalog.map { ($0.id, $0) }) + let byID = Dictionary(uniqueKeysWithValues: all(userCatalog: userCatalog).map { ($0.id, $0) }) return ids.compactMap { byID[$0] } } public static func instruction( for skill: AIClipboardSkill, locale: String, - translationTargetLocaleId: String + translationTargetLocaleId: String, + now: Date = Date() ) -> String { - instruction( + if let custom = skill.customPrompt? + .trimmingCharacters(in: .whitespacesAndNewlines), + !custom.isEmpty { + return custom + } + return instruction( skillID: skill.id, locale: locale, - translationTargetLocaleId: translationTargetLocaleId + translationTargetLocaleId: translationTargetLocaleId, + now: now ) } @@ -159,7 +231,8 @@ public enum AIClipboardSkillCatalog: Sendable { public static func instruction( skillID: String, locale: String, - translationTargetLocaleId: String + translationTargetLocaleId: String, + now: Date = Date() ) -> String { let zh = locale == "zh" switch skillID { @@ -188,6 +261,10 @@ public enum AIClipboardSkillCatalog: Sendable { If there are no actionable tasks, output NONE and nothing else. Do not treat the whole clipboard as one task. If the clipboard itself is already one short task (for example "buy milk"), output that single line. """ + case extractEventsID: + return eventInstruction(zh: zh, now: now) + case navigateID: + return navigateInstruction(zh: zh) default: return zh ? "请根据剪贴板内容完成用户选择的操作。" @@ -195,6 +272,61 @@ public enum AIClipboardSkillCatalog: Sendable { } } + private static func navigateInstruction(zh: Bool) -> String { + if zh { + return """ + 请从剪贴板提取明确的地点用于导航。只输出一行,两段用 | 分隔:起点|终点 + 从当前位置出发则起点留空,但保留竖线,例如 |朝阳区酒仙桥路10号 + 两点都写了则两侧都填,例如 北京南站|三里屯太古里 + 可以是完整地址或常用地名。不要编号、不要解释、不要多行、不要链接。 + 若有多条地址,只输出最明确的一条。 + 没有可导航的地点时,只输出 NONE。不要把整段原文当成一个地点。 + """ + } + return """ + Extract one place for turn-by-turn navigation from the clipboard. One line, two fields separated by | : origin|destination + Leave origin empty when starting from the current location, but keep the pipe, for example |10 Jiuxianqiao Road + Fill both sides when the source names two places, for example Beijing South|Sanlitun Taikoo Li + A full address or a well-known place name is fine. No numbering, commentary, extra lines, or URLs. + If there are several addresses, output only the clearest one. + If there is no navigable place, output NONE and nothing else. Do not treat the whole clipboard as one place. + """ + } + + /// Clock context so relative phrases (tomorrow, 3pm) resolve to local time. + private static func eventInstruction(zh: Bool, now: Date) -> String { + let clock = clockContext(now: now, zh: zh) + if zh { + return """ + \(clock) + 请从剪贴板提取明确的日程。每条一行,四段用 | 分隔:开始|结束|标题|地点 + 开始有钟点用 YYYY-MM-DD HH:mm;只有日期(全天)用 YYYY-MM-DD。没有结束时间或地点则该段留空,但保留竖线。标题中不要出现 |。最多 20 条。不要编号、不要解释。 + 只有时刻、没有日期时,使用今天的日期。日期和时间都没有的条目不要输出。 + 原文写了结束时间就填写结束段,否则留空(后续按 1 小时处理)。原文有地点就填写地点段。 + 若没有任何带日期或时间的日程,只输出 NONE,不要把整段原文当成一条日程。 + """ + } + return """ + \(clock) + Extract explicit calendar events from the clipboard. One event per line, four fields separated by | : start|end|title|location + Timed start uses YYYY-MM-DD HH:mm; date-only (all-day) uses YYYY-MM-DD. Leave end or location empty when unknown, but keep the pipes. Do not put | in the title. Maximum 20 lines. No numbering or commentary. + Time without a date uses today. Skip items that have neither a date nor a time. + Fill the end field when the source gives an end time; otherwise leave it empty (treated as 1 hour). Fill location when the source names a place. + If there are no events with a date or time, output NONE and nothing else. Do not treat the whole clipboard as one event. + """ + } + + private static func clockContext(now: Date, zh: Bool) -> String { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: zh ? "zh_CN" : "en_US_POSIX") + formatter.timeZone = TimeZone.current + formatter.dateFormat = zh ? "yyyy年M月d日EEEE HH:mm" : "EEEE, d MMMM yyyy, HH:mm" + let stamp = formatter.string(from: now) + return zh + ? "现在是\(stamp)(设备本地时区)。" + : "It is now \(stamp) (device local timezone)." + } + /// Uses the keyboard translation target when set; otherwise Chinese ↔ English. private static func translateInstruction( locale: String, diff --git a/OSGKeyboardShared/Services/AIEventExtraction.swift b/OSGKeyboardShared/Services/AIEventExtraction.swift new file mode 100644 index 0000000..2613a6f --- /dev/null +++ b/OSGKeyboardShared/Services/AIEventExtraction.swift @@ -0,0 +1,279 @@ +// AIEventExtraction.swift +// OSGKeyboard · Shared +// +// Parses the LLM's extract-events reply into Shortcut lines. Fail closed: +// empty / NONE / lines without a parseable start never become a run. +// Canonical line: start|end|title|location +// All-day uses end token ALLDAY so the companion Shortcut can branch. + +import Foundation + +public enum AIEventExtraction: Sendable { + public static let maximumItems = 20 + public static let defaultDuration: TimeInterval = 3600 + /// End-field sentinel for all-day events (Shortcut If equals this). + public static let allDaySentinel = "ALLDAY" + + private static let emptyTokens: Set = [ + "none", "no", "n/a", "na", "nil", "null", + "无", "没有", "没有日程", "没有事件", "无日程", + "没有日期", "没有时间", "没有日期或时间", "没有日期和时间", + "no events", "no event", "no calendar events", + "no date", "no time", "no date or time", "no date and time", + ] + + /// Lines to send to the companion Shortcut. Empty → do not run it. + public static func lines( + from raw: String, + sourceClipboard: String? = nil, + now: Date = Date(), + calendar: Calendar = .current + ) -> [String] { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return [] } + if isEmptyToken(trimmed) { return [] } + + var seen = Set() + var items: [String] = [] + for line in trimmed.components(separatedBy: .newlines) { + guard let encoded = encodeLine( + line, + now: now, + calendar: calendar + ) else { continue } + let key = encoded.lowercased() + guard seen.insert(key).inserted else { continue } + items.append(encoded) + if items.count == maximumItems { break } + } + + if items.count == 1, isWholeClipboardEcho(items[0], source: sourceClipboard) { + return [] + } + return items + } + + // MARK: - Line + + private static func encodeLine( + _ line: String, + now: Date, + calendar: Calendar + ) -> String? { + let fields = splitFields(line) + guard fields.count >= 2 else { return nil } + + let startRaw = fields[0] + let endRaw: String + let titleRaw: String + let locationRaw: String + switch fields.count { + case 2: + endRaw = "" + titleRaw = fields[1] + locationRaw = "" + case 3: + // 3 fields: start|end|title if segment 2 is a time/empty; + // otherwise start|title|location. + if fields[1].isEmpty || parseInstant(fields[1], on: now, calendar: calendar) != nil { + endRaw = fields[1] + titleRaw = fields[2] + locationRaw = "" + } else { + endRaw = "" + titleRaw = fields[1] + locationRaw = fields[2] + } + default: + endRaw = fields[1] + titleRaw = fields[2] + locationRaw = fields[3] + } + + let title = stripBullet(titleRaw) + guard !title.isEmpty, !isEmptyToken(title) else { return nil } + let location = locationRaw.trimmingCharacters(in: .whitespacesAndNewlines) + + guard let start = parseInstant(startRaw, on: now, calendar: calendar) else { + return nil + } + + switch start { + case .allDay(let day): + return encode( + start: formatDay(day, calendar: calendar), + end: allDaySentinel, + title: title, + location: location + ) + case .timed(let startDate, _): + let endDate = resolveEnd( + endRaw, + start: startDate, + calendar: calendar + ) + return encode( + start: formatMinute(startDate, calendar: calendar), + end: formatMinute(endDate, calendar: calendar), + title: title, + location: location + ) + } + } + + /// Split on `|` and keep at most 4 fields; extra segments join into location. + private static func splitFields(_ line: String) -> [String] { + let parts = line + .split(separator: "|", omittingEmptySubsequences: false) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + guard parts.count > 4 else { return parts } + let location = parts[3...].joined(separator: "|") + return [parts[0], parts[1], parts[2], location] + } + + private static func encode(start: String, end: String, title: String, location: String) -> String { + "\(start)|\(end)|\(title)|\(location)" + } + + // MARK: - Time + + private enum Instant { + case allDay(Date) + case timed(Date, timeOnly: Bool) + } + + private static func parseInstant( + _ raw: String, + on day: Date, + calendar: Calendar + ) -> Instant? { + let text = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { return nil } + if text.caseInsensitiveCompare(allDaySentinel) == .orderedSame { + return .allDay(calendar.startOfDay(for: day)) + } + + for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm"] { + if let date = date(from: text, format: format, calendar: calendar) { + return .timed(date, timeOnly: false) + } + } + if let date = date(from: text, format: "yyyy-MM-dd", calendar: calendar) { + return .allDay(calendar.startOfDay(for: date)) + } + for format in ["HH:mm:ss", "HH:mm", "H:mm"] { + if let parsed = date(from: text, format: format, calendar: calendar) { + let parts = calendar.dateComponents([.hour, .minute, .second], from: parsed) + guard let combined = calendar.date( + bySettingHour: parts.hour ?? 0, + minute: parts.minute ?? 0, + second: 0, + of: day + ) else { return nil } + return .timed(combined, timeOnly: true) + } + } + return nil + } + + private static func resolveEnd( + _ raw: String, + start: Date, + calendar: Calendar + ) -> Date { + let fallback = start.addingTimeInterval(defaultDuration) + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, + trimmed.caseInsensitiveCompare(allDaySentinel) != .orderedSame else { + return fallback + } + guard let parsed = parseInstant(trimmed, on: start, calendar: calendar) else { + return fallback + } + let end: Date + switch parsed { + case .allDay(let day): + // Date-only end with a timed start: use that calendar day at 23:59. + end = calendar.date(bySettingHour: 23, minute: 59, second: 0, of: day) ?? fallback + case .timed(let date, let timeOnly): + if timeOnly, date <= start { + end = calendar.date(byAdding: .day, value: 1, to: date) ?? fallback + } else { + end = date + } + } + return end <= start ? fallback : end + } + + private static func date(from text: String, format: String, calendar: Calendar) -> Date? { + let formatter = DateFormatter() + formatter.calendar = calendar + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = calendar.timeZone + formatter.isLenient = false + formatter.dateFormat = format + return formatter.date(from: text) + } + + private static func formatDay(_ date: Date, calendar: Calendar) -> String { + format(date, "yyyy-MM-dd", calendar: calendar) + } + + private static func formatMinute(_ date: Date, calendar: Calendar) -> String { + format(date, "yyyy-MM-dd HH:mm", calendar: calendar) + } + + private static func format(_ date: Date, _ format: String, calendar: Calendar) -> String { + let formatter = DateFormatter() + formatter.calendar = calendar + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = calendar.timeZone + formatter.dateFormat = format + return formatter.string(from: date) + } + + // MARK: - Tokens + + private static func isEmptyToken(_ text: String) -> Bool { + let folded = text + .trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "。.!!")) + .lowercased() + return emptyTokens.contains(folded) + } + + private static func stripBullet(_ line: String) -> String { + var text = line.trimmingCharacters(in: .whitespacesAndNewlines) + let prefixes = ["- ", "* ", "• ", "、"] + for prefix in prefixes where text.hasPrefix(prefix) { + text = String(text.dropFirst(prefix.count)) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + if let dotted = text.range(of: #"^\d+[\.\)、]\s*"#, options: .regularExpression) { + text = String(text[dotted.upperBound...]) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + return text + } + + /// One long title that is essentially the clipboard body is not an event. + private static func isWholeClipboardEcho(_ item: String, source: String?) -> Bool { + guard let source, source.count > 80 else { return false } + let fields = item.split(separator: "|", omittingEmptySubsequences: false) + guard fields.count >= 3 else { return false } + let title = String(fields[2]) + guard title.count > 80 else { return false } + let a = collapse(title) + let b = collapse(source) + guard !a.isEmpty, !b.isEmpty else { return false } + if a == b { return true } + return a.contains(b) || b.contains(a) + } + + private static func collapse(_ text: String) -> String { + text.components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + .joined(separator: " ") + .lowercased() + } +} diff --git a/OSGKeyboardShared/Services/AIGenericSkillExport.swift b/OSGKeyboardShared/Services/AIGenericSkillExport.swift new file mode 100644 index 0000000..f246c20 --- /dev/null +++ b/OSGKeyboardShared/Services/AIGenericSkillExport.swift @@ -0,0 +1,25 @@ +// AIGenericSkillExport.swift +// OSGKeyboard · Shared +// +// Line-oriented Shortcut input for user-created export skills. Built-in +// extract-todos / extract-events keep their dedicated parsers. + +import Foundation + +public enum AIGenericSkillExport { + public static let maximumItems = 20 + + public static func items(from answer: String) -> [String] { + let trimmed = answer.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return [] } + if trimmed.compare("NONE", options: .caseInsensitive) == .orderedSame { + return [] + } + let lines = trimmed + .split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + if lines.isEmpty { return [trimmed] } + return Array(lines.prefix(maximumItems)) + } +} diff --git a/OSGKeyboardShared/Services/AIMapNavigation.swift b/OSGKeyboardShared/Services/AIMapNavigation.swift new file mode 100644 index 0000000..03a434b --- /dev/null +++ b/OSGKeyboardShared/Services/AIMapNavigation.swift @@ -0,0 +1,126 @@ +// AIMapNavigation.swift +// OSGKeyboard · Shared +// +// Builds one turn-by-turn URL. Provider order is Amap (高德) → Baidu → +// Apple Maps. Shortcuts cannot call `canOpenURL`, so the host injects that +// check and passes the resulting URL to the companion Shortcut. + +import Foundation + +public enum AIMapProvider: String, Equatable, Sendable { + case amap + case baidu + case apple +} + +public enum AIMapNavigation: Sendable { + public static let sourceApplication = "OSGKeyboard" + public static let baiduSource = "ios.osgkeyboard" + /// Amap's usual current-location label when we have no coordinates. + public static let amapCurrentLocationName = "我的位置" + + public static func provider(canOpen: (URL) -> Bool) -> AIMapProvider { + if canOpen(probe("iosamap")) || canOpen(probe("amapuri")) { + return .amap + } + if canOpen(probe("baidumap")) { + return .baidu + } + return .apple + } + + public static func url(for route: AIMapRoute, canOpen: (URL) -> Bool) -> URL { + switch provider(canOpen: canOpen) { + case .amap: + if canOpen(probe("iosamap")) { + return amapURL(route, useLegacyScheme: true) + } + return amapURL(route, useLegacyScheme: false) + case .baidu: + return baiduURL(route) + case .apple: + return appleURL(route) + } + } + + /// Keyboard payload is `origin|destination`. Host turns it into one URL. + public static func shortcutInput( + from encoded: String, + canOpen: (URL) -> Bool + ) -> String? { + guard let route = AIAddressExtraction.route(from: encoded) else { return nil } + return url(for: route, canOpen: canOpen).absoluteString + } + + // MARK: - Providers + + private static func amapURL(_ route: AIMapRoute, useLegacyScheme: Bool) -> URL { + let originName = route.origin ?? amapCurrentLocationName + let items = [ + URLQueryItem(name: "sourceApplication", value: sourceApplication), + URLQueryItem(name: "sname", value: originName), + URLQueryItem(name: "dname", value: route.destination), + URLQueryItem(name: "dev", value: "0"), + URLQueryItem(name: "t", value: "0"), + ] + if useLegacyScheme { + return makeURL(scheme: "iosamap", host: "path", path: nil, items: items) + } + let url = makeURL(scheme: "amapuri", host: "route", path: "/plan/", items: items) + // URLComponents drops the trailing slash; Amap's documented path is `/plan/`. + let withSlash = url.absoluteString.replacingOccurrences( + of: "://route/plan?", + with: "://route/plan/?" + ) + return URL(string: withSlash) ?? url + } + + private static func baiduURL(_ route: AIMapRoute) -> URL { + var items = [ + URLQueryItem(name: "destination", value: "name:\(route.destination)"), + URLQueryItem(name: "mode", value: "driving"), + URLQueryItem(name: "src", value: baiduSource), + ] + if let origin = route.origin { + items.insert( + URLQueryItem(name: "origin", value: "name:\(origin)"), + at: 0 + ) + } + return makeURL(scheme: "baidumap", host: "map", path: "/direction", items: items) + } + + private static func appleURL(_ route: AIMapRoute) -> URL { + var items = [ + URLQueryItem(name: "daddr", value: route.destination), + URLQueryItem(name: "dirflg", value: "d"), + ] + if let origin = route.origin { + items.insert(URLQueryItem(name: "saddr", value: origin), at: 0) + } + // `URLComponents(string: "maps://")` keeps the `://` that opening needs. + var components = URLComponents(string: "maps://")! + components.queryItems = items + return components.url ?? URL(string: "maps://")! + } + + private static func probe(_ scheme: String) -> URL { + URL(string: "\(scheme)://")! + } + + private static func makeURL( + scheme: String, + host: String?, + path: String?, + items: [URLQueryItem] + ) -> URL { + var components = URLComponents() + components.scheme = scheme + components.host = host + if let path { + components.path = path + } + components.queryItems = items + return components.url ?? URL(string: "\(scheme)://")! + } +} diff --git a/OSGKeyboardShared/Services/AIModeLLMClientFactory.swift b/OSGKeyboardShared/Services/AIModeLLMClientFactory.swift index 7e31d7b..6f582e2 100644 --- a/OSGKeyboardShared/Services/AIModeLLMClientFactory.swift +++ b/OSGKeyboardShared/Services/AIModeLLMClientFactory.swift @@ -8,14 +8,15 @@ import Foundation public enum AIModeLLMClientFactory { - /// Build an AI-mode client. Thinking is always forced on for this path. - /// When `allowWebSearch` is false, returns the plain polish-compatible client. + /// Build an AI-mode client. Thinking defaults on for spoken questions. + /// Clipboard skills pass `thinkingEnabled` from the skill (built-in: off). public static func make( providerId: String, baseURL: String, apiKey: String, model: String, allowWebSearch: Bool = true, + thinkingEnabled: Bool = true, session: URLSession = .shared ) -> any LLMClient { let plain = LLMClientFactory.make( @@ -23,10 +24,10 @@ public enum AIModeLLMClientFactory { baseURL: baseURL, apiKey: apiKey, model: model, - thinkingEnabled: true, + thinkingEnabled: thinkingEnabled, session: session ) - guard allowWebSearch else { return plain } + guard thinkingEnabled, allowWebSearch else { return plain } guard let searching = makeSearchingClient( providerId: providerId, diff --git a/OSGKeyboardShared/Services/AIQuestionService.swift b/OSGKeyboardShared/Services/AIQuestionService.swift index 793979d..6a61c5d 100644 --- a/OSGKeyboardShared/Services/AIQuestionService.swift +++ b/OSGKeyboardShared/Services/AIQuestionService.swift @@ -123,7 +123,8 @@ public struct AIQuestionService: Sendable { public static func configured( store: any ConfigurationStore, - conversations: AIConversationStore + conversations: AIConversationStore, + thinkingEnabled: Bool = true ) throws -> AIQuestionService { // Same provider + baseURL + model resolution as dictation polish so the // Settings LLM card is the single source of truth for both modes. @@ -151,7 +152,8 @@ public struct AIQuestionService: Sendable { baseURL: endpoint.baseURL, apiKey: apiKey, model: endpoint.model, - allowWebSearch: true + allowWebSearch: true, + thinkingEnabled: thinkingEnabled ), conversations: conversations, responseLength: store.aiResponseLength diff --git a/OSGKeyboardShared/Services/AIShortcutShareLink.swift b/OSGKeyboardShared/Services/AIShortcutShareLink.swift new file mode 100644 index 0000000..3d967b6 --- /dev/null +++ b/OSGKeyboardShared/Services/AIShortcutShareLink.swift @@ -0,0 +1,94 @@ +// AIShortcutShareLink.swift +// OSGKeyboard · Shared +// +// Validates iCloud Shortcut share URLs and reads the published name from +// Apple's undocumented records endpoint. Running a Shortcut still requires +// that name (`shortcuts://run-shortcut?name=`); the share link is install-only. + +import Foundation + +public enum AIShortcutShareLink { + /// `https://www.icloud.com/shortcuts/{token}` or `https://icloud.com/shortcuts/{token}`. + public static func isValid(_ url: URL) -> Bool { + guard let token = AIAgentShortcutRun.iCloudShareToken(from: url) else { return false } + let allowed = CharacterSet.alphanumerics + return (16...64).contains(token.count) + && token.unicodeScalars.allSatisfy { allowed.contains($0) } + } + + public static func parse(_ raw: String) -> URL? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + let withScheme: String + if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { + withScheme = trimmed + } else { + withScheme = "https://\(trimmed)" + } + guard let url = URL(string: withScheme), isValid(url) else { return nil } + return url + } +} + +public enum AIShortcutShareMetadataError: Error, Equatable, Sendable { + case invalidLink + case network + case missingName +} + +public enum AIShortcutShareMetadata { + public static func recordsURL(for shareURL: URL) -> URL? { + guard let token = AIAgentShortcutRun.iCloudShareToken(from: shareURL) else { return nil } + return URL(string: "https://www.icloud.com/shortcuts/api/records/\(token)") + } + + /// Parses `fields.name.value`, or the older `records[0].fields.name.value`. + public static func name(fromRecordsJSON data: Data) throws -> String { + let object = try JSONSerialization.jsonObject(with: data) + guard let root = object as? [String: Any] else { + throw AIShortcutShareMetadataError.missingName + } + if let name = stringName(in: root) { + return name + } + if let records = root["records"] as? [[String: Any]], + let first = records.first, + let name = stringName(in: first) { + return name + } + throw AIShortcutShareMetadataError.missingName + } + + public static func fetchName( + from shareURL: URL, + session: URLSession = .shared + ) async throws -> String { + guard let url = recordsURL(for: shareURL) else { + throw AIShortcutShareMetadataError.invalidLink + } + let data: Data + do { + let (bytes, response) = try await session.data(from: url) + guard let http = response as? HTTPURLResponse, + (200..<300).contains(http.statusCode) else { + throw AIShortcutShareMetadataError.network + } + data = bytes + } catch let error as AIShortcutShareMetadataError { + throw error + } catch { + throw AIShortcutShareMetadataError.network + } + return try name(fromRecordsJSON: data) + } + + private static func stringName(in record: [String: Any]) -> String? { + guard let fields = record["fields"] as? [String: Any], + let nameField = fields["name"] as? [String: Any], + let value = nameField["value"] as? String else { + return nil + } + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } +} diff --git a/OSGKeyboardShared/Services/AppGroupStore.swift b/OSGKeyboardShared/Services/AppGroupStore.swift index 80b2ab4..9cbc4d7 100644 --- a/OSGKeyboardShared/Services/AppGroupStore.swift +++ b/OSGKeyboardShared/Services/AppGroupStore.swift @@ -99,7 +99,11 @@ public struct AppGroupStore: @unchecked Sendable { public var localASRCustomLanguageModelEnabled: Bool { configuration.localASRCustomLanguageModelEnabled } /// Kept off `AppGroupConfiguration.save()` so other settings writes cannot clobber it. public var agentSkillLayout: AIAgentSkillLayout { - Self.decodeAgentSkillLayout(from: defaults) + Self.decodeAgentSkillLayout(from: defaults, userCatalog: agentUserSkillCatalog) + } + + public var agentUserSkillCatalog: AIUserSkillCatalog { + Self.decodeUserSkillCatalog(from: defaults) } // MARK: - Writes @@ -205,7 +209,9 @@ public struct AppGroupStore: @unchecked Sendable { public func setAgentSkillLayout(_ layout: AIAgentSkillLayout) { do { - let data = try JSONEncoder().encode(layout.sanitized()) + let data = try JSONEncoder().encode( + layout.sanitized(catalog: AIClipboardSkillCatalog.all(userCatalog: agentUserSkillCatalog)) + ) defaults.set(data, forKey: AppGroupConfiguration.Keys.agentSkillLayout) } catch { OSGLog.config.warning("agentSkillLayout encode failed: \(error.localizedDescription, privacy: .public)") @@ -213,6 +219,20 @@ public struct AppGroupStore: @unchecked Sendable { AppGroupConfigDarwin.postConfigChanged() } + public func setAgentUserSkillCatalog(_ catalog: AIUserSkillCatalog) { + do { + defaults.set( + try JSONEncoder().encode(catalog), + forKey: AppGroupConfiguration.Keys.agentUserSkillCatalog + ) + } catch { + OSGLog.config.warning( + "agentUserSkillCatalog encode failed: \(error.localizedDescription, privacy: .public)" + ) + } + AppGroupConfigDarwin.postConfigChanged() + } + public func setPendingShortcutRun(skillID: String, titles: [String]) { let payload = AIAgentShortcutRunPayload(skillID: skillID, titles: titles) if let data = AIAgentShortcutRun.encode(payload) { @@ -244,18 +264,37 @@ public struct AppGroupStore: @unchecked Sendable { return payload } - private static func decodeAgentSkillLayout(from defaults: UserDefaults) -> AIAgentSkillLayout { + private static func decodeAgentSkillLayout( + from defaults: UserDefaults, + userCatalog: AIUserSkillCatalog + ) -> AIAgentSkillLayout { + let catalog = AIClipboardSkillCatalog.all(userCatalog: userCatalog) guard let data = defaults.data(forKey: AppGroupConfiguration.Keys.agentSkillLayout) else { return .default } do { - return try JSONDecoder().decode(AIAgentSkillLayout.self, from: data).sanitized() + return try JSONDecoder().decode(AIAgentSkillLayout.self, from: data) + .sanitized(catalog: catalog) } catch { OSGLog.config.warning("agentSkillLayout decode failed: \(error.localizedDescription, privacy: .public)") return .default } } + private static func decodeUserSkillCatalog(from defaults: UserDefaults) -> AIUserSkillCatalog { + guard let data = defaults.data(forKey: AppGroupConfiguration.Keys.agentUserSkillCatalog) else { + return .empty + } + do { + return try JSONDecoder().decode(AIUserSkillCatalog.self, from: data) + } catch { + OSGLog.config.warning( + "agentUserSkillCatalog decode failed: \(error.localizedDescription, privacy: .public)" + ) + return .empty + } + } + public var hasCompletedOnboarding: Bool { get { configuration.hasCompletedOnboarding } set { setHasCompletedOnboarding(newValue) } diff --git a/OSGKeyboardTests/AIAddressExtractionTests.swift b/OSGKeyboardTests/AIAddressExtractionTests.swift new file mode 100644 index 0000000..5cdf8b5 --- /dev/null +++ b/OSGKeyboardTests/AIAddressExtractionTests.swift @@ -0,0 +1,180 @@ +// AIAddressExtractionTests.swift +// OSGKeyboardTests + +import XCTest +@testable import OSGKeyboardShared + +final class AIAddressExtractionTests: XCTestCase { + func testNONEAndEmptyProduceNoItems() { + XCTAssertEqual(AIAddressExtraction.lines(from: "NONE"), []) + XCTAssertEqual(AIAddressExtraction.lines(from: "没有地址"), []) + XCTAssertEqual(AIAddressExtraction.lines(from: "no address"), []) + XCTAssertEqual(AIAddressExtraction.lines(from: " \n "), []) + } + + func testDestinationOnlyKeepsLeadingPipe() { + XCTAssertEqual( + AIAddressExtraction.lines(from: "|三里屯太古里"), + ["|三里屯太古里"] + ) + XCTAssertEqual( + AIAddressExtraction.lines(from: "朝阳区酒仙桥路10号"), + ["|朝阳区酒仙桥路10号"] + ) + XCTAssertEqual( + AIAddressExtraction.lines(from: "|三里屯|"), + ["|三里屯"] + ) + } + + func testOriginAndDestination() { + XCTAssertEqual( + AIAddressExtraction.lines(from: "北京南站|三里屯太古里"), + ["北京南站|三里屯太古里"] + ) + } + + func testFirstValidLineOnly() { + let raw = """ + NONE + 北京南站|三里屯 + 国贸|望京 + """ + XCTAssertEqual(AIAddressExtraction.lines(from: raw), ["北京南站|三里屯"]) + } + + func testSameOriginAndDestinationDropsOrigin() { + XCTAssertEqual( + AIAddressExtraction.lines(from: "故宫|故宫"), + ["|故宫"] + ) + } + + func testRejectsURLDestinations() { + XCTAssertEqual( + AIAddressExtraction.lines(from: "|https://maps.apple.com/?daddr=x"), + [] + ) + } + + func testStripsBullet() { + XCTAssertEqual( + AIAddressExtraction.lines(from: "- |三里屯"), + ["|三里屯"] + ) + } + + func testWholeClipboardEchoIsRejected() { + let source = String(repeating: "这是一段很长的会议纪要内容,包含许多句子。", count: 4) + XCTAssertEqual( + AIAddressExtraction.lines(from: source, sourceClipboard: source), + [] + ) + } + + func testPromptIncludesPipeContractAndNONE() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.navigateID, + locale: "zh", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId + ) + XCTAssertTrue(prompt.contains("起点|终点")) + XCTAssertTrue(prompt.contains("NONE")) + XCTAssertTrue(prompt.contains("不要把整段原文当成一个地点")) + } + + func testEnglishPromptIncludesNONE() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.navigateID, + locale: "en", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId + ) + XCTAssertTrue(prompt.contains("origin|destination")) + XCTAssertTrue(prompt.contains("NONE")) + } +} + +final class AIMapNavigationTests: XCTestCase { + private let destinationOnly = AIMapRoute(origin: nil, destination: "三里屯太古里") + private let twoPoint = AIMapRoute(origin: "北京南站", destination: "三里屯太古里") + + func testPrefersAmapWhenIosamapOpens() { + let url = AIMapNavigation.url(for: destinationOnly) { $0.scheme == "iosamap" } + XCTAssertEqual(url.scheme, "iosamap") + XCTAssertEqual(url.host, "path") + let items = query(url) + XCTAssertEqual(items["sourceApplication"], "OSGKeyboard") + XCTAssertEqual(items["dname"], "三里屯太古里") + XCTAssertEqual(items["sname"], "我的位置") + XCTAssertEqual(items["t"], "0") + } + + func testAmapUsesAmapuriWhenOnlyAmapuriOpens() { + let url = AIMapNavigation.url(for: twoPoint) { $0.scheme == "amapuri" } + XCTAssertEqual(url.scheme, "amapuri") + XCTAssertEqual(url.host, "route") + XCTAssertTrue(url.absoluteString.contains("://route/plan/?")) + let items = query(url) + XCTAssertEqual(items["sname"], "北京南站") + XCTAssertEqual(items["dname"], "三里屯太古里") + } + + func testFallsBackToBaiduWhenAmapMissing() { + let url = AIMapNavigation.url(for: twoPoint) { $0.scheme == "baidumap" } + XCTAssertEqual(url.scheme, "baidumap") + XCTAssertEqual(url.host, "map") + XCTAssertEqual(url.path, "/direction") + let items = query(url) + XCTAssertEqual(items["origin"], "name:北京南站") + XCTAssertEqual(items["destination"], "name:三里屯太古里") + XCTAssertEqual(items["mode"], "driving") + } + + func testBaiduOmitsOriginWhenStartingFromHere() { + let url = AIMapNavigation.url(for: destinationOnly) { $0.scheme == "baidumap" } + let items = query(url) + XCTAssertNil(items["origin"]) + XCTAssertEqual(items["destination"], "name:三里屯太古里") + } + + func testFallsBackToAppleMaps() { + let url = AIMapNavigation.url(for: twoPoint) { _ in false } + XCTAssertEqual(url.scheme, "maps") + XCTAssertTrue(url.absoluteString.hasPrefix("maps://")) + let items = query(url) + XCTAssertEqual(items["saddr"], "北京南站") + XCTAssertEqual(items["daddr"], "三里屯太古里") + XCTAssertEqual(items["dirflg"], "d") + } + + func testAppleOmitsSaddrWhenStartingFromHere() { + let url = AIMapNavigation.url(for: destinationOnly) { _ in false } + let items = query(url) + XCTAssertNil(items["saddr"]) + XCTAssertEqual(items["daddr"], "三里屯太古里") + } + + func testShortcutInputEncodesFirstRoute() { + let text = AIMapNavigation.shortcutInput(from: "北京南站|三里屯") { _ in false } + XCTAssertEqual(text?.hasPrefix("maps:"), true) + XCTAssertTrue(text?.contains("daddr") == true) + } + + func testShortcutInputRejectsNONE() { + XCTAssertNil(AIMapNavigation.shortcutInput(from: "NONE") { _ in false }) + } + + func testProviderOrder() { + XCTAssertEqual(AIMapNavigation.provider { $0.scheme == "iosamap" }, .amap) + XCTAssertEqual(AIMapNavigation.provider { $0.scheme == "baidumap" }, .baidu) + XCTAssertEqual(AIMapNavigation.provider { _ in false }, .apple) + } + + private func query(_ url: URL) -> [String: String] { + let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] + return Dictionary(uniqueKeysWithValues: items.compactMap { item in + guard let value = item.value else { return nil } + return (item.name, value) + }) + } +} diff --git a/OSGKeyboardTests/AIAgentSkillLayoutTests.swift b/OSGKeyboardTests/AIAgentSkillLayoutTests.swift index 1b46f05..7115402 100644 --- a/OSGKeyboardTests/AIAgentSkillLayoutTests.swift +++ b/OSGKeyboardTests/AIAgentSkillLayoutTests.swift @@ -101,6 +101,28 @@ final class AIAgentSkillLayoutTests: XCTestCase { ) } + func testReorderMovesEnabledSkillToIndex() { + let store = AIAgentSkillLayoutStore(defaults: makeDefaults()) + store.moveEnabled(id: AIClipboardSkillCatalog.summarizeID, toIndex: 2) + XCTAssertEqual( + store.layout.enabledIDs, + [ + AIClipboardSkillCatalog.replyID, + AIClipboardSkillCatalog.translateID, + AIClipboardSkillCatalog.summarizeID, + ] + ) + store.moveEnabled(id: AIClipboardSkillCatalog.summarizeID, toIndex: 0) + XCTAssertEqual( + store.layout.enabledIDs, + [ + AIClipboardSkillCatalog.summarizeID, + AIClipboardSkillCatalog.replyID, + AIClipboardSkillCatalog.translateID, + ] + ) + } + func testVisibleEmptyEnabledIDsShowsNoChips() { XCTAssertEqual(AIClipboardSkillCatalog.visible(enabledIDs: []).map(\.id), []) } @@ -183,17 +205,52 @@ final class AIAgentSkillLayoutTests: XCTestCase { ) XCTAssertEqual(skill?.shortcutICloudURL?.host, "www.icloud.com") XCTAssertEqual(skill?.shortcutName, "OSG · 提取待办") + XCTAssertEqual(skill?.shortcutResourceName, "OSGExtractTodos") + } + + func testExtractEventsUsesICloudShareLink() { + let skill = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.extractEventsID) + XCTAssertEqual( + skill?.shortcutICloudURL, + AIClipboardSkillCatalog.extractEventsShortcutICloudURL + ) + XCTAssertEqual(skill?.shortcutICloudURL?.host, "www.icloud.com") + XCTAssertEqual(skill?.shortcutName, "OSG · 提取日程") + XCTAssertEqual( + skill?.shortcutResourceName, + "OSGExtractEvents" + ) + XCTAssertEqual(skill?.systemImage, "calendar") + XCTAssertFalse(skill?.isDefault ?? true) + XCTAssertEqual( + AIAgentShortcutRun.iCloudShareToken(from: skill!.shortcutICloudURL!), + "1f4afcf7ee22400cbf84e319d969aadf" + ) + } + + func testNavigateUsesBundledShortcut() { + let skill = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.navigateID) + XCTAssertNil(skill?.shortcutICloudURL) + XCTAssertEqual(skill?.shortcutName, "OSG · 导航") + XCTAssertEqual(skill?.shortcutResourceName, "OSGNavigate") + XCTAssertEqual( + skill?.systemImage, + "arrow.triangle.turn.up.right.diamond.fill" + ) + XCTAssertFalse(skill?.isDefault ?? true) + XCTAssertEqual(skill?.kind, .export) + XCTAssertTrue(skill?.requiresShortcut ?? false) } func testICloudShareLinkMapsToShortcutsInstallURL() { - let share = URL(string: "https://www.icloud.com/shortcuts/520317da7ae74759b64d5fb069c71f81")! + let share = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")! let url = AIAgentShortcutRun.shortcutsInstallURL(from: share) XCTAssertEqual(url?.scheme, "shortcuts") XCTAssertEqual(url?.host, "shortcuts") - XCTAssertEqual(url?.path, "/520317da7ae74759b64d5fb069c71f81") + XCTAssertEqual(url?.path, "/65bf33ba4206484ba78d582eaf1e9c44") XCTAssertEqual( AIAgentShortcutRun.iCloudShareToken(from: share), - "520317da7ae74759b64d5fb069c71f81" + "65bf33ba4206484ba78d582eaf1e9c44" ) } } diff --git a/OSGKeyboardTests/AIEventExtractionTests.swift b/OSGKeyboardTests/AIEventExtractionTests.swift new file mode 100644 index 0000000..1285ab6 --- /dev/null +++ b/OSGKeyboardTests/AIEventExtractionTests.swift @@ -0,0 +1,143 @@ +// AIEventExtractionTests.swift +// OSGKeyboardTests + +import XCTest +@testable import OSGKeyboardShared + +final class AIEventExtractionTests: XCTestCase { + private var calendar: Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "Asia/Shanghai")! + return calendar + } + + private var now: Date { + date(2026, 8, 13, 20, 24) + } + + func testNONEAndEmptyProduceNoItems() { + XCTAssertEqual(lines("NONE"), []) + XCTAssertEqual(lines("没有日期或时间"), []) + XCTAssertEqual(lines("no events"), []) + XCTAssertEqual(lines(" \n "), []) + } + + func testAllDayDateOnly() { + XCTAssertEqual( + lines("2026-08-15||提交周报|"), + ["2026-08-15|ALLDAY|提交周报|"] + ) + } + + func testTimedFillsDefaultOneHour() { + XCTAssertEqual( + lines("2026-08-15 14:00||项目评审|"), + ["2026-08-15 14:00|2026-08-15 15:00|项目评审|"] + ) + } + + func testTimedKeepsExplicitEndAndLocation() { + XCTAssertEqual( + lines("2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"), + ["2026-08-15 14:00|2026-08-15 16:00|项目评审|3楼会议室"] + ) + } + + func testTimeOnlyUsesToday() { + XCTAssertEqual( + lines("15:00||打电话给客户|"), + ["2026-08-13 15:00|2026-08-13 16:00|打电话给客户|"] + ) + } + + func testTimeOnlyEndOnSameDayOvernightRollsForward() { + XCTAssertEqual( + lines("2026-08-15 23:00|01:00|跨夜值班|"), + ["2026-08-15 23:00|2026-08-16 01:00|跨夜值班|"] + ) + } + + func testTwoFieldStartAndTitle() { + XCTAssertEqual( + lines("2026-08-15 14:00|开会"), + ["2026-08-15 14:00|2026-08-15 15:00|开会|"] + ) + } + + func testThreeFieldStartTitleLocationWhenMiddleIsNotATime() { + XCTAssertEqual( + lines("2026-08-15 14:00|开会|会议室A"), + ["2026-08-15 14:00|2026-08-15 15:00|开会|会议室A"] + ) + } + + func testDropsLinesWithoutStartOrTitle() { + let raw = """ + 买牛奶 + ||无开始| + 2026-08-15 14:00|| + 2026-08-16||有效全天| + """ + XCTAssertEqual(lines(raw), ["2026-08-16|ALLDAY|有效全天|"]) + } + + func testMultipleEventsCapAtTwenty() { + let raw = (1...25).map { "2026-08-15 10:00||任务\($0)|" }.joined(separator: "\n") + let items = lines(raw) + XCTAssertEqual(items.count, 20) + XCTAssertEqual(items.first, "2026-08-15 10:00|2026-08-15 11:00|任务1|") + XCTAssertEqual(items.last, "2026-08-15 10:00|2026-08-15 11:00|任务20|") + } + + func testWholeClipboardEchoIsRejected() { + let source = String(repeating: "这是一段很长的会议纪要内容,包含许多句子。", count: 4) + XCTAssertEqual( + AIEventExtraction.lines( + from: "2026-08-15||\(source)|", + sourceClipboard: source, + now: now, + calendar: calendar + ), + [] + ) + } + + func testPromptIncludesClockPipeContractAndNONE() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.extractEventsID, + locale: "zh", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId, + now: now + ) + XCTAssertTrue(prompt.contains("本地时区")) + XCTAssertTrue(prompt.contains("开始|结束|标题|地点")) + XCTAssertTrue(prompt.contains("NONE")) + XCTAssertTrue(prompt.contains("不要把整段原文当成一条日程")) + } + + func testEnglishPromptIncludesClockAndNONE() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.extractEventsID, + locale: "en", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId, + now: now + ) + XCTAssertTrue(prompt.contains("local timezone")) + XCTAssertTrue(prompt.contains("NONE")) + XCTAssertTrue(prompt.contains("all-day")) + } + + private func lines(_ raw: String) -> [String] { + AIEventExtraction.lines(from: raw, now: now, calendar: calendar) + } + + private func date(_ y: Int, _ m: Int, _ d: Int, _ h: Int, _ min: Int) -> Date { + var components = DateComponents() + components.year = y + components.month = m + components.day = d + components.hour = h + components.minute = min + return calendar.date(from: components)! + } +} diff --git a/OSGKeyboardTests/AIHintKeywordExtractorTests.swift b/OSGKeyboardTests/AIHintKeywordExtractorTests.swift index 18a80ed..a27bada 100644 --- a/OSGKeyboardTests/AIHintKeywordExtractorTests.swift +++ b/OSGKeyboardTests/AIHintKeywordExtractorTests.swift @@ -245,4 +245,24 @@ final class AIClipboardSkillTests: XCTestCase { XCTAssertTrue(prompt.contains("NONE")) XCTAssertTrue(prompt.contains("不要把整段原文当成一条待办")) } + + func testExtractEventsAsksForNONEWhenEmpty() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.extractEventsID, + locale: "zh", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId + ) + XCTAssertTrue(prompt.contains("NONE")) + XCTAssertTrue(prompt.contains("开始|结束|标题|地点")) + } + + func testNavigateAsksForNONEWhenEmpty() { + let prompt = AIClipboardSkillCatalog.instruction( + skillID: AIClipboardSkillCatalog.navigateID, + locale: "zh", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId + ) + XCTAssertTrue(prompt.contains("NONE")) + XCTAssertTrue(prompt.contains("起点|终点")) + } } diff --git a/OSGKeyboardTests/AIModeLLMClientTests.swift b/OSGKeyboardTests/AIModeLLMClientTests.swift index c43f29c..c43b750 100644 --- a/OSGKeyboardTests/AIModeLLMClientTests.swift +++ b/OSGKeyboardTests/AIModeLLMClientTests.swift @@ -77,6 +77,18 @@ final class AIModeLLMClientTests: XCTestCase { XCTAssertTrue(client is AIModeSearchFallbackClient) } + func testFactorySkipsSearchWhenThinkingDisabled() { + let client = AIModeLLMClientFactory.make( + providerId: "openai", + baseURL: "https://api.openai.com/v1", + apiKey: "sk-test", + model: "gpt-5.4-mini", + thinkingEnabled: false + ) + XCTAssertFalse(client is AIModeSearchFallbackClient) + XCTAssertTrue(client is OpenAICompatibleClient) + } + func testFactoryPlainForGroq() { let client = AIModeLLMClientFactory.make( providerId: "groq", diff --git a/OSGKeyboardTests/AIUserSkillTests.swift b/OSGKeyboardTests/AIUserSkillTests.swift new file mode 100644 index 0000000..0275f8a --- /dev/null +++ b/OSGKeyboardTests/AIUserSkillTests.swift @@ -0,0 +1,189 @@ +// AIUserSkillTests.swift +// OSGKeyboardTests + +import XCTest +@testable import OSGKeyboardShared + +final class AIUserSkillTests: XCTestCase { + private let sampleURL = URL( + string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44" + )! + + func testValidICloudShareLink() { + XCTAssertNotNil(AIShortcutShareLink.parse( + "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44" + )) + XCTAssertNotNil(AIShortcutShareLink.parse( + "https://icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf" + )) + XCTAssertNil(AIShortcutShareLink.parse("https://example.com/shortcuts/abc")) + XCTAssertNil(AIShortcutShareLink.parse("https://www.icloud.com/shortcuts/api/records/x")) + XCTAssertNil(AIShortcutShareLink.parse("not a url")) + XCTAssertNil(AIShortcutShareLink.parse("https://www.icloud.com/shortcuts/short")) + } + + func testParsesShortcutNameFromRecordsJSON() throws { + let json = """ + {"fields":{"name":{"type":"STRING","value":"OSG · 提取待办"}}} + """.data(using: .utf8)! + XCTAssertEqual(try AIShortcutShareMetadata.name(fromRecordsJSON: json), "OSG · 提取待办") + } + + func testParsesShortcutNameFromLegacyRecordsArray() throws { + let json = """ + {"records":[{"fields":{"name":{"value":"OSG · 提取日程"}}}]} + """.data(using: .utf8)! + XCTAssertEqual(try AIShortcutShareMetadata.name(fromRecordsJSON: json), "OSG · 提取日程") + } + + func testRejectsRecordsJSONWithoutName() { + let json = Data(#"{"fields":{}}"#.utf8) + XCTAssertThrowsError(try AIShortcutShareMetadata.name(fromRecordsJSON: json)) + } + + func testCatalogUpsertKeepsShortcutNameIndependentOfSkillName() throws { + var catalog = AIUserSkillCatalog() + var skill = AIUserSkill( + name: "会议纪要", + summary: "抽要点", + prompt: "提取要点", + shortcutICloudURL: sampleURL, + shortcutName: "OSG · 提取待办" + ) + try catalog.upsert(skill) + XCTAssertEqual(catalog.entries.first?.name, "会议纪要") + XCTAssertEqual(catalog.entries.first?.shortcutName, "OSG · 提取待办") + + skill.name = "纪要" + skill.shortcutName = "My Tasks" + try catalog.upsert(skill) + XCTAssertEqual(catalog.entries.count, 1) + XCTAssertEqual(catalog.entries.first?.name, "纪要") + XCTAssertEqual(catalog.entries.first?.shortcutName, "My Tasks") + } + + func testThinkingDefaultsOffAndBuiltinCannotEnable() { + let user = AIUserSkill( + name: "Custom", + prompt: "Do it", + shortcutICloudURL: sampleURL, + shortcutName: "Run Me" + ) + XCTAssertFalse(user.thinkingEnabled) + XCTAssertFalse(user.asClipboardSkill().thinkingEnabled) + + let withThinking = AIUserSkill( + name: "Custom", + prompt: "Do it", + shortcutICloudURL: sampleURL, + shortcutName: "Run Me", + thinkingEnabled: true + ) + XCTAssertTrue(withThinking.asClipboardSkill().thinkingEnabled) + + let builtin = AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.replyID) + XCTAssertEqual(builtin?.thinkingEnabled, false) + } + + func testInstructionUsesCustomPrompt() { + let skill = AIUserSkill( + name: "Custom", + prompt: "只输出一行标题", + shortcutICloudURL: sampleURL, + shortcutName: "Run Me" + ).asClipboardSkill() + XCTAssertEqual( + AIClipboardSkillCatalog.instruction( + for: skill, + locale: "zh", + translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId + ), + "只输出一行标题" + ) + } + + func testVisibleIncludesUserSkills() throws { + var catalog = AIUserSkillCatalog() + let skill = AIUserSkill( + name: "Custom", + prompt: "Do it", + shortcutICloudURL: sampleURL, + shortcutName: "Run Me" + ) + try catalog.upsert(skill) + let visible = AIClipboardSkillCatalog.visible( + enabledIDs: [skill.id], + userCatalog: catalog + ) + XCTAssertEqual(visible.map(\.id), [skill.id]) + XCTAssertEqual(visible.first?.customName, "Custom") + } + + func testSanitizeKeepsConfirmedUserExportSkill() throws { + var catalog = AIUserSkillCatalog() + let skill = AIUserSkill( + name: "Custom", + prompt: "Do it", + shortcutICloudURL: sampleURL, + shortcutName: "Run Me" + ) + try catalog.upsert(skill) + let layout = AIAgentSkillLayout( + enabledIDs: [skill.id], + confirmedShortcutIDs: [skill.id] + ).sanitized(catalog: AIClipboardSkillCatalog.all(userCatalog: catalog)) + XCTAssertEqual(layout.enabledIDs, [skill.id]) + } + + func testGenericExportSplitsLinesAndHonorsNONE() { + XCTAssertEqual(AIGenericSkillExport.items(from: "NONE"), []) + XCTAssertEqual(AIGenericSkillExport.items(from: "买牛奶\n回邮件"), ["买牛奶", "回邮件"]) + XCTAssertEqual(AIGenericSkillExport.items(from: "一段没有换行的结果"), ["一段没有换行的结果"]) + } + + func testNoUserSkillLimit() throws { + var catalog = AIUserSkillCatalog() + for index in 0..<12 { + try catalog.upsert( + AIUserSkill( + name: "Skill \(index)", + prompt: "Do it", + shortcutICloudURL: sampleURL, + shortcutName: "Run \(index)" + ) + ) + } + XCTAssertEqual(catalog.entries.count, 12) + } +} + +@MainActor +final class AIUserSkillStoreTests: XCTestCase { + private func makeDefaults() -> UserDefaults { + let suite = "group.com.osgkeyboard.shared.tests.userSkills.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suite)! + defaults.removePersistentDomain(forName: suite) + return defaults + } + + func testChangingShortcutLinkDropsConfirmation() throws { + let store = AIAgentSkillLayoutStore(defaults: makeDefaults()) + let firstURL = URL(string: "https://www.icloud.com/shortcuts/65bf33ba4206484ba78d582eaf1e9c44")! + let secondURL = URL(string: "https://www.icloud.com/shortcuts/1f4afcf7ee22400cbf84e319d969aadf")! + var skill = AIUserSkill( + name: "Custom", + prompt: "Do it", + shortcutICloudURL: firstURL, + shortcutName: "One" + ) + try store.saveUserSkill(skill) + XCTAssertEqual(store.confirmShortcutAndEnable(skill.id), .enabled) + XCTAssertTrue(store.layout.isEnabled(skill.id)) + + skill.shortcutICloudURL = secondURL + skill.shortcutName = "Two" + try store.saveUserSkill(skill) + XCTAssertFalse(store.layout.hasConfirmedShortcut(skill.id)) + XCTAssertFalse(store.layout.isEnabled(skill.id)) + } +} diff --git a/OSGKeyboardTests/FlowSessionBridgeTests.swift b/OSGKeyboardTests/FlowSessionBridgeTests.swift index 78b28d1..24be168 100644 --- a/OSGKeyboardTests/FlowSessionBridgeTests.swift +++ b/OSGKeyboardTests/FlowSessionBridgeTests.swift @@ -323,6 +323,47 @@ final class FlowSessionBridgeTests: XCTestCase { XCTAssertEqual(decoded, command) XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容") + XCTAssertNil(decoded.aiThinkingEnabled) + } + + func testSubmitAIQuestionCommandRoundTripsThinkingOverride() throws { + let command = FlowCommand( + sessionId: UUID(), + utteranceId: UUID(), + commandSeq: 45, + action: .submitAIQuestion, + localeId: "zh-Hans", + utteranceMode: .aiQuestion, + aiConversationID: UUID(), + aiQuestionText: "总结这段剪贴板内容", + aiThinkingEnabled: false + ) + let decoded = try JSONDecoder().decode( + FlowCommand.self, + from: JSONEncoder().encode(command) + ) + XCTAssertEqual(decoded.aiThinkingEnabled, false) + } + + func testFlowCommandDecodesLegacyJSONWithoutThinkingKey() throws { + let command = FlowCommand( + sessionId: UUID(), + utteranceId: UUID(), + commandSeq: 44, + action: .submitAIQuestion, + localeId: "zh-Hans", + utteranceMode: .aiQuestion, + aiConversationID: UUID(), + aiQuestionText: "总结这段剪贴板内容" + ) + var object = try JSONSerialization.jsonObject( + with: JSONEncoder().encode(command) + ) as! [String: Any] + object.removeValue(forKey: "aiThinkingEnabled") + let data = try JSONSerialization.data(withJSONObject: object) + let decoded = try JSONDecoder().decode(FlowCommand.self, from: data) + XCTAssertNil(decoded.aiThinkingEnabled) + XCTAssertEqual(decoded.aiQuestionText, "总结这段剪贴板内容") } func testSecureFieldContextRedactsText() { diff --git a/Scripts/generate_extract_events_shortcut.py b/Scripts/generate_extract_events_shortcut.py new file mode 100644 index 0000000..b6141e3 --- /dev/null +++ b/Scripts/generate_extract_events_shortcut.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Build the companion Shortcut for the Extract Events skill. + +Writes an unsigned binary plist that `shortcuts sign` can notarize. + +Workflow (must materialize Shortcut Input as Text first — Split Text's +WFInput/ExtensionInput binding finishes the run without iterating): + + Text (Shortcut Input) + → Split Text (new lines) + → Repeat Each + → Split Text (|) + → Get items 1–4 (start, end, title, location) + → If end == ALLDAY + → Add New Event (all-day) + Otherwise + → Add New Event (timed) +""" + +from __future__ import annotations + +import plistlib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "OSGKeyboard" / "Resources" / "Shortcuts" +UNSIGNED = OUT_DIR / "OSGExtractEvents.unsigned.shortcut" +SIGNED = OUT_DIR / "OSGExtractEvents.shortcut" + +OBJECT_REPLACEMENT = "\ufffc" + +TEXT_UUID = "A1B2C3D0-1111-4E00-8A5A-FD7E1B0C396F" +SPLIT_LINES_UUID = "A1B2C3D1-2222-4F11-9A6B-0E8F2C1D4A70" +REPEAT_GROUP = "A1B2C3D2-3333-4E02-B3C4-91F0E6A7B812" +REPEAT_START = "A1B2C3D3-4444-4F13-A4D5-02A1F7B8C923" +SPLIT_FIELDS_UUID = "A1B2C3D4-5555-4014-B5E6-13B2A8C9D034" +START_UUID = "A1B2C3D5-6666-4125-C6F7-24C3B9DAE145" +END_UUID = "A1B2C3D6-7777-4236-D7A8-35D4CAEBF256" +TITLE_UUID = "A1B2C3D7-8888-4347-E8B9-46E5DBF0C367" +LOCATION_UUID = "A1B2C3D8-9999-4458-F9CA-57F6EC01D478" +IF_GROUP = "A1B2C3D9-AAAA-4569-0ADB-6807FD12E589" +IF_START = "A1B2C3DA-BBBB-467A-1BEC-79180E23F69A" +EVENT_ALLDAY_UUID = "A1B2C3DB-CCCC-478B-2CFD-8A291F3407AB" +IF_ELSE = "A1B2C3DC-DDDD-489C-3D0E-9B3A204518BC" +EVENT_TIMED_UUID = "A1B2C3DD-EEEE-49AD-4E1F-AC4B315629CD" +IF_END = "A1B2C3DE-FFFF-4ABE-5F20-BD5C42673ADE" +REPEAT_END = "A1B2C3DF-0000-4BCF-6031-CE6D53784BEF" + + +def attachment(value: dict) -> dict: + return {"Value": value, "WFSerializationType": "WFTextTokenAttachment"} + + +def token_string(attachment_value: dict) -> dict: + return { + "Value": { + "attachmentsByRange": {"{0, 1}": attachment_value}, + "string": OBJECT_REPLACEMENT, + }, + "WFSerializationType": "WFTextTokenString", + } + + +def shortcut_input_text() -> dict: + return token_string({"Type": "ExtensionInput"}) + + +def action_output_text(output_name: str, output_uuid: str) -> dict: + return token_string( + { + "Aggrandizements": [], + "OutputName": output_name, + "OutputUUID": output_uuid, + "Type": "ActionOutput", + } + ) + + +def action_output_attachment(output_name: str, output_uuid: str) -> dict: + return attachment( + { + "Aggrandizements": [], + "OutputName": output_name, + "OutputUUID": output_uuid, + "Type": "ActionOutput", + } + ) + + +def named_variable_text(variable_name: str) -> dict: + return token_string( + { + "Aggrandizements": [], + "Type": "Variable", + "VariableName": variable_name, + } + ) + + +def if_input(output_name: str, output_uuid: str) -> dict: + return { + "Type": "Variable", + "Variable": { + "Value": { + "Aggrandizements": [], + "OutputName": output_name, + "OutputUUID": output_uuid, + "Type": "ActionOutput", + }, + "WFSerializationType": "WFTextTokenAttachment", + }, + } + + +def get_item(uuid: str, name: str, index: int) -> dict: + return { + "WFWorkflowActionIdentifier": "is.workflow.actions.getitemfromlist", + "WFWorkflowActionParameters": { + "UUID": uuid, + "CustomOutputName": name, + "WFInput": action_output_attachment("字段", SPLIT_FIELDS_UUID), + "WFItemSpecifier": "Item At Index", + "WFItemIndex": index, + }, + } + + +def add_event( + uuid: str, + *, + all_day: bool, + include_end: bool, +) -> dict: + parameters = { + "UUID": uuid, + "WFCalendarItemTitle": action_output_text("标题", TITLE_UUID), + "WFCalendarItemLocation": action_output_text("地点", LOCATION_UUID), + "WFCalendarItemDates": True, + "WFCalendarItemStartDate": action_output_text("开始", START_UUID), + "WFCalendarItemAllDay": all_day, + "WFCalendarItemShowComposer": False, + "ShowComposeSheet": False, + "WFShowWhenRun": False, + } + if include_end: + parameters["WFCalendarItemEndDate"] = action_output_text("结束", END_UUID) + return { + "WFWorkflowActionIdentifier": "is.workflow.actions.addnewevent", + "WFWorkflowActionParameters": parameters, + } + + +def workflow() -> dict: + return { + "WFWorkflowClientVersion": "3600.0.4", + "WFWorkflowClientRelease": "26.0", + "WFWorkflowMinimumClientVersion": 900, + "WFWorkflowMinimumClientVersionString": "900", + "WFWorkflowName": "OSG · 提取日程", + "WFWorkflowHasShortcutInputVariables": True, + "WFWorkflowHasOutputFallback": False, + "WFWorkflowImportQuestions": [], + "WFWorkflowTypes": [], + "WFWorkflowInputContentItemClasses": [ + "WFStringContentItem", + "WFRichTextContentItem", + ], + "WFWorkflowOutputContentItemClasses": [], + "WFWorkflowNoInputBehavior": { + "Name": "WFWorkflowNoInputBehaviorAskForInput", + "Parameters": { + "ItemClass": "WFStringContentItem", + }, + }, + "WFWorkflowIcon": { + "WFWorkflowIconGlyphNumber": 59675, + "WFWorkflowIconStartColor": 4286828031, + }, + "WFWorkflowActions": [ + { + "WFWorkflowActionIdentifier": "is.workflow.actions.gettext", + "WFWorkflowActionParameters": { + "UUID": TEXT_UUID, + "CustomOutputName": "快捷指令文本", + "WFTextActionText": shortcut_input_text(), + }, + }, + { + "WFWorkflowActionIdentifier": "is.workflow.actions.text.split", + "WFWorkflowActionParameters": { + "UUID": SPLIT_LINES_UUID, + "CustomOutputName": "行", + "text": action_output_text("快捷指令文本", TEXT_UUID), + "WFTextSeparator": "New Lines", + }, + }, + { + "WFWorkflowActionIdentifier": "is.workflow.actions.repeat.each", + "WFWorkflowActionParameters": { + "UUID": REPEAT_START, + "GroupingIdentifier": REPEAT_GROUP, + "WFControlFlowMode": 0, + "WFInput": action_output_attachment("行", SPLIT_LINES_UUID), + }, + }, + { + "WFWorkflowActionIdentifier": "is.workflow.actions.text.split", + "WFWorkflowActionParameters": { + "UUID": SPLIT_FIELDS_UUID, + "CustomOutputName": "字段", + "text": named_variable_text("Repeat Item"), + "WFTextSeparator": "Custom", + "WFTextCustomSeparator": "|", + }, + }, + get_item(START_UUID, "开始", 1), + get_item(END_UUID, "结束", 2), + get_item(TITLE_UUID, "标题", 3), + get_item(LOCATION_UUID, "地点", 4), + { + "WFWorkflowActionIdentifier": "is.workflow.actions.conditional", + "WFWorkflowActionParameters": { + "UUID": IF_START, + "GroupingIdentifier": IF_GROUP, + "WFControlFlowMode": 0, + "WFCondition": 4, + "WFConditionalActionString": "ALLDAY", + "WFInput": if_input("结束", END_UUID), + }, + }, + add_event(EVENT_ALLDAY_UUID, all_day=True, include_end=False), + { + "WFWorkflowActionIdentifier": "is.workflow.actions.conditional", + "WFWorkflowActionParameters": { + "UUID": IF_ELSE, + "GroupingIdentifier": IF_GROUP, + "WFControlFlowMode": 1, + }, + }, + add_event(EVENT_TIMED_UUID, all_day=False, include_end=True), + { + "WFWorkflowActionIdentifier": "is.workflow.actions.conditional", + "WFWorkflowActionParameters": { + "UUID": IF_END, + "GroupingIdentifier": IF_GROUP, + "WFControlFlowMode": 2, + }, + }, + { + "WFWorkflowActionIdentifier": "is.workflow.actions.repeat.each", + "WFWorkflowActionParameters": { + "UUID": REPEAT_END, + "GroupingIdentifier": REPEAT_GROUP, + "WFControlFlowMode": 2, + }, + }, + ], + } + + +def main() -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + UNSIGNED.write_bytes(plistlib.dumps(workflow(), fmt=plistlib.FMT_BINARY)) + print(f"wrote {UNSIGNED}") + print( + "sign with: shortcuts sign --mode anyone " + f"--input {UNSIGNED} --output {SIGNED}" + ) + + +if __name__ == "__main__": + main() diff --git a/Scripts/generate_navigate_shortcut.py b/Scripts/generate_navigate_shortcut.py new file mode 100644 index 0000000..55567f5 --- /dev/null +++ b/Scripts/generate_navigate_shortcut.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Build the companion Shortcut for the Navigate skill. + +Writes an unsigned binary plist that `shortcuts sign` can notarize. + +The host already picked 高德 → 百度 → Apple Maps and passes one URL. +This recipe only materializes Shortcut Input as Text and opens it: + + Text (Shortcut Input) + → Open URLs +""" + +from __future__ import annotations + +import plistlib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "OSGKeyboard" / "Resources" / "Shortcuts" +UNSIGNED = OUT_DIR / "OSGNavigate.unsigned.shortcut" +SIGNED = OUT_DIR / "OSGNavigate.shortcut" + +OBJECT_REPLACEMENT = "\ufffc" + +TEXT_UUID = "C1D2E3F0-1111-4E00-8A5A-FD7E1B0C396F" +OPEN_UUID = "C1D2E3F1-2222-4F11-9A6B-0E8F2C1D4A70" + + +def token_string(attachment_value: dict) -> dict: + return { + "Value": { + "attachmentsByRange": {"{0, 1}": attachment_value}, + "string": OBJECT_REPLACEMENT, + }, + "WFSerializationType": "WFTextTokenString", + } + + +def shortcut_input_text() -> dict: + return token_string({"Type": "ExtensionInput"}) + + +def action_output_text(output_name: str, output_uuid: str) -> dict: + return token_string( + { + "Aggrandizements": [], + "OutputName": output_name, + "OutputUUID": output_uuid, + "Type": "ActionOutput", + } + ) + + +def workflow() -> dict: + return { + "WFWorkflowClientVersion": "3600.0.4", + "WFWorkflowClientRelease": "26.0", + "WFWorkflowMinimumClientVersion": 900, + "WFWorkflowMinimumClientVersionString": "900", + "WFWorkflowName": "OSG · 导航", + "WFWorkflowHasShortcutInputVariables": True, + "WFWorkflowHasOutputFallback": False, + "WFWorkflowImportQuestions": [], + "WFWorkflowTypes": [], + "WFWorkflowInputContentItemClasses": [ + "WFStringContentItem", + "WFURLContentItem", + "WFRichTextContentItem", + ], + "WFWorkflowOutputContentItemClasses": [], + "WFWorkflowNoInputBehavior": { + "Name": "WFWorkflowNoInputBehaviorAskForInput", + "Parameters": { + "ItemClass": "WFStringContentItem", + }, + }, + "WFWorkflowIcon": { + "WFWorkflowIconGlyphNumber": 59723, + "WFWorkflowIconStartColor": 4278222847, + }, + "WFWorkflowActions": [ + { + "WFWorkflowActionIdentifier": "is.workflow.actions.gettext", + "WFWorkflowActionParameters": { + "UUID": TEXT_UUID, + "CustomOutputName": "快捷指令文本", + "WFTextActionText": shortcut_input_text(), + }, + }, + { + "WFWorkflowActionIdentifier": "is.workflow.actions.openurl", + "WFWorkflowActionParameters": { + "UUID": OPEN_UUID, + "WFURL": action_output_text("快捷指令文本", TEXT_UUID), + "WFShowWhenRun": False, + }, + }, + ], + } + + +def main() -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + UNSIGNED.write_bytes(plistlib.dumps(workflow(), fmt=plistlib.FMT_BINARY)) + print(f"wrote {UNSIGNED}") + print( + "sign with: shortcuts sign --mode anyone " + f"--input {UNSIGNED} --output {SIGNED}" + ) + + +if __name__ == "__main__": + main() diff --git a/Tests/suite-manifest.json b/Tests/suite-manifest.json index 5486e70..81e1073 100644 --- a/Tests/suite-manifest.json +++ b/Tests/suite-manifest.json @@ -134,6 +134,10 @@ "OSGKeyboardTests/AIHintPoolTests", "OSGKeyboardTests/AIHintKeywordExtractorTests", "OSGKeyboardTests/AIAgentSkillLayoutTests", + "OSGKeyboardTests/AIUserSkillTests", + "OSGKeyboardTests/AIUserSkillStoreTests", + "OSGKeyboardTests/AIEventExtractionTests", + "OSGKeyboardTests/AIAddressExtractionTests", "OSGKeyboardTests/ClipboardHistoryPolicyTests", "OSGKeyboardTests/ClipboardHistoryStoreTests" ] diff --git a/project.yml b/project.yml index 48c18a4..4671e18 100644 --- a/project.yml +++ b/project.yml @@ -49,7 +49,7 @@ settings: STRING_CATALOG_GENERATE_SYMBOLS: YES CLANG_CXX_LANGUAGE_STANDARD: c++17 MARKETING_VERSION: "1.7.5" - CURRENT_PROJECT_VERSION: "67" + CURRENT_PROJECT_VERSION: "69" # 签名配置来自 Signing.local.xcconfig(gitignored,不会被覆盖) # 项目级签名 xcconfig,适用于所有 target @@ -88,6 +88,17 @@ targets: buildPhase: resources - path: OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings buildPhase: resources + # `.shortcut` is not a XcodeGen-known resource type; list each file + # as an explicit resource source or it never lands in the app bundle. + - path: OSGKeyboard/Resources/Shortcuts/OSGExtractTodos.shortcut + type: file + buildPhase: resources + - path: OSGKeyboard/Resources/Shortcuts/OSGExtractEvents.shortcut + type: file + buildPhase: resources + - path: OSGKeyboard/Resources/Shortcuts/OSGNavigate.shortcut + type: file + buildPhase: resources entitlements: path: OSGKeyboard/OSGKeyboard.entitlements properties: @@ -121,7 +132,6 @@ targets: - path: OSGKeyboard/Resources/Typing/Rime/manifest.json - path: OSGKeyboard/Resources/HostCLM/v1/OSGKeyboardCLM.bin - path: OSGKeyboard/Resources/HostCLM/v1/compiled-manifest.json - - path: OSGKeyboard/Resources/Shortcuts/OSGExtractTodos.shortcut - path: OSGKeyboard/Resources/LocalASR/local-asr-catalog.json - path: OSGKeyboard/Resources/Typing/Licenses/NOTICE.txt - path: OSGKeyboard/Resources/Typing/Licenses/LICENSE.txt @@ -197,6 +207,10 @@ targets: - skype - zoomus - shortcuts + - iosamap + - amapuri + - baidumap + - maps - things - todoist - evernote