mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
feat(ui-design): add comprehensive UI/UX design plugin v1.0.0
New plugin covering mobile (iOS, Android, React Native) and web applications with modern design patterns, accessibility, and design systems. Components: - 9 skills: design-system-patterns, accessibility-compliance, responsive-design, mobile-ios-design, mobile-android-design, react-native-design, web-component-design, interaction-design, visual-design-foundations - 4 commands: design-review, create-component, accessibility-audit, design-system-setup - 3 agents: ui-designer, accessibility-expert, design-system-architect Marketplace updated: - Version bumped to 1.3.4 - 102 agents (+3), 116 skills (+9)
This commit is contained in:
@@ -0,0 +1,557 @@
|
||||
# SwiftUI Component Library
|
||||
|
||||
## Lists and Collections
|
||||
|
||||
### Basic List
|
||||
```swift
|
||||
struct ItemListView: View {
|
||||
@State private var items: [Item] = []
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
.onMove(perform: moveItems)
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.refreshable {
|
||||
await loadItems()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(at offsets: IndexSet) {
|
||||
items.remove(atOffsets: offsets)
|
||||
}
|
||||
|
||||
private func moveItems(from source: IndexSet, to destination: Int) {
|
||||
items.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sectioned List
|
||||
```swift
|
||||
struct SectionedListView: View {
|
||||
let groupedItems: [String: [Item]]
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(groupedItems.keys.sorted(), id: \.self) { key in
|
||||
Section(header: Text(key)) {
|
||||
ForEach(groupedItems[key] ?? []) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Search Integration
|
||||
```swift
|
||||
struct SearchableListView: View {
|
||||
@State private var searchText = ""
|
||||
@State private var items: [Item] = []
|
||||
|
||||
var filteredItems: [Item] {
|
||||
if searchText.isEmpty {
|
||||
return items
|
||||
}
|
||||
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(filteredItems) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Search items")
|
||||
.searchSuggestions {
|
||||
ForEach(searchSuggestions, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Items")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Forms and Input
|
||||
|
||||
### Settings Form
|
||||
```swift
|
||||
struct SettingsView: View {
|
||||
@AppStorage("notifications") private var notificationsEnabled = true
|
||||
@AppStorage("soundEnabled") private var soundEnabled = true
|
||||
@State private var selectedTheme = Theme.system
|
||||
@State private var username = ""
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Account") {
|
||||
TextField("Username", text: $username)
|
||||
.textContentType(.username)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
Section("Preferences") {
|
||||
Toggle("Enable Notifications", isOn: $notificationsEnabled)
|
||||
Toggle("Sound Effects", isOn: $soundEnabled)
|
||||
|
||||
Picker("Theme", selection: $selectedTheme) {
|
||||
ForEach(Theme.allCases) { theme in
|
||||
Text(theme.rawValue).tag(theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("About") {
|
||||
LabeledContent("Version", value: "1.0.0")
|
||||
|
||||
Link(destination: URL(string: "https://example.com/privacy")!) {
|
||||
Text("Privacy Policy")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Input Fields
|
||||
```swift
|
||||
struct ValidatedTextField: View {
|
||||
let title: String
|
||||
@Binding var text: String
|
||||
let validation: (String) -> Bool
|
||||
|
||||
@State private var isValid = true
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextField(title, text: $text)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused($isFocused)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(borderColor, lineWidth: 1)
|
||||
)
|
||||
.onChange(of: text) { _, newValue in
|
||||
isValid = validation(newValue)
|
||||
}
|
||||
|
||||
if !isValid && !text.isEmpty {
|
||||
Text("Invalid input")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isFocused {
|
||||
return isValid ? .blue : .red
|
||||
}
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Buttons and Actions
|
||||
|
||||
### Button Styles
|
||||
```swift
|
||||
// Primary filled button
|
||||
Button("Continue") {
|
||||
// action
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
|
||||
// Secondary button
|
||||
Button("Cancel") {
|
||||
// action
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
// Destructive button
|
||||
Button("Delete", role: .destructive) {
|
||||
// action
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
// Custom button style
|
||||
struct ScaleButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Menu and Context Menu
|
||||
```swift
|
||||
// Menu button
|
||||
Menu {
|
||||
Button("Edit", systemImage: "pencil") { }
|
||||
Button("Duplicate", systemImage: "doc.on.doc") { }
|
||||
Divider()
|
||||
Button("Delete", systemImage: "trash", role: .destructive) { }
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
|
||||
// Context menu on any view
|
||||
Text("Long press me")
|
||||
.contextMenu {
|
||||
Button("Copy", systemImage: "doc.on.doc") { }
|
||||
Button("Share", systemImage: "square.and.arrow.up") { }
|
||||
} preview: {
|
||||
ItemPreviewView()
|
||||
}
|
||||
```
|
||||
|
||||
## Sheets and Modals
|
||||
|
||||
### Sheet Presentation
|
||||
```swift
|
||||
struct ParentView: View {
|
||||
@State private var showSettings = false
|
||||
@State private var selectedItem: Item?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Button("Settings") {
|
||||
showSettings = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsSheet()
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(item: $selectedItem) { item in
|
||||
ItemDetailSheet(item: item)
|
||||
.presentationDetents([.height(300), .large])
|
||||
.presentationCornerRadius(24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
SettingsContent()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Confirmation Dialog
|
||||
```swift
|
||||
struct DeleteConfirmationView: View {
|
||||
@State private var showConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Button("Delete Account", role: .destructive) {
|
||||
showConfirmation = true
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete Account",
|
||||
isPresented: $showConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete", role: .destructive) {
|
||||
deleteAccount()
|
||||
}
|
||||
Button("Cancel", role: .cancel) { }
|
||||
} message: {
|
||||
Text("This action cannot be undone.")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loading and Progress
|
||||
|
||||
### Progress Indicators
|
||||
```swift
|
||||
// Indeterminate spinner
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
|
||||
// Determinate progress
|
||||
ProgressView(value: downloadProgress, total: 1.0) {
|
||||
Text("Downloading...")
|
||||
} currentValueLabel: {
|
||||
Text("\(Int(downloadProgress * 100))%")
|
||||
}
|
||||
|
||||
// Custom loading view
|
||||
struct LoadingOverlay: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(0.4)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.scaleEffect(1.5)
|
||||
.tint(.white)
|
||||
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(24)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Skeleton Loading
|
||||
```swift
|
||||
struct SkeletonRow: View {
|
||||
@State private var isAnimating = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(.gray.opacity(0.3))
|
||||
.frame(height: 14)
|
||||
.frame(maxWidth: 200)
|
||||
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(.gray.opacity(0.2))
|
||||
.frame(height: 12)
|
||||
.frame(maxWidth: 150)
|
||||
}
|
||||
}
|
||||
.opacity(isAnimating ? 0.5 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.8).repeatForever(), value: isAnimating)
|
||||
.onAppear { isAnimating = true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Content Loading
|
||||
|
||||
### AsyncImage
|
||||
```swift
|
||||
AsyncImage(url: imageURL) { phase in
|
||||
switch phase {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
case .failure:
|
||||
Image(systemName: "photo")
|
||||
.foregroundStyle(.secondary)
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
```
|
||||
|
||||
### Task-Based Loading
|
||||
```swift
|
||||
struct AsyncContentView: View {
|
||||
@State private var items: [Item] = []
|
||||
@State private var isLoading = true
|
||||
@State private var error: Error?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView("Loading...")
|
||||
} else if let error {
|
||||
ContentUnavailableView(
|
||||
"Failed to Load",
|
||||
systemImage: "exclamationmark.triangle",
|
||||
description: Text(error.localizedDescription)
|
||||
)
|
||||
} else if items.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Items",
|
||||
systemImage: "tray",
|
||||
description: Text("Add your first item to get started.")
|
||||
)
|
||||
} else {
|
||||
List(items) { item in
|
||||
ItemRow(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadItems()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadItems() async {
|
||||
do {
|
||||
items = try await api.fetchItems()
|
||||
isLoading = false
|
||||
} catch {
|
||||
self.error = error
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Animations
|
||||
|
||||
### Implicit Animations
|
||||
```swift
|
||||
struct AnimatedCard: View {
|
||||
@State private var isExpanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Tap to expand")
|
||||
|
||||
if isExpanded {
|
||||
Text("Additional content here")
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.blue.opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
|
||||
.onTapGesture {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Transitions
|
||||
```swift
|
||||
extension AnyTransition {
|
||||
static var slideAndFade: AnyTransition {
|
||||
.asymmetric(
|
||||
insertion: .move(edge: .trailing).combined(with: .opacity),
|
||||
removal: .move(edge: .leading).combined(with: .opacity)
|
||||
)
|
||||
}
|
||||
|
||||
static var scaleAndFade: AnyTransition {
|
||||
.scale(scale: 0.8).combined(with: .opacity)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase Animator (iOS 17+)
|
||||
```swift
|
||||
struct PulsingButton: View {
|
||||
var body: some View {
|
||||
Button("Tap Me") { }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.phaseAnimator([false, true]) { content, phase in
|
||||
content
|
||||
.scaleEffect(phase ? 1.05 : 1.0)
|
||||
} animation: { _ in
|
||||
.easeInOut(duration: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gestures
|
||||
|
||||
### Drag Gesture
|
||||
```swift
|
||||
struct DraggableCard: View {
|
||||
@State private var offset = CGSize.zero
|
||||
@State private var isDragging = false
|
||||
|
||||
var body: some View {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.blue)
|
||||
.frame(width: 200, height: 150)
|
||||
.offset(offset)
|
||||
.scaleEffect(isDragging ? 1.05 : 1.0)
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged { value in
|
||||
offset = value.translation
|
||||
isDragging = true
|
||||
}
|
||||
.onEnded { _ in
|
||||
withAnimation(.spring()) {
|
||||
offset = .zero
|
||||
isDragging = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Simultaneous Gestures
|
||||
```swift
|
||||
struct ZoomableImage: View {
|
||||
@State private var scale: CGFloat = 1.0
|
||||
@State private var lastScale: CGFloat = 1.0
|
||||
|
||||
var body: some View {
|
||||
Image("photo")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.scaleEffect(scale)
|
||||
.gesture(
|
||||
MagnificationGesture()
|
||||
.onChanged { value in
|
||||
scale = lastScale * value
|
||||
}
|
||||
.onEnded { _ in
|
||||
lastScale = scale
|
||||
}
|
||||
)
|
||||
.gesture(
|
||||
TapGesture(count: 2)
|
||||
.onEnded {
|
||||
withAnimation {
|
||||
scale = 1.0
|
||||
lastScale = 1.0
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user