feat(widgets): 홈 위젯 풀블리드 적용 및 다짐 현황 간격 개선

- 홈 위젯 5종에 contentMarginsDisabled() 적용: 시스템 기본 베젤 제거,
  여백은 각 뷰가 직접 관리 (다중 셀 10pt, 콘텐츠형 13pt)
- 소형(단일 셀) 위젯은 셀이 위젯 전체를 여백 없이 채움 — 셀 배경/테두리가
  ContainerRelativeShape로 위젯 모서리에 정확히 맞음 (A·B·C 위젯)
- 다짐 현황(D) 위젯 간격 개선: 링 그리드 행 간격 6→12, 열 간격 4→8,
  섹션 간격 6→10, 대형 목표 2개 간격 10→16, 링-이름 간격 확대
- 미리보기 하니스도 여백 제거를 반영해 실제 렌더링과 일치

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-10 15:42:37 +09:00
parent c0c59af0ac
commit c57c39002b
6 changed files with 81 additions and 39 deletions

View File

@ -199,8 +199,8 @@ struct WidgetPreviewScreen: View {
}
// containerBackground
// /
// contentMarginsDisabled
return content()
.padding(12)
.frame(width: size.width, height: size.height)
.background {
if theme == .glass {

View File

@ -88,6 +88,8 @@ struct ActionRunCellView: View {
let cell: ActionCellSnapshot
let periodLabel: String
var compact = false
/// ()
var fullBleed = false
var body: some View {
Button(intent: RunActionIntent(action: cell.entity)) {
@ -121,13 +123,18 @@ struct ActionRunCellView: View {
.lineLimit(1)
}
.foregroundStyle(.white)
.padding(compact ? 8 : 12)
.padding(fullBleed ? 14 : (compact ? 8 : 12))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.background(WidgetTintedCellBackground(color: cell.color))
.background(WidgetTintedCellBackground(color: cell.color, fullBleed: fullBleed))
.overlay {
if cell.isRunning {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(AppTheme.yellow, lineWidth: 2)
if fullBleed {
ContainerRelativeShape()
.strokeBorder(AppTheme.yellow, lineWidth: 2)
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(AppTheme.yellow, lineWidth: 2)
}
}
}
}
@ -146,11 +153,13 @@ struct ActionRunWidgetView: View {
Group {
if entry.locked {
WidgetLockedView()
.padding(12)
} else if entry.cells.isEmpty {
Text("앱에서 행동을 만들면 여기서 실행할 수 있어요.")
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(12)
} else {
switch family {
case .systemMedium:
@ -159,6 +168,7 @@ struct ActionRunWidgetView: View {
ActionRunCellView(cell: cell, periodLabel: entry.periodLabel, compact: true)
}
}
.padding(10)
case .systemLarge:
let cells = Array(entry.cells.prefix(4))
VStack(spacing: 8) {
@ -175,9 +185,11 @@ struct ActionRunWidgetView: View {
}
}
}
.padding(10)
default:
// : ()
if let cell = entry.cells.first {
ActionRunCellView(cell: cell, periodLabel: entry.periodLabel)
ActionRunCellView(cell: cell, periodLabel: entry.periodLabel, fullBleed: true)
}
}
}
@ -200,5 +212,6 @@ struct ActionRunWidget: Widget {
.configurationDisplayName("행동 실행")
.description("행동을 위젯에서 바로 실행하고 누적값을 확인해요. (프리미엄)")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -68,6 +68,8 @@ struct GoalBarsProvider: AppIntentTimelineProvider {
struct GoalBarsCellView: View {
let goal: GoalSnapshot
/// ()
var fullBleed = false
var body: some View {
VStack(alignment: .leading, spacing: 5) {
@ -83,9 +85,9 @@ struct GoalBarsCellView: View {
SpanBarRow(label: "주간", ratio: goal.weekRatio, color: goal.color)
SpanBarRow(label: "월간", ratio: goal.monthRatio, color: goal.color)
}
.padding(10)
.padding(fullBleed ? 14 : 10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(WidgetCellBackground())
.background(WidgetCellBackground(fullBleed: fullBleed))
}
}
@ -108,11 +110,13 @@ struct GoalBarsWidgetView: View {
Group {
if entry.locked {
WidgetLockedView()
.padding(12)
} else if entry.goals.isEmpty {
Text("앱의 목표 탭에서 목표를 만들면 진행률이 표시돼요.")
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(12)
} else {
let goals = Array(entry.goals.prefix(visibleCount))
switch family {
@ -120,6 +124,7 @@ struct GoalBarsWidgetView: View {
HStack(spacing: 8) {
ForEach(goals) { GoalBarsCellView(goal: $0) }
}
.padding(10)
case .systemLarge:
VStack(spacing: 8) {
HStack(spacing: 8) {
@ -131,9 +136,11 @@ struct GoalBarsWidgetView: View {
}
}
}
.padding(10)
default:
// : ()
if let goal = goals.first {
GoalBarsCellView(goal: goal)
GoalBarsCellView(goal: goal, fullBleed: true)
}
}
}
@ -154,6 +161,7 @@ struct GoalBarsWidget: Widget {
.configurationDisplayName("목표 진행률")
.description("목표의 하루·주간·월간 진행률을 진행바로 확인해요. (프리미엄)")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}
@ -224,7 +232,7 @@ struct QuestRingCellView: View {
var ringSize: CGFloat = 34
var body: some View {
VStack(spacing: 3) {
VStack(spacing: 5) {
QuestRingView(snapshot: quest, lineWidth: 4, iconSize: ringSize * 0.36)
.frame(width: ringSize, height: ringSize)
Text(quest.targetName)
@ -261,7 +269,7 @@ struct GoalQuestGridWidgetView: View {
case .systemLarge:
// D: 2 2, 1 8
if entry.goals.count > 1 {
VStack(spacing: 10) {
VStack(spacing: 16) {
ForEach(entry.goals.prefix(2)) { goal in
goalSection(goal, maxCount: 4, columns: 4, ringSize: 40)
}
@ -277,11 +285,12 @@ struct GoalQuestGridWidgetView: View {
}
}
}
.padding(13)
.widgetTheme(entry.theme)
}
private func goalSection(_ goal: GoalSnapshot, maxCount: Int, columns: Int, ringSize: CGFloat) -> some View {
VStack(spacing: 6) {
VStack(spacing: 10) {
HStack(spacing: 4) {
Image(systemName: goal.symbolName)
.font(.system(size: 10, weight: .semibold))
@ -302,8 +311,8 @@ struct GoalQuestGridWidgetView: View {
Spacer()
} else {
let quests = Array(goal.quests.prefix(maxCount))
let grid = [GridItem](repeating: GridItem(.flexible(), spacing: 4), count: columns)
LazyVGrid(columns: grid, spacing: 6) {
let grid = [GridItem](repeating: GridItem(.flexible(), spacing: 8), count: columns)
LazyVGrid(columns: grid, spacing: 12) {
ForEach(quests) { quest in
QuestRingCellView(quest: quest, ringSize: ringSize)
}
@ -326,5 +335,6 @@ struct GoalQuestGridWidget: Widget {
.configurationDisplayName("다짐 현황")
.description("목표에 속한 다짐들의 진행률을 원형 링으로 한눈에 봐요. (프리미엄)")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -100,6 +100,9 @@ struct QuestRingActionCellView: View {
}
}
/// ()
var fullBleed = false
private var content: some View {
VStack(spacing: 4) {
ZStack {
@ -120,8 +123,8 @@ struct QuestRingActionCellView: View {
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(6)
.background(WidgetCellBackground())
.padding(fullBleed ? 10 : 6)
.background(WidgetCellBackground(fullBleed: fullBleed))
}
private var percentText: String {
@ -153,11 +156,13 @@ struct QuestRingWidgetView: View {
Group {
if entry.locked {
WidgetLockedView()
.padding(12)
} else if entry.cells.isEmpty {
Text("앱의 목표 탭에서 다짐을 만들면 진행률이 표시돼요.")
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(12)
} else {
let cells = Array(entry.cells.prefix(visibleCount))
switch family {
@ -165,6 +170,7 @@ struct QuestRingWidgetView: View {
HStack(spacing: 8) {
ForEach(cells) { QuestRingActionCellView(cell: $0, spanLabel: entry.spanLabel) }
}
.padding(10)
case .systemLarge:
VStack(spacing: 8) {
HStack(spacing: 8) {
@ -180,9 +186,11 @@ struct QuestRingWidgetView: View {
}
}
}
.padding(10)
default:
// : ()
if let cell = cells.first {
QuestRingActionCellView(cell: cell, spanLabel: entry.spanLabel, ringSize: 58)
QuestRingActionCellView(cell: cell, spanLabel: entry.spanLabel, ringSize: 58, fullBleed: true)
}
}
}
@ -205,5 +213,6 @@ struct QuestRingWidget: Widget {
.configurationDisplayName("다짐 진행률")
.description("다짐의 진행률을 원형 링으로 보고, 눌러서 바로 실행해요. (프리미엄)")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -190,6 +190,7 @@ struct StatsChartWidgetView: View {
}
}
}
.padding(13)
.widgetTheme(entry.theme)
}
@ -249,5 +250,6 @@ struct StatsChartWidget: Widget {
.configurationDisplayName("행동 통계")
.description("선택한 행동들의 통계 꺾은선 그래프를 확인해요. (프리미엄)")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
}
}

View File

@ -105,20 +105,26 @@ extension View {
}
/// : , +
/// fullBleed = (ContainerRelativeShape)
struct WidgetCellBackground: View {
@Environment(\.widgetGlass) private var glass
var fullBleed = false
var body: some View {
if glass {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(.thinMaterial)
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(.white.opacity(0.25), lineWidth: 1)
}
if fullBleed {
styled(ContainerRelativeShape())
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(AppTheme.surface)
styled(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
@ViewBuilder
private func styled(_ shape: some InsettableShape) -> some View {
if glass {
shape.fill(.thinMaterial)
.overlay { shape.strokeBorder(.white.opacity(0.25), lineWidth: 1) }
} else {
shape.fill(AppTheme.surface)
}
}
}
@ -128,22 +134,24 @@ struct WidgetCellBackground: View {
struct WidgetTintedCellBackground: View {
@Environment(\.widgetGlass) private var glass
let color: Color
var fullBleed = false
var body: some View {
if glass {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(.thinMaterial)
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(color.opacity(0.5))
}
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(.white.opacity(0.3), lineWidth: 1)
}
if fullBleed {
styled(ContainerRelativeShape())
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(color)
styled(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
@ViewBuilder
private func styled(_ shape: some InsettableShape) -> some View {
if glass {
shape.fill(.thinMaterial)
.overlay { shape.fill(color.opacity(0.5)) }
.overlay { shape.strokeBorder(.white.opacity(0.3), lineWidth: 1) }
} else {
shape.fill(color)
}
}
}