{"id":23152419,"url":"https://github.com/1amageek/toolbar","last_synced_at":"2026-05-22T06:10:41.848Z","repository":{"id":56924390,"uuid":"88851916","full_name":"1amageek/Toolbar","owner":"1amageek","description":"Awesome autolayout Toolbar. Toolbar is a library for iOS. You can easily create chat InputBar.","archived":false,"fork":false,"pushed_at":"2021-01-14T08:42:42.000Z","size":4469,"stargazers_count":455,"open_issues_count":8,"forks_count":32,"subscribers_count":9,"default_branch":"main","last_synced_at":"2024-11-24T16:42:51.998Z","etag":null,"topics":["autolayout","inputbar","ios","swift","toolbar"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/1amageek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-20T10:12:47.000Z","updated_at":"2024-10-17T01:30:00.000Z","dependencies_parsed_at":"2022-08-21T05:20:45.646Z","dependency_job_id":null,"html_url":"https://github.com/1amageek/Toolbar","commit_stats":null,"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FToolbar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FToolbar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FToolbar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FToolbar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/1amageek","download_url":"https://codeload.github.com/1amageek/Toolbar/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247294541,"owners_count":20915340,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["autolayout","inputbar","ios","swift","toolbar"],"created_at":"2024-12-17T19:14:37.936Z","updated_at":"2026-05-22T06:10:41.842Z","avatar_url":"https://github.com/1amageek.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Toolbar\n\nA SwiftUI Liquid Glass composer kit for AI chat interfaces.\n\n`Toolbar` provides the primitives modern AI input bars are built from — multi-line editor, file / image / path attachments, slash commands, voice input, and a unified Liquid Glass surface — and lets you compose them declaratively. There is no monolithic `Toolbar` view: you put what you need inside a `ToolbarContainer`.\n\n## Requirements\n\n| | |\n|---|---|\n| Swift | 6.2+ |\n| Platforms | iOS 26+ / iPadOS 26+ / macOS 26+ |\n| Xcode | 26+ |\n\n## Installation\n\nSwift Package Manager:\n\n```swift\n.package(url: \"https://github.com/1amageek/Toolbar.git\", branch: \"main\")\n```\n\nThen add `Toolbar` to your target dependencies.\n\n## Quick start\n\nA minimal composer with a menu, editor, and adaptive trailing button (Send / Stop / Voice):\n\n```swift\nimport SwiftUI\nimport Toolbar\n\nstruct ChatView: View {\n    @State private var text = \"\"\n    @State private var height: CGFloat = ToolbarControlMetrics.circleDiameter\n    @State private var isFocused = false\n    @State private var isStreaming = false\n\n    var body: some View {\n        ScrollView {\n            messageList\n        }\n        .safeAreaInset(edge: .bottom) {\n            ToolbarContainer {\n                HStack(alignment: .bottom, spacing: 8) {\n                    ToolbarMenuButton {\n                        Button(\"File\",   systemImage: \"doc\")   { pickFile() }\n                        Button(\"Image\",  systemImage: \"photo\") { pickImage() }\n                        Button(\"Folder\", systemImage: \"folder\"){ pickFolder() }\n                    } label: {\n                        Image(systemName: \"plus\")\n                    }\n\n                    ToolbarEditor(\n                        text: $text,\n                        contentHeight: $height,\n                        isFocused: $isFocused,\n                        placeholder: \"Message...\"\n                    )\n                    .frame(\n                        minHeight: ToolbarControlMetrics.circleDiameter,\n                        maxHeight: max(ToolbarControlMetrics.circleDiameter, min(height, 220))\n                    )\n\n                    if isStreaming {\n                        StopButton(action: cancel)\n                    } else {\n                        SendButton(isEnabled: !text.isEmpty, action: send)\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n`ToolbarContainer` paints **one** continuous Liquid Glass slab and wraps slab-local children in a `GlassEffectContainer`, so glass-circle buttons and attachment chips morph cohesively with the slab. Floating surfaces such as slash command suggestions belong in `.popup { }`, outside the slab. Embed the toolbar via `.safeAreaInset(edge: .bottom)` on the message scroll view — never directly inside a `VStack`.\n\nUse `ToolbarContainer(contentInsets:)` for padding inside the glass slab. Use regular `.padding(...)` outside the container for spacing around the whole toolbar.\n\n## Voice + accessory area\n\n`.accessory { }` inserts a view above the composer content within the same slab. Use it for ephemeral state strips: attachment chips, live waveforms during recording, \"transcribing…\" indicators, error banners, draft previews, suggestion chips, upload progress.\n\n```swift\n@State private var voiceState: VoiceState = .idle\n@State private var amplitudes: [Float] = []\n@State private var amplitudeSource: (any VoiceAmplitudeSource)? = nil\n\nToolbarContainer {\n    HStack(alignment: .bottom, spacing: 8) {\n        // ... menu, editor ...\n\n        if text.isEmpty {\n            VoiceButton(\n                provider: voiceProvider,\n                onResult: { result in /* finalized text/audio */ },\n                onStateChange: { state in\n                    withAnimation(.smooth(duration: 0.25)) { voiceState = state }\n                    amplitudeSource = (state == .recording)\n                        ? voiceProvider as? any VoiceAmplitudeSource\n                        : nil\n                }\n            )\n        } else {\n            SendButton(isEnabled: true, action: send)\n        }\n    }\n}\n.accessory {\n    switch voiceState {\n    case .idle:\n        EmptyView()\n    case .recording:\n        VoiceWaveform(amplitudes: amplitudes)\n            .transition(.opacity.combined(with: .scale(scale: 0.96)))\n    case .transcribing:\n        TranscribingIndicator()\n            .transition(.opacity.combined(with: .scale(scale: 0.96)))\n    }\n}\n.voiceAmplitudes(from: amplitudeSource, into: $amplitudes)\n```\n\nThe `.transcribing` state exists so the accessory **stays visible** between `stopRecording()` and the final result — without it the bar would briefly collapse and reflow once the transcript arrives.\n\n## Slash commands\n\nSlash popup goes in `.popup(isPresented:)`, outside the toolbar slab. Present it only while the editor text is in slash-command mode:\n\n```swift\nToolbarContainer {\n    HStack(alignment: .bottom, spacing: 8) {\n        // ... editor with $text driving `matches` via SlashCommandProvider ...\n    }\n}\n.popup(isPresented: text.hasPrefix(\"/\")) {\n    SlashCommandPopup(\n        commands: matches,\n        selectedIndex: selectedIndex,\n        onSelect: commit\n    )\n}\n```\n\nImplement `SlashCommandProvider` against your own command source, or use the bundled `StaticSlashCommandProvider` for fixed lists.\n\n## Attachments\n\nThree URL-backed concrete types (`FileAttachment`, `ImageAttachment`, `PathAttachment`) all conform to the `ToolbarAttachment` protocol. Render them as fixed-height previews with `AttachmentChip` in the accessory area above the editor row. Image attachments keep their aspect ratio and render as rounded thumbnails; other attachments render as extension badges. The thumbnail radius is inset-aware so its curve aligns with the outer toolbar slab:\n\n```swift\nToolbarContainer {\n    HStack(alignment: .bottom, spacing: 8) {\n        // ... menu, editor, trailing action ...\n    }\n}\n.accessory {\n    if !attachments.isEmpty {\n        AttachmentStrip(attachments: attachments) { attachment in\n            remove(attachment)\n        }\n    }\n}\n```\n\nFor inline `[[marker]]` attachments rendered directly inside the editor text, conform to `InlineAttachmentRenderer`.\n\n## Public surface\n\n| Component | Role |\n|---|---|\n| `ToolbarContainer` | Liquid Glass slab + `GlassEffectContainer` morph domain |\n| `.popup { }` | Modifier inserting a floating surface above the slab |\n| `.accessory { }` | Modifier inserting an ephemeral strip above the content |\n| `.footer { }` | Modifier inserting persistent controls below the content |\n| `ToolbarEditor` | Cross-platform multi-line editor (`NSTextView` / `UITextView` backed) |\n| `ToolbarMenuButton` | Glass-circle menu styled to match other buttons |\n| `SendButton` / `StopButton` | Trailing action buttons with shared metrics |\n| `VoiceButton` | Mic ↔ stop, drives a `VoiceInputProvider` and emits `VoiceState` |\n| `VoiceWaveform` | Bar-graph visualization fed by `VoiceAmplitudeSource` |\n| `TranscribingIndicator` | Progress strip for the post-recording analysis state |\n| `AttachmentChip` | Fixed-height preview for any `ToolbarAttachment` |\n| `AttachmentStrip` | Horizontal attachment row with scroll clipping disabled |\n| `SlashCommandPopup` | Glass popup list of matches |\n| `GlassCircleButtonStyle` | Re-usable circular Liquid Glass button style |\n| `ToolbarControlMetrics` | Platform-tuned `circleDiameter` / `symbolSize` (28/13 macOS, 40/17 iOS) |\n\nProtocols you implement: `VoiceInputProvider`, `VoiceAmplitudeSource`, `SlashCommandProvider`, `ToolbarAttachment`, `InlineAttachmentRenderer`.\n\n## Footer controls\n\nUse `.footer { }` for persistent composer controls that belong below the editor, such as attachment menus, workspace selectors, model selectors, and send / stop buttons:\n\n```swift\nToolbarContainer {\n    ToolbarEditor(\n        text: $text,\n        contentHeight: $height,\n        isFocused: $isFocused,\n        placeholder: \"Ask anything...\"\n    )\n    .frame(minHeight: 96, maxHeight: 180, alignment: .topLeading)\n}\n.footer {\n    HStack(spacing: 8) {\n        ToolbarMenuButton {\n            Button(\"File\", systemImage: \"doc\") { pickFile() }\n            Button(\"Image\", systemImage: \"photo\") { pickImage() }\n        } label: {\n            Image(systemName: \"plus\")\n        }\n\n        WorkspacePickerChip(selection: $workspace)\n\n        Spacer()\n\n        SendButton(isEnabled: !text.isEmpty, action: send)\n    }\n}\n```\n\n## Design philosophy\n\n- **Declarative composition** — no environment-modifier soup. The composer body is just SwiftUI views in an `HStack` / `VStack`.\n- **One slab, one morph domain** — children that need glass (popup, capsule chips, circle buttons) share the container's `GlassEffectContainer` so shape transitions stay fluid.\n- **Provider-driven I/O** — voice, slash commands, and inline attachment rendering are protocols. The library never talks to Whisper, Speech, OCR, or any concrete backend.\n- **Cross-platform internals stay private** — `ToolbarEditor` is the only place AppKit / UIKit leaks, and that leak is sealed behind the public SwiftUI surface.\n\nSee [`DESIGN.md`](DESIGN.md) for the full architecture write-up.\n\n## License\n\nMIT — see [`LICENSE`](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1amageek%2Ftoolbar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F1amageek%2Ftoolbar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1amageek%2Ftoolbar/lists"}