Internal tooling for Mac utility for storage management.
Swift
98.7%
JSON
1.1%
Markdown
0.2%
//
// BubbleView.swift
// MUA
//
// Created by Mitchel Volkering on 21/12/2025.
//
import SwiftUI
/// A single bubble representing a file or folder
struct BubbleView: View {
let item: FileItem
let size: CGFloat
let isSelected: Bool
let onTap: () -> Void
let onDoubleTap: () -> Void
@State private var isHovered = false
@State private var isPressed = false
private var bubbleScale: CGFloat {
if isPressed { return 0.95 }
if isHovered { return 1.03 }
return 1.0
}
var body: some View {
ZStack {
// Glass bubble background
Circle()
.fill(
.linearGradient(
colors: [
item.color.opacity(0.7),
item.color.opacity(0.4)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
// Glass highlight
Circle()
.fill(
.linearGradient(
colors: [
.white.opacity(0.4),
.white.opacity(0.1),
.clear
],
startPoint: .topLeading,
endPoint: .center
)
)
)
.overlay(
// Glass edge
Circle()
.strokeBorder(
.linearGradient(
colors: [
.white.opacity(0.6),
.white.opacity(0.2),
item.color.opacity(0.3)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: isSelected ? 3 : 1.5
)
)
.shadow(color: item.color.opacity(0.3), radius: isHovered ? 15 : 8, y: 4)
.shadow(color: .black.opacity(0.1), radius: 2, y: 1)
// Content
VStack(spacing: size > 80 ? 4 : 2) {
if size > 50 {
Image(systemName: item.icon)
.font(.system(size: min(size * 0.25, 28)))
.foregroundStyle(.white)
.shadow(color: .black.opacity(0.2), radius: 1)
}
if size > 70 {
Text(item.name)
.font(.system(size: min(size * 0.1, 12), weight: .medium))
.foregroundStyle(.white)
.lineLimit(1)
.truncationMode(.middle)
.frame(maxWidth: size * 0.8)
.shadow(color: .black.opacity(0.3), radius: 1)
}
if size > 90 {
Text(item.formattedSize)
.font(.system(size: min(size * 0.08, 10), weight: .regular))
.foregroundStyle(.white.opacity(0.9))
.shadow(color: .black.opacity(0.2), radius: 1)
}
}
}
.frame(width: size, height: size)
.scaleEffect(bubbleScale)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isHovered)
.animation(.spring(response: 0.2, dampingFraction: 0.8), value: isPressed)
.onHover { hovering in
isHovered = hovering
}
.onTapGesture(count: 2) {
onDoubleTap()
}
.onTapGesture(count: 1) {
withAnimation(.spring(response: 0.15)) {
isPressed = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isPressed = false
}
onTap()
}
.accessibilityElement()
.accessibilityLabel("\(item.name), \(item.formattedSize)")
.accessibilityHint(item.isDirectory ? "Double-tap to open folder" : "File")
}
}
#Preview {
HStack(spacing: 20) {
BubbleView(
item: FileItem(url: URL(fileURLWithPath: "/Applications"), isDirectory: true, size: 5_000_000_000),
size: 120,
isSelected: false,
onTap: {},
onDoubleTap: {}
)
BubbleView(
item: FileItem(url: URL(fileURLWithPath: "/test.mp4"), isDirectory: false, size: 2_500_000_000),
size: 100,
isSelected: true,
onTap: {},
onDoubleTap: {}
)
BubbleView(
item: FileItem(url: URL(fileURLWithPath: "/photo.jpg"), isDirectory: false, size: 500_000_000),
size: 70,
isSelected: false,
onTap: {},
onDoubleTap: {}
)
}
.padding(40)
.background(.black.opacity(0.9))
}
About
Internal tooling for Mac utility for storage management.
0 stars
0 forks