feat(ui): UI 骨架基线 — 3 Tab + RecordSheet + Quick/Archive 流程占位

替换 Xcode 默认模板:
- 删除 ContentView/Item/__App
- 新增 App/TijiApp(SwiftData ModelContainer)、RootView(3 Tab + RecordSheet)
- DesignSystem:Tokens(色板/字体/圆角)+ Components(卡片/按钮/Chip)
- Models:Indicator / Report / DiaryEntry @Model 初版
- Features:Home / Quick(A1-A3)/ Archive(B1-B5)/ Record / Trends / Me 静态 UI

W2 AI 基座工作将在此基线上叠加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
link2026
2026-05-25 14:49:21 +08:00
parent 96cae73d8f
commit c050865db5
23 changed files with 2586 additions and 97 deletions

View File

@@ -0,0 +1,146 @@
import SwiftUI
struct TjLockChip: View {
var body: some View {
HStack(spacing: 4) {
Image(systemName: "lock.fill")
.font(.system(size: 9, weight: .semibold))
Text("本地加密")
.font(.system(size: 10))
.tracking(0.5)
}
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(Capsule().fill(Tj.Palette.ink))
}
}
enum TjBadgeStyle {
case brick, amber, leaf, ink, neutral
var bg: Color {
switch self {
case .brick: return Tj.Palette.brickSoft
case .amber: return Color(red: 0.957, green: 0.890, blue: 0.749)
case .leaf: return Tj.Palette.leafSoft
case .ink: return Tj.Palette.ink
case .neutral: return Tj.Palette.sand2
}
}
var fg: Color {
switch self {
case .brick: return Tj.Palette.brick
case .amber: return Tj.Palette.amber
case .leaf: return Tj.Palette.leaf
case .ink: return Tj.Palette.paper
case .neutral: return Tj.Palette.text2
}
}
}
struct TjBadge: View {
let text: String
var style: TjBadgeStyle = .neutral
var body: some View {
Text(text)
.font(.system(size: 10, weight: .semibold))
.tracking(0.3)
.foregroundStyle(style.fg)
.padding(.horizontal, 7)
.padding(.vertical, 2)
.background(Capsule().fill(style.bg))
.lineLimit(1)
}
}
struct TjPlaceholder: View {
let label: String
var dark: Bool = false
var radius: CGFloat = Tj.Radius.sm
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: radius, style: .continuous)
.fill(dark ? Color(red: 0.110, green: 0.122, blue: 0.110) : Tj.Palette.sand2)
DiagonalStripes(spacing: 7, color: dark ? Color.white.opacity(0.04) : Color.black.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
Text(label)
.font(.system(size: 11, design: .monospaced))
.tracking(0.5)
.foregroundStyle(dark ? Color.white.opacity(0.5) : Tj.Palette.text3)
.multilineTextAlignment(.center)
.padding(8)
}
}
}
private struct DiagonalStripes: View {
let spacing: CGFloat
let color: Color
var body: some View {
Canvas { ctx, size in
let step = spacing
let count = Int((size.width + size.height) / step) + 4
for i in -2..<count {
let x = CGFloat(i) * step
var path = Path()
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x + size.height, y: size.height))
ctx.stroke(path, with: .color(color), lineWidth: 1)
}
}
}
}
struct TjPrimaryButton: ButtonStyle {
var height: CGFloat = 48
var fontSize: CGFloat = 15
var horizontalPadding: CGFloat = 22
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: fontSize, weight: .semibold))
.tracking(1)
.foregroundStyle(Tj.Palette.paper)
.padding(.horizontal, horizontalPadding)
.frame(height: height)
.background(Capsule().fill(Tj.Palette.ink))
.opacity(configuration.isPressed ? 0.85 : 1)
}
}
struct TjGhostButton: ButtonStyle {
var height: CGFloat = 48
var fontSize: CGFloat = 15
var horizontalPadding: CGFloat = 22
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: fontSize, weight: .semibold))
.tracking(1)
.foregroundStyle(Tj.Palette.ink)
.padding(.horizontal, horizontalPadding)
.frame(height: height)
.background(
Capsule().strokeBorder(Tj.Palette.ink, lineWidth: 1)
)
.opacity(configuration.isPressed ? 0.7 : 1)
}
}
struct TjDashedDivider: View {
var body: some View {
Rectangle()
.fill(Tj.Palette.line)
.frame(height: 1)
.mask(
HStack(spacing: 4) {
ForEach(0..<200, id: \.self) { _ in
Rectangle().frame(width: 4, height: 1)
}
}
)
}
}

View File

@@ -0,0 +1,62 @@
import SwiftUI
enum Tj {
enum Palette {
static let ink = Color(red: 0.165, green: 0.153, blue: 0.137)
static let ink2 = Color(red: 0.286, green: 0.275, blue: 0.251)
static let inkSoft = Color(red: 0.459, green: 0.447, blue: 0.424)
static let sand = Color(red: 0.976, green: 0.969, blue: 0.949)
static let sand2 = Color(red: 0.929, green: 0.918, blue: 0.886)
static let sand3 = Color(red: 0.878, green: 0.859, blue: 0.816)
static let paper = Color(red: 0.992, green: 0.988, blue: 0.973)
static let line = Color(red: 0.875, green: 0.863, blue: 0.831)
static let lineSoft = Color(red: 0.925, green: 0.918, blue: 0.890)
static let text = Color(red: 0.149, green: 0.137, blue: 0.118)
static let text2 = Color(red: 0.420, green: 0.408, blue: 0.384)
static let text3 = Color(red: 0.616, green: 0.604, blue: 0.580)
static let brick = Color(red: 0.886, green: 0.388, blue: 0.314)
static let brickSoft = Color(red: 0.976, green: 0.863, blue: 0.824)
static let amber = Color(red: 0.871, green: 0.627, blue: 0.314)
static let leaf = Color(red: 0.180, green: 0.357, blue: 0.518)
static let leafSoft = Color(red: 0.867, green: 0.910, blue: 0.941)
static let darkBg = Color(red: 0.051, green: 0.063, blue: 0.059)
}
enum Radius {
static let xs: CGFloat = 8
static let sm: CGFloat = 14
static let md: CGFloat = 20
static let lg: CGFloat = 28
static let xl: CGFloat = 36
static let pill: CGFloat = 999
}
enum Shadow {
static func card() -> some View {
Color.clear
}
}
}
extension Font {
static func tjTitle(_ size: CGFloat = 30) -> Font { .system(size: size, weight: .bold, design: .default) }
static func tjH2(_ size: CGFloat = 18) -> Font { .system(size: size, weight: .bold, design: .default) }
static func tjMono(_ size: CGFloat = 11) -> Font { .system(size: size, weight: .regular, design: .monospaced) }
static func tjSerifBody(_ size: CGFloat = 17) -> Font { .system(size: size, weight: .regular, design: .default) }
}
extension View {
func tjCard(bordered: Bool = false, radius: CGFloat = Tj.Radius.md) -> some View {
self
.background(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.fill(Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.strokeBorder(Tj.Palette.lineSoft, lineWidth: bordered ? 1 : 0)
)
.shadow(color: bordered ? .clear : Color(red: 0.196, green: 0.157, blue: 0.098).opacity(0.05),
radius: 2, x: 0, y: 1)
}
}