An open API service indexing awesome lists of open source software.

https://github.com/1amageek/toolbar

Awesome autolayout Toolbar. Toolbar is a library for iOS. You can easily create chat InputBar.
https://github.com/1amageek/toolbar

autolayout inputbar ios swift toolbar

Last synced: 21 days ago
JSON representation

Awesome autolayout Toolbar. Toolbar is a library for iOS. You can easily create chat InputBar.

Awesome Lists containing this project

README

          

# Toolbar

A SwiftUI Liquid Glass composer kit for AI chat interfaces.

`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`.

## Requirements

| | |
|---|---|
| Swift | 6.2+ |
| Platforms | iOS 26+ / iPadOS 26+ / macOS 26+ |
| Xcode | 26+ |

## Installation

Swift Package Manager:

```swift
.package(url: "https://github.com/1amageek/Toolbar.git", branch: "main")
```

Then add `Toolbar` to your target dependencies.

## Quick start

A minimal composer with a menu, editor, and adaptive trailing button (Send / Stop / Voice):

```swift
import SwiftUI
import Toolbar

struct ChatView: View {
@State private var text = ""
@State private var height: CGFloat = ToolbarControlMetrics.circleDiameter
@State private var isFocused = false
@State private var isStreaming = false

var body: some View {
ScrollView {
messageList
}
.safeAreaInset(edge: .bottom) {
ToolbarContainer {
HStack(alignment: .bottom, spacing: 8) {
ToolbarMenuButton {
Button("File", systemImage: "doc") { pickFile() }
Button("Image", systemImage: "photo") { pickImage() }
Button("Folder", systemImage: "folder"){ pickFolder() }
} label: {
Image(systemName: "plus")
}

ToolbarEditor(
text: $text,
contentHeight: $height,
isFocused: $isFocused,
placeholder: "Message..."
)
.frame(
minHeight: ToolbarControlMetrics.circleDiameter,
maxHeight: max(ToolbarControlMetrics.circleDiameter, min(height, 220))
)

if isStreaming {
StopButton(action: cancel)
} else {
SendButton(isEnabled: !text.isEmpty, action: send)
}
}
}
}
}
}
```

`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`.

Use `ToolbarContainer(contentInsets:)` for padding inside the glass slab. Use regular `.padding(...)` outside the container for spacing around the whole toolbar.

## Voice + accessory area

`.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.

```swift
@State private var voiceState: VoiceState = .idle
@State private var amplitudes: [Float] = []
@State private var amplitudeSource: (any VoiceAmplitudeSource)? = nil

ToolbarContainer {
HStack(alignment: .bottom, spacing: 8) {
// ... menu, editor ...

if text.isEmpty {
VoiceButton(
provider: voiceProvider,
onResult: { result in /* finalized text/audio */ },
onStateChange: { state in
withAnimation(.smooth(duration: 0.25)) { voiceState = state }
amplitudeSource = (state == .recording)
? voiceProvider as? any VoiceAmplitudeSource
: nil
}
)
} else {
SendButton(isEnabled: true, action: send)
}
}
}
.accessory {
switch voiceState {
case .idle:
EmptyView()
case .recording:
VoiceWaveform(amplitudes: amplitudes)
.transition(.opacity.combined(with: .scale(scale: 0.96)))
case .transcribing:
TranscribingIndicator()
.transition(.opacity.combined(with: .scale(scale: 0.96)))
}
}
.voiceAmplitudes(from: amplitudeSource, into: $amplitudes)
```

The `.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.

## Slash commands

Slash popup goes in `.popup(isPresented:)`, outside the toolbar slab. Present it only while the editor text is in slash-command mode:

```swift
ToolbarContainer {
HStack(alignment: .bottom, spacing: 8) {
// ... editor with $text driving `matches` via SlashCommandProvider ...
}
}
.popup(isPresented: text.hasPrefix("/")) {
SlashCommandPopup(
commands: matches,
selectedIndex: selectedIndex,
onSelect: commit
)
}
```

Implement `SlashCommandProvider` against your own command source, or use the bundled `StaticSlashCommandProvider` for fixed lists.

## Attachments

Three 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:

```swift
ToolbarContainer {
HStack(alignment: .bottom, spacing: 8) {
// ... menu, editor, trailing action ...
}
}
.accessory {
if !attachments.isEmpty {
AttachmentStrip(attachments: attachments) { attachment in
remove(attachment)
}
}
}
```

For inline `[[marker]]` attachments rendered directly inside the editor text, conform to `InlineAttachmentRenderer`.

## Public surface

| Component | Role |
|---|---|
| `ToolbarContainer` | Liquid Glass slab + `GlassEffectContainer` morph domain |
| `.popup { }` | Modifier inserting a floating surface above the slab |
| `.accessory { }` | Modifier inserting an ephemeral strip above the content |
| `.footer { }` | Modifier inserting persistent controls below the content |
| `ToolbarEditor` | Cross-platform multi-line editor (`NSTextView` / `UITextView` backed) |
| `ToolbarMenuButton` | Glass-circle menu styled to match other buttons |
| `SendButton` / `StopButton` | Trailing action buttons with shared metrics |
| `VoiceButton` | Mic ↔ stop, drives a `VoiceInputProvider` and emits `VoiceState` |
| `VoiceWaveform` | Bar-graph visualization fed by `VoiceAmplitudeSource` |
| `TranscribingIndicator` | Progress strip for the post-recording analysis state |
| `AttachmentChip` | Fixed-height preview for any `ToolbarAttachment` |
| `AttachmentStrip` | Horizontal attachment row with scroll clipping disabled |
| `SlashCommandPopup` | Glass popup list of matches |
| `GlassCircleButtonStyle` | Re-usable circular Liquid Glass button style |
| `ToolbarControlMetrics` | Platform-tuned `circleDiameter` / `symbolSize` (28/13 macOS, 40/17 iOS) |

Protocols you implement: `VoiceInputProvider`, `VoiceAmplitudeSource`, `SlashCommandProvider`, `ToolbarAttachment`, `InlineAttachmentRenderer`.

## Footer controls

Use `.footer { }` for persistent composer controls that belong below the editor, such as attachment menus, workspace selectors, model selectors, and send / stop buttons:

```swift
ToolbarContainer {
ToolbarEditor(
text: $text,
contentHeight: $height,
isFocused: $isFocused,
placeholder: "Ask anything..."
)
.frame(minHeight: 96, maxHeight: 180, alignment: .topLeading)
}
.footer {
HStack(spacing: 8) {
ToolbarMenuButton {
Button("File", systemImage: "doc") { pickFile() }
Button("Image", systemImage: "photo") { pickImage() }
} label: {
Image(systemName: "plus")
}

WorkspacePickerChip(selection: $workspace)

Spacer()

SendButton(isEnabled: !text.isEmpty, action: send)
}
}
```

## Design philosophy

- **Declarative composition** — no environment-modifier soup. The composer body is just SwiftUI views in an `HStack` / `VStack`.
- **One slab, one morph domain** — children that need glass (popup, capsule chips, circle buttons) share the container's `GlassEffectContainer` so shape transitions stay fluid.
- **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.
- **Cross-platform internals stay private** — `ToolbarEditor` is the only place AppKit / UIKit leaks, and that leak is sealed behind the public SwiftUI surface.

See [`DESIGN.md`](DESIGN.md) for the full architecture write-up.

## License

MIT — see [`LICENSE`](LICENSE).