chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's spaces, radii, icons and avatars are tokens
ios/Knotless/Support/Markdown.swift+7−7
| @@ -20,7 +20,7 @@ struct MarkdownView: View { | ||
| ||
var body: some View { | ||
let colours = html.map(MarkdownFences.ranges) ?? [:] | ||
VStack(alignment: .leading, spacing: 10) { | ||
VStack(alignment: .leading, spacing: Space.s2_5) { | ||
ForEach(Array(Self.parse(source).enumerated()), id: \.offset) { _, block in | ||
view(block, colours) | ||
} | ||
| @@ -31,22 +31,22 @@ struct MarkdownView: View { | ||
@ViewBuilder private func view(_ block: Block, _ colours: [String: String]) -> some View { | ||
switch block { | ||
case .heading(let level, let text): | ||
inline(text).font(level == 1 ? .title2.bold() : level == 2 ? .title3.bold() : .headline).padding(.top, 4) | ||
inline(text).font(level == 1 ? .title2.bold() : level == 2 ? .title3.bold() : .headline).padding(.top, Space.s1) | ||
case .paragraph(let text): | ||
rich(text) | ||
case .bullet(let text, let marker, let depth): | ||
HStack(alignment: .firstTextBaseline, spacing: 8) { | ||
HStack(alignment: .firstTextBaseline, spacing: Space.s2) { | ||
Text(marker).foregroundStyle(.secondary) | ||
rich(text) | ||
} | ||
.padding(.leading, CGFloat(depth) * 16) | ||
case .quote(let text): | ||
inline(text).foregroundStyle(.secondary).padding(.leading, 12) | ||
inline(text).foregroundStyle(.secondary).padding(.leading, Space.s3) | ||
case .code(let text): | ||
ScrollView(.horizontal, showsIndicators: false) { | ||
Text(Self.painted(text, colours[text])).font(.footnote.monospaced()).textSelection(.enabled).padding(10) | ||
Text(Self.painted(text, colours[text])).font(.footnote.monospaced()).textSelection(.enabled).padding(Space.s2_5) | ||
} | ||
.background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 8)) | ||
.background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: Radius.medium)) | ||
case .rule: | ||
Divider() | ||
} | ||
| @@ -68,7 +68,7 @@ struct MarkdownView: View { | ||
if pieces.count == 1, case .text(let only) = pieces[0] { | ||
inline(only) | ||
} else { | ||
VStack(alignment: .leading, spacing: 8) { | ||
VStack(alignment: .leading, spacing: Space.s2) { | ||
ForEach(Array(pieces.enumerated()), id: \.offset) { _, piece in | ||
switch piece { | ||
case .text(let words): | ||
ios/Knotless/Support/Theme.swift+56−0
| @@ -25,6 +25,62 @@ extension Color { | ||
} | ||
} | ||
| ||
/// Spaces, on the scale of `--space-*` in `web/src/tokens.css`: `s2` is its `--space-2`. | ||
enum Space { | ||
static let s0_5: CGFloat = 2 | ||
static let s1: CGFloat = 4 | ||
static let s1_5: CGFloat = 6 | ||
static let s2: CGFloat = 8 | ||
static let s2_5: CGFloat = 10 | ||
static let s3: CGFloat = 12 | ||
static let s3_5: CGFloat = 14 | ||
static let s4: CGFloat = 16 | ||
static let s5: CGFloat = 20 | ||
static let s6: CGFloat = 24 | ||
static let s7: CGFloat = 28 | ||
static let s9: CGFloat = 36 | ||
} | ||
| ||
enum Radius { | ||
static let small: CGFloat = 6 | ||
static let medium: CGFloat = 8 | ||
static let large: CGFloat = 12 | ||
/// A card of rows, or a sheet's box. | ||
static let card: CGFloat = 20 | ||
} | ||
| ||
/// An Octicon's size. | ||
enum Icon { | ||
static let xs: CGFloat = 12 | ||
static let sm: CGFloat = 14 | ||
static let md: CGFloat = 16 | ||
static let lg: CGFloat = 18 | ||
static let xl: CGFloat = 20 | ||
static let xxl: CGFloat = 24 | ||
} | ||
| ||
enum Size { | ||
enum Avatar { | ||
static let tiny: CGFloat = 16 | ||
static let xsmall: CGFloat = 20 | ||
static let small: CGFloat = 24 | ||
static let regular: CGFloat = 30 | ||
static let medium: CGFloat = 32 | ||
static let large: CGFloat = 44 | ||
static let xlarge: CGFloat = 64 | ||
} | ||
| ||
/// The least height of a row that opens a screen. | ||
static let row: CGFloat = 56 | ||
static let fileHeader: CGFloat = 52 | ||
/// Room under a page for the button floating over its end. | ||
static let floatingClearance: CGFloat = 60 | ||
/// Where a row's text starts, past its padding and a regular avatar or | ||
/// icon square, for the divider under it to line up with. | ||
static let rowInset: CGFloat = Space.s4 + Avatar.regular + Space.s3 | ||
static let smallRowInset: CGFloat = Space.s4 + Avatar.small + Space.s3 | ||
} | ||
| ||
extension UIColor { | ||
convenience init(hex: UInt32) { | ||
self.init(red: CGFloat(hex >> 16 & 255) / 255, green: CGFloat(hex >> 8 & 255) / 255, blue: CGFloat(hex & 255) / 255, alpha: 1) | ||
ios/Knotless/Views/CodeViews.swift+4−4
| @@ -69,19 +69,19 @@ private struct CodeLines: View { | ||
| ||
var body: some View { | ||
ScrollView { | ||
HStack(alignment: .top, spacing: 8) { | ||
HStack(alignment: .top, spacing: Space.s2) { | ||
column(widest: "\(painted.count)", alignment: .trailing) { i in | ||
Text("\(i + 1)").foregroundStyle(.secondary) | ||
} | ||
.padding(.leading, 8) | ||
.padding(.leading, Space.s2) | ||
ScrollView(.horizontal) { | ||
column(widest: lines.max { $0.count < $1.count } ?? "", alignment: .leading) { i in | ||
Text(painted[i]) | ||
} | ||
.padding(.trailing, 16) | ||
.padding(.trailing, Space.s4) | ||
} | ||
} | ||
.padding(.vertical, 8) | ||
.padding(.vertical, Space.s2) | ||
} | ||
} | ||
| ||
ios/Knotless/Views/CommitView.swift+17−17
| @@ -6,8 +6,8 @@ struct CommitRow: View { | ||
let commit: Commit | ||
| ||
var body: some View { | ||
VStack(alignment: .leading, spacing: 5) { | ||
HStack(alignment: .firstTextBaseline, spacing: 10) { | ||
VStack(alignment: .leading, spacing: Space.s1_5) { | ||
HStack(alignment: .firstTextBaseline, spacing: Space.s2_5) { | ||
Text(commit.subject).font(.body.weight(.semibold)).lineLimit(1) | ||
Spacer(minLength: 0) | ||
Text(When.short(commit.when)).font(.subheadline).foregroundStyle(.secondary).fixedSize() | ||
| @@ -48,10 +48,10 @@ private struct CommitPeople: View { | ||
| ||
var body: some View { | ||
let authors = commit.authors | ||
HStack(spacing: 8) { | ||
HStack(spacing: Space.s2) { | ||
HStack(spacing: -6) { | ||
ForEach(Array(authors.prefix(2).enumerated()), id: \.offset) { _, person in | ||
Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: 22) | ||
Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: Size.Avatar.small) | ||
} | ||
} | ||
if whole, let committer = commit.otherCommitter { | ||
| @@ -108,8 +108,8 @@ private struct CommitChanges: View { | ||
var body: some View { | ||
ScrollViewReader { reader in | ||
ScrollView { | ||
VStack(alignment: .leading, spacing: 12) { | ||
HStack(alignment: .firstTextBaseline, spacing: 10) { | ||
VStack(alignment: .leading, spacing: Space.s3) { | ||
HStack(alignment: .firstTextBaseline, spacing: Space.s2_5) { | ||
CommitPeople(commit: commit, whole: true) | ||
Spacer(minLength: 0) | ||
Text(When.short(commit.when)).font(.subheadline).foregroundStyle(.secondary).fixedSize() | ||
| @@ -118,14 +118,14 @@ private struct CommitChanges: View { | ||
if !commit.details.isEmpty { MarkdownView(source: commit.details, place: MarkdownPlace(repo: repo)) } | ||
Text(summary).font(.footnote).foregroundStyle(.secondary) | ||
} | ||
.padding(16) | ||
.padding(Space.s4) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { | ||
ForEach(answer.files) { file in | ||
FileSection(repo: repo, file: file).id(file.id) | ||
} | ||
} | ||
.padding(.bottom, 60) | ||
.padding(.bottom, Size.floatingClearance) | ||
} | ||
.overlay(alignment: .bottomTrailing) { | ||
if answer.files.count > 1 { | ||
| @@ -137,7 +137,7 @@ private struct CommitChanges: View { | ||
SwiftUI.Label { Text("Jump to file") } icon: { Oct(name: "file-moved") }.labelStyle(TightLabel()) | ||
} | ||
.floating() | ||
.padding(16) | ||
.padding(Space.s4) | ||
} | ||
} | ||
} | ||
| @@ -161,14 +161,14 @@ private struct CommitDetails: View { | ||
var body: some View { | ||
List { | ||
Section("Commit") { | ||
HStack(spacing: 10) { | ||
HStack(spacing: Space.s2_5) { | ||
Text(commit.sha).font(.body.monospaced()).lineLimit(1).truncationMode(.tail) | ||
Spacer(minLength: 0) | ||
Button { | ||
UIPasteboard.general.string = commit.sha | ||
copied = true | ||
} label: { | ||
Oct(name: copied ? "check" : "copy", size: 20).foregroundStyle(copied ? Color.success : Color.accent) | ||
Oct(name: copied ? "check" : "copy", size: Icon.xl).foregroundStyle(copied ? Color.success : Color.accent) | ||
} | ||
.buttonStyle(.borderless) | ||
.accessibilityLabel(copied ? "Copied" : "Copy") | ||
| @@ -187,8 +187,8 @@ private struct CommitDetails: View { | ||
Section(parents.count == 1 ? "Parent" : "Parents") { | ||
ForEach(parents, id: \.sha) { parent in | ||
NavigationLink(value: Route.commit(repo, sha: parent.sha)) { | ||
HStack(spacing: 10) { | ||
Oct(name: "git-commit", size: 20).foregroundStyle(.secondary) | ||
HStack(spacing: Space.s2_5) { | ||
Oct(name: "git-commit", size: Icon.xl).foregroundStyle(.secondary) | ||
Text(parent.sha).font(.body.monospaced()).lineLimit(1).truncationMode(.tail) | ||
} | ||
} | ||
| @@ -198,8 +198,8 @@ private struct CommitDetails: View { | ||
if let pull = commit.pull { | ||
Section("Pull") { | ||
NavigationLink(value: Route.thread(repo, uri: pull.uri)) { | ||
HStack(spacing: 10) { | ||
Oct(name: "git-pull-request", size: 20).foregroundStyle(.secondary) | ||
HStack(spacing: Space.s2_5) { | ||
Oct(name: "git-pull-request", size: Icon.xl).foregroundStyle(.secondary) | ||
Text(pull.title ?? "This commit's pull").lineLimit(2) | ||
} | ||
} | ||
| @@ -216,8 +216,8 @@ private struct CommitDetails: View { | ||
} | ||
| ||
private func person(_ person: CommitAuthor, _ role: String) -> some View { | ||
HStack(spacing: 12) { | ||
Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: 32) | ||
HStack(spacing: Space.s3) { | ||
Avatar(url: person.actor?.avatar, name: person.label, did: person.actor?.did, face: person.face, size: Size.Avatar.medium) | ||
Text(person.label).lineLimit(1) | ||
Spacer(minLength: 8) | ||
Text(role).foregroundStyle(.secondary) | ||
ios/Knotless/Views/ExploreView.swift+4−4
| @@ -68,9 +68,9 @@ struct EventRow: View { | ||
let event: FeedEvent | ||
| ||
var body: some View { | ||
HStack(alignment: .top, spacing: 12) { | ||
Avatar(url: event.actor.avatar, name: event.actor.handle, did: event.actor.did, size: 32) | ||
VStack(alignment: .leading, spacing: 3) { | ||
HStack(alignment: .top, spacing: Space.s3) { | ||
Avatar(url: event.actor.avatar, name: event.actor.handle, did: event.actor.did, size: Size.Avatar.medium) | ||
VStack(alignment: .leading, spacing: Space.s1) { | ||
HStack { | ||
Text(event.repo.label).lineLimit(1).foregroundStyle(.secondary) | ||
Spacer() | ||
| @@ -84,6 +84,6 @@ struct EventRow: View { | ||
} | ||
} | ||
} | ||
.padding(.vertical, 3) | ||
.padding(.vertical, Space.s1) | ||
} | ||
} | ||
ios/Knotless/Views/FilesView.swift+9−9
| @@ -25,7 +25,7 @@ struct FilesView: View { | ||
} | ||
.safeAreaInset(edge: .bottom) { | ||
if account.signedIn, thread.item.state == "open" { | ||
ReviewButton { reviewing = true }.padding(.bottom, 8) | ||
ReviewButton { reviewing = true }.padding(.bottom, Space.s2) | ||
} | ||
} | ||
.sheet(isPresented: $reviewing) { | ||
| @@ -42,7 +42,7 @@ private extension View { | ||
func navigationSubtitleCompat(added: Int, removed: Int) -> some View { | ||
toolbar { | ||
ToolbarItem(placement: .topBarTrailing) { | ||
HStack(spacing: 4) { | ||
HStack(spacing: Space.s1) { | ||
Text("+\(added)").foregroundStyle(Color.success) | ||
Text("-\(removed)").foregroundStyle(Color.danger) | ||
} | ||
| @@ -85,8 +85,8 @@ struct FileSection: View { | ||
} | ||
} header: { | ||
Button { open.toggle() } label: { | ||
HStack(spacing: 12) { | ||
Oct(name: "chevron-down", size: 16) | ||
HStack(spacing: Space.s3) { | ||
Oct(name: "chevron-down", size: Icon.md) | ||
.rotationEffect(open ? .zero : .degrees(-90)) | ||
.foregroundStyle(.secondary) | ||
Text(file.path).font(.subheadline.monospaced()).lineLimit(1).truncationMode(.head) | ||
| @@ -96,8 +96,8 @@ struct FileSection: View { | ||
Text("-\(file.removed)").foregroundStyle(Color.danger) | ||
} | ||
.font(.footnote.monospacedDigit()) | ||
.padding(.horizontal, 16) | ||
.frame(minHeight: 52) | ||
.padding(.horizontal, Space.s4) | ||
.frame(minHeight: Size.fileHeader) | ||
.background(Color(.secondarySystemBackground)) | ||
.overlay(alignment: .top) { Color(.separator).frame(height: 0.5) } | ||
.overlay(alignment: .bottom) { Color(.separator).frame(height: 0.5) } | ||
| @@ -159,7 +159,7 @@ private struct DiffLine: View { | ||
} | ||
| ||
var body: some View { | ||
HStack(alignment: .firstTextBaseline, spacing: 6) { | ||
HStack(alignment: .firstTextBaseline, spacing: Space.s1_5) { | ||
if line.row.line { | ||
Text(number).frame(width: 34, alignment: .trailing).foregroundStyle(.secondary) | ||
Text(marker).foregroundStyle(line.row.kind == "a" ? Color.success : Color.danger) | ||
| @@ -171,8 +171,8 @@ private struct DiffLine: View { | ||
.padding(.leading, line.row.line ? 0 : 12) | ||
} | ||
.font(.caption.monospaced()) | ||
.padding(.horizontal, 6) | ||
.padding(.vertical, line.row.line ? 1.5 : 5) | ||
.padding(.horizontal, Space.s1_5) | ||
.padding(.vertical, line.row.line ? Space.s0_5 : Space.s1_5) | ||
.background(tint) | ||
} | ||
| ||
ios/Knotless/Views/HomeView.swift+33−33
| @@ -62,18 +62,18 @@ private struct HomeScreen: View { | ||
row(.starred(person.handle), "Starred", "star", .attention) | ||
} | ||
Heading("Favorites") { | ||
Button { editing = true } label: { Oct(name: "kebab-horizontal", size: 20).foregroundStyle(.secondary) } | ||
Button { editing = true } label: { Oct(name: "kebab-horizontal", size: Icon.xl).foregroundStyle(.secondary) } | ||
.buttonStyle(.plain) | ||
.accessibilityLabel("Edit favorites") | ||
} | ||
Card { | ||
if favorites.isEmpty { | ||
VStack(spacing: 10) { | ||
VStack(spacing: Space.s2_5) { | ||
Text("Keep repositories you visit a lot here.").foregroundStyle(.secondary).multilineTextAlignment(.center) | ||
Button("Add favorites") { editing = true }.fontWeight(.medium) | ||
} | ||
.frame(maxWidth: .infinity) | ||
.padding(20) | ||
.padding(Space.s5) | ||
} | ||
ForEach(Array(favorites.enumerated()), id: \.element) { at, favorite in | ||
if at > 0 { Separator() } | ||
| @@ -93,21 +93,21 @@ private struct HomeScreen: View { | ||
} | ||
} | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.bottom, 24) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.bottom, Space.s6) | ||
} | ||
.background(Color(.systemGroupedBackground)) | ||
.navigationBarTitleDisplayMode(.inline) | ||
.toolbar { | ||
ToolbarItem(placement: .topBarLeading) { | ||
NavigationLink(value: Route.profile(person.handle)) { | ||
Avatar(url: person.avatar, name: person.handle, did: person.did, size: 32) | ||
Avatar(url: person.avatar, name: person.handle, did: person.did, size: Size.Avatar.medium) | ||
} | ||
.accessibilityLabel("Your profile") | ||
} | ||
ToolbarItemGroup(placement: .topBarTrailing) { | ||
Button { writing = true } label: { Oct(name: "plus", size: 24) }.tint(.primary).accessibilityLabel("New issue") | ||
NavigationLink(value: Route.search) { Oct(name: "search", size: 24) }.tint(.primary).accessibilityLabel("Search") | ||
Button { writing = true } label: { Oct(name: "plus", size: Icon.xxl) }.tint(.primary).accessibilityLabel("New issue") | ||
NavigationLink(value: Route.search) { Oct(name: "search", size: Icon.xxl) }.tint(.primary).accessibilityLabel("Search") | ||
} | ||
} | ||
.sheet(isPresented: $editing) { EditFavorites(person: person, stored: $stored) } | ||
| @@ -142,8 +142,8 @@ private struct Heading<Trailing: View>: View { | ||
Spacer() | ||
trailing | ||
} | ||
.padding(.top, 24) | ||
.padding(.bottom, 10) | ||
.padding(.top, Space.s6) | ||
.padding(.bottom, Space.s2_5) | ||
} | ||
} | ||
| ||
| @@ -152,21 +152,21 @@ private struct Card<Content: View>: View { | ||
| ||
var body: some View { | ||
VStack(spacing: 0) { content } | ||
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 20)) | ||
.clipShape(RoundedRectangle(cornerRadius: 20)) | ||
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: Radius.card)) | ||
.clipShape(RoundedRectangle(cornerRadius: Radius.card)) | ||
} | ||
} | ||
| ||
private struct Separator: View { | ||
var body: some View { Divider().padding(.leading, 58) } | ||
var body: some View { Divider().padding(.leading, Size.rowInset) } | ||
} | ||
| ||
private struct FavoriteRow: View { | ||
let favorite: Favorite | ||
| ||
var body: some View { | ||
HStack(spacing: 12) { | ||
Avatar(url: favorite.avatar, name: favorite.handle, did: favorite.did, size: 30) | ||
HStack(spacing: Space.s3) { | ||
Avatar(url: favorite.avatar, name: favorite.handle, did: favorite.did, size: Size.Avatar.regular) | ||
VStack(alignment: .leading, spacing: 0) { | ||
Text(favorite.handle).font(.footnote).foregroundStyle(.secondary).lineLimit(1) | ||
Text(favorite.name).font(.body.weight(.medium)).lineLimit(1) | ||
| @@ -174,8 +174,8 @@ private struct FavoriteRow: View { | ||
Spacer() | ||
RowChevron(name: "chevron-right") | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.vertical, 12) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.vertical, Space.s3) | ||
.contentShape(Rectangle()) | ||
} | ||
} | ||
| @@ -185,12 +185,12 @@ private struct RecentRow: View { | ||
| ||
var body: some View { | ||
let style = StateStyle(kind: kind(ofURI: event.action.subject), state: event.action.state) | ||
HStack(alignment: .top, spacing: 12) { | ||
Oct(name: String(style.icon.dropFirst(4)), size: 20) | ||
HStack(alignment: .top, spacing: Space.s3) { | ||
Oct(name: String(style.icon.dropFirst(4)), size: Icon.xl) | ||
.foregroundStyle(event.action.state == nil ? Color.neutral : style.color) | ||
.frame(width: 30) | ||
.padding(.top, 2) | ||
VStack(alignment: .leading, spacing: 3) { | ||
.padding(.top, Space.s0_5) | ||
VStack(alignment: .leading, spacing: Space.s1) { | ||
HStack(alignment: .firstTextBaseline) { | ||
Text(event.repo.label).font(.subheadline).foregroundStyle(.secondary).lineLimit(1) | ||
Spacer() | ||
| @@ -200,8 +200,8 @@ private struct RecentRow: View { | ||
Text(event.action.yours).font(.subheadline).foregroundStyle(.secondary) | ||
} | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.vertical, 10) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.vertical, Space.s2_5) | ||
.contentShape(Rectangle()) | ||
} | ||
} | ||
| @@ -319,7 +319,7 @@ struct MyItemsView: View { | ||
| ||
var body: some View { | ||
VStack(spacing: 0) { | ||
HStack(spacing: 8) { | ||
HStack(spacing: Space.s2) { | ||
Menu { | ||
Picker("State", selection: $state) { | ||
Text("Open").tag("open") | ||
| @@ -335,8 +335,8 @@ struct MyItemsView: View { | ||
.tint(.primary) | ||
Spacer() | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.vertical, 8) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.vertical, Space.s2) | ||
if let did = account.person?.did { | ||
PagedList<ActorItem, MineRow>(empty: kind == "pull" ? "No pull requests here." : "No issues here.", fetch: { [kind, state, relation] cursor in | ||
let answer: ActorItems = try await Client.shared.query("listActorItems", [ | ||
| @@ -352,14 +352,14 @@ struct MyItemsView: View { | ||
} | ||
| ||
private func chip(_ title: String) -> some View { | ||
HStack(spacing: 4) { | ||
HStack(spacing: Space.s1) { | ||
Text(title) | ||
Oct(name: "chevron-down", size: 12) | ||
Oct(name: "chevron-down", size: Icon.xs) | ||
} | ||
.font(.subheadline) | ||
.foregroundStyle(.primary) | ||
.padding(.horizontal, 12) | ||
.padding(.vertical, 6) | ||
.padding(.horizontal, Space.s3) | ||
.padding(.vertical, Space.s1_5) | ||
.background(Color(.tertiarySystemFill), in: Capsule()) | ||
} | ||
} | ||
| @@ -369,7 +369,7 @@ private struct MineRow: View { | ||
| ||
var body: some View { | ||
NavigationLink(value: Route.thread(found.repo, uri: found.item.uri)) { | ||
VStack(alignment: .leading, spacing: 3) { | ||
VStack(alignment: .leading, spacing: Space.s1) { | ||
Text(found.repo.label).font(.footnote).foregroundStyle(.secondary).lineLimit(1) | ||
ItemRow(item: found.item) | ||
} | ||
| @@ -409,8 +409,8 @@ struct OrganizationsView: View { | ||
} else { | ||
List(orgs, id: \.actor.did) { org in | ||
NavigationLink(value: Route.profile(org.actor.handle)) { | ||
HStack(spacing: 12) { | ||
Avatar(url: org.actor.avatar, name: org.actor.handle, did: org.actor.did, size: 32) | ||
HStack(spacing: Space.s3) { | ||
Avatar(url: org.actor.avatar, name: org.actor.handle, did: org.actor.did, size: Size.Avatar.medium) | ||
Text(org.actor.handle).lineLimit(1) | ||
} | ||
} | ||
ios/Knotless/Views/InboxView.swift+11−11
| @@ -29,18 +29,18 @@ struct InboxView: View { | ||
| ||
private var chips: some View { | ||
ScrollView(.horizontal, showsIndicators: false) { | ||
HStack(spacing: 8) { | ||
HStack(spacing: Space.s2) { | ||
ForEach(Self.filters, id: \.name) { option in | ||
Button(option.name) { filter = option.kind } | ||
.font(.subheadline.weight(.medium)) | ||
.foregroundStyle(filter == option.kind ? Color.accent : .primary) | ||
.padding(.horizontal, 12) | ||
.padding(.vertical, 7) | ||
.padding(.horizontal, Space.s3) | ||
.padding(.vertical, Space.s2) | ||
.background(filter == option.kind ? Color.accentSoft : Color(.secondarySystemBackground), in: Capsule()) | ||
} | ||
} | ||
.padding(.horizontal) | ||
.padding(.vertical, 8) | ||
.padding(.vertical, Space.s2) | ||
} | ||
} | ||
| ||
| @@ -136,19 +136,19 @@ struct InboxRow: View { | ||
private var style: StateStyle { StateStyle(kind: kind(ofURI: item.reasonSubject), state: item.subjectState) } | ||
| ||
var body: some View { | ||
HStack(alignment: .top, spacing: 12) { | ||
VStack(spacing: 8) { | ||
HStack(alignment: .top, spacing: Space.s3) { | ||
VStack(spacing: Space.s2) { | ||
if kind(ofURI: item.reasonSubject) == nil { | ||
Image(systemName: other).font(.title3).foregroundStyle(Color.neutral) | ||
} else { | ||
StateIcon(style: style, size: 22) | ||
StateIcon(style: style, size: Icon.xxl) | ||
} | ||
if !item.isRead { | ||
Circle().fill(Color.accent).frame(width: 9, height: 9).accessibilityLabel("Unread") | ||
} | ||
} | ||
.frame(width: 28) | ||
VStack(alignment: .leading, spacing: 3) { | ||
VStack(alignment: .leading, spacing: Space.s1) { | ||
HStack { | ||
Text(item.repo?.label ?? item.author.handle).lineLimit(1).foregroundStyle(.secondary) | ||
Spacer() | ||
| @@ -156,13 +156,13 @@ struct InboxRow: View { | ||
} | ||
.font(.subheadline) | ||
Text(item.title ?? what).font(.body.weight(.medium)).lineLimit(2) | ||
HStack(spacing: 6) { | ||
Avatar(url: item.author.avatar, name: item.author.handle, did: item.author.did, size: 18) | ||
HStack(spacing: Space.s1_5) { | ||
Avatar(url: item.author.avatar, name: item.author.handle, did: item.author.did, size: Size.Avatar.xsmall) | ||
Text(MarkdownView.plain(preview)).font(.subheadline).foregroundStyle(.secondary).lineLimit(1) | ||
} | ||
} | ||
} | ||
.padding(.vertical, 3) | ||
.padding(.vertical, Space.s1) | ||
} | ||
| ||
private var preview: String { | ||
ios/Knotless/Views/MarkdownImage.swift+1−1
| @@ -26,7 +26,7 @@ struct MarkdownImage: View { | ||
if failed { | ||
if !alt.isEmpty { Text(alt).foregroundStyle(.secondary) } | ||
} else { | ||
RoundedRectangle(cornerRadius: 6).fill(Color(.secondarySystemBackground)).frame(width: 120, height: 24) | ||
RoundedRectangle(cornerRadius: Radius.small).fill(Color(.secondarySystemBackground)).frame(width: 120, height: 24) | ||
} | ||
} | ||
} | ||
ios/Knotless/Views/ProfileView.swift+27−27
| @@ -51,10 +51,10 @@ private struct ProfilePage: View { | ||
let p = profile | ||
ScrollView { | ||
VStack(alignment: .leading, spacing: 0) { | ||
VStack(alignment: .leading, spacing: 14) { | ||
HStack(spacing: 14) { | ||
Avatar(url: p.avatar, name: p.handle, did: p.did, size: 64) | ||
VStack(alignment: .leading, spacing: 2) { | ||
VStack(alignment: .leading, spacing: Space.s3_5) { | ||
HStack(spacing: Space.s3_5) { | ||
Avatar(url: p.avatar, name: p.handle, did: p.did, size: Size.Avatar.xlarge) | ||
VStack(alignment: .leading, spacing: Space.s0_5) { | ||
Text(p.displayName ?? p.handle).font(.title2.bold()) | ||
Text(p.handle).foregroundStyle(.secondary) | ||
if let pronouns = p.pronouns { Text(pronouns).font(.footnote).foregroundStyle(.secondary) } | ||
| @@ -62,7 +62,7 @@ private struct ProfilePage: View { | ||
} | ||
if let about = p.description, !about.isEmpty { Text(about) } | ||
if let link = p.links?.first { LinkLine(link: link) } | ||
HStack(spacing: 6) { | ||
HStack(spacing: Space.s1_5) { | ||
Oct(name: "person").foregroundStyle(.secondary) | ||
Text("\(Text("\(p.followersCount ?? 0)").fontWeight(.semibold)) \(Text((p.followersCount ?? 0) == 1 ? "follower" : "followers").foregroundStyle(.secondary)) · \(Text("\(p.followsCount ?? 0)").fontWeight(.semibold)) \(Text("following").foregroundStyle(.secondary))") | ||
} | ||
| @@ -71,9 +71,9 @@ private struct ProfilePage: View { | ||
FollowButton(did: p.did, following: p.viewer?.following != nil) | ||
} | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.top, 8) | ||
.padding(.bottom, 20) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.top, Space.s2) | ||
.padding(.bottom, Space.s5) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
| ||
Sections { | ||
| @@ -85,15 +85,15 @@ private struct ProfilePage: View { | ||
ForEach(Array(orgs.enumerated()), id: \.element.actor.did) { index, org in | ||
if index > 0 { RowDivider() } | ||
NavigationLink(value: Route.profile(org.actor.handle)) { | ||
HStack(spacing: 12) { | ||
Avatar(url: org.actor.avatar, name: org.actor.handle, did: org.actor.did, size: 30) | ||
HStack(spacing: Space.s3) { | ||
Avatar(url: org.actor.avatar, name: org.actor.handle, did: org.actor.did, size: Size.Avatar.regular) | ||
Text(org.actor.handle) | ||
Spacer() | ||
RowChevron(name: "chevron-right") | ||
} | ||
.padding(.horizontal, 16) | ||
.padding(.vertical, 8) | ||
.frame(minHeight: 56) | ||
.padding(.horizontal, Space.s4) | ||
.padding(.vertical, Space.s2) | ||
.frame(minHeight: Size.row) | ||
.contentShape(Rectangle()) | ||
} | ||
.buttonStyle(RowPress()) | ||
| @@ -103,15 +103,15 @@ private struct ProfilePage: View { | ||
if !repos.isEmpty { | ||
SectionTitle(icon: "clock", title: "Recent") { EmptyView() } | ||
ScrollView(.horizontal, showsIndicators: false) { | ||
HStack(alignment: .top, spacing: 12) { | ||
HStack(alignment: .top, spacing: Space.s3) { | ||
ForEach(repos.prefix(6)) { RepoCard(repo: $0) } | ||
} | ||
.fixedSize(horizontal: false, vertical: true) | ||
.padding(.horizontal, 16) | ||
.padding(.horizontal, Space.s4) | ||
} | ||
} | ||
} | ||
.padding(.bottom, 24) | ||
.padding(.bottom, Space.s6) | ||
} | ||
.background(Color(.systemGroupedBackground)) | ||
} | ||
| @@ -127,7 +127,7 @@ private struct FollowButton: View { | ||
Button { | ||
Task { await toggle() } | ||
} label: { | ||
HStack(spacing: 6) { | ||
HStack(spacing: Space.s1_5) { | ||
if !following { Image(systemName: "plus") } | ||
Text(following ? "Following" : "Follow").fontWeight(.semibold) | ||
} | ||
| @@ -158,15 +158,15 @@ private struct RepoStats: View { | ||
let repo: Repo | ||
| ||
var body: some View { | ||
HStack(spacing: 12) { | ||
HStack(spacing: Space.s3) { | ||
if let stars = repo.starCount { | ||
HStack(spacing: 4) { | ||
HStack(spacing: Space.s1) { | ||
Oct(name: "star") | ||
Text("\(stars)") | ||
} | ||
} | ||
if let language = repo.language { | ||
HStack(spacing: 5) { | ||
HStack(spacing: Space.s1_5) { | ||
Circle().fill(Color.hex(language.color) ?? .neutral).frame(width: 10, height: 10) | ||
Text(language.name) | ||
} | ||
| @@ -182,9 +182,9 @@ private struct RepoCard: View { | ||
| ||
var body: some View { | ||
NavigationLink(value: Route.repo(repo.ref)) { | ||
VStack(alignment: .leading, spacing: 6) { | ||
HStack(spacing: 6) { | ||
Avatar(url: repo.owner.avatar, name: repo.owner.handle, did: repo.owner.did, size: 20) | ||
VStack(alignment: .leading, spacing: Space.s1_5) { | ||
HStack(spacing: Space.s1_5) { | ||
Avatar(url: repo.owner.avatar, name: repo.owner.handle, did: repo.owner.did, size: Size.Avatar.xsmall) | ||
Text(repo.owner.handle).font(.subheadline).foregroundStyle(.secondary).lineLimit(1) | ||
} | ||
Text(repo.name).font(.headline).lineLimit(1) | ||
| @@ -192,10 +192,10 @@ private struct RepoCard: View { | ||
Spacer(minLength: 4) | ||
RepoStats(repo: repo) | ||
} | ||
.padding(14) | ||
.padding(Space.s3_5) | ||
.frame(width: 280, alignment: .leading) | ||
.frame(maxHeight: .infinity, alignment: .topLeading) | ||
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 14)) | ||
.background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: Radius.large)) | ||
} | ||
.buttonStyle(.plain) | ||
} | ||
| @@ -211,8 +211,8 @@ struct RepositoriesView: View { | ||
}) { repos in | ||
List(repos) { repo in | ||
NavigationLink(value: Route.repo(repo.ref)) { | ||
VStack(alignment: .leading, spacing: 5) { | ||
HStack(spacing: 5) { | ||
VStack(alignment: .leading, spacing: Space.s1_5) { | ||
HStack(spacing: Space.s1_5) { | ||
Text(repo.name).font(.body.weight(.semibold)).lineLimit(1) | ||
if repo.private == true { | ||
Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary).accessibilityLabel("Private") | ||