Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9
@@ -20,8 +20,11 @@ struct LibraryGridView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||
spacing: 8
|
||||
// Two columns, not three: at the scaled-up type a third
|
||||
// column leaves ~9 characters per line and every title
|
||||
// truncates. The text size sets the column count.
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 2),
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(items) { item in
|
||||
card(item)
|
||||
|
||||
@@ -160,9 +160,9 @@ extension LibraryItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cards give the stamp one 11pt monospaced line, roughly 14 characters.
|
||||
/// 92 of 300 real domains are longer than that, so drop the subdomain:
|
||||
/// `toolkit.artlist.io` -> `artlist.io`. The list still shows it in full.
|
||||
/// Cards give the stamp one monospaced line, roughly 14 characters at the
|
||||
/// card's width. 92 of 300 real domains are longer than that, so drop the
|
||||
/// subdomain: `toolkit.artlist.io` -> `artlist.io`. The list shows it full.
|
||||
static func registered(_ host: String) -> String {
|
||||
let parts = host.split(separator: ".")
|
||||
guard parts.count > 2 else { return host }
|
||||
@@ -249,23 +249,23 @@ struct LibraryCard: View {
|
||||
let item: LibraryItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(item.cardDisplay)
|
||||
.font(.system(size: 11, design: .serif))
|
||||
.font(.system(size: 16.5, design: .serif))
|
||||
.foregroundStyle(item.inkOnSwatch)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(5)
|
||||
.minimumScaleFactor(0.85)
|
||||
.minimumScaleFactor(0.8)
|
||||
Spacer(minLength: 4)
|
||||
Text(item.stamp)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.font(.system(size: 16.5, design: .monospaced))
|
||||
.foregroundStyle(item.inkOnSwatch)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
.padding(9)
|
||||
.padding(13)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.aspectRatio(0.70, contentMode: .fit)
|
||||
.aspectRatio(0.82, contentMode: .fit)
|
||||
.background(RoundedRectangle(cornerRadius: 5).fill(item.swatch))
|
||||
}
|
||||
}
|
||||
@@ -304,23 +304,23 @@ struct LibraryTagStrip: View {
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 12, weight: .light))
|
||||
.font(.system(size: 18, weight: .light))
|
||||
.foregroundStyle(Paper.ink.opacity(0.6))
|
||||
.frame(width: 38, height: 30)
|
||||
.frame(width: 50, height: 42)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
}
|
||||
}
|
||||
.frame(height: 30)
|
||||
.frame(height: 42)
|
||||
}
|
||||
|
||||
private func tab(_ filter: LibraryFilter) -> some View {
|
||||
let active = filter.id == selection
|
||||
return HStack(spacing: 7) {
|
||||
Text(filter.name)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.font(.system(size: 16.5, design: .monospaced))
|
||||
.foregroundStyle(active ? Paper.ink : Paper.ink.opacity(0.45))
|
||||
.lineLimit(1)
|
||||
if filters.count > 1 {
|
||||
@@ -328,14 +328,14 @@ struct LibraryTagStrip: View {
|
||||
withAnimation(.spring(duration: 0.3, bounce: 0)) { close(filter) }
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 8, weight: .medium))
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(Paper.ink.opacity(active ? 0.5 : 0.25))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 11)
|
||||
.frame(height: 30)
|
||||
.padding(.horizontal, 15)
|
||||
.frame(height: 42)
|
||||
.background(alignment: .bottom) {
|
||||
if active {
|
||||
// Paper fill sits 0.6pt proud so it erases the strip rule
|
||||
|
||||
@@ -27,7 +27,7 @@ struct LibraryListRow: View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(item.swatch)
|
||||
.frame(width: 26, height: 34)
|
||||
.frame(width: 34, height: 46)
|
||||
.overlay(alignment: .topLeading) {
|
||||
if bookmark.unread {
|
||||
// Ink, not blue: the palette is the only color the
|
||||
@@ -35,15 +35,15 @@ struct LibraryListRow: View {
|
||||
// the chip it sits on.
|
||||
Circle()
|
||||
.fill(Paper.ink)
|
||||
.frame(width: 7, height: 7)
|
||||
.overlay(Circle().stroke(Paper.sheet, lineWidth: 1.5))
|
||||
.offset(x: -3, y: -3)
|
||||
.frame(width: 10, height: 10)
|
||||
.overlay(Circle().stroke(Paper.sheet, lineWidth: 2))
|
||||
.offset(x: -4, y: -4)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(item.listDisplay)
|
||||
.font(.system(size: 14, design: .serif))
|
||||
.font(.system(size: 21, design: .serif))
|
||||
.foregroundStyle(Paper.ink)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -55,13 +55,13 @@ struct LibraryListRow: View {
|
||||
for: bookmark.dateAdded, relativeTo: Date()
|
||||
))
|
||||
}
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.font(.system(size: 15, design: .monospaced))
|
||||
.foregroundStyle(Paper.ink.opacity(0.45))
|
||||
.lineLimit(1)
|
||||
|
||||
if let excerpt = rowExcerpt {
|
||||
Text(excerpt.text)
|
||||
.font(.system(size: 12, design: .serif))
|
||||
.font(.system(size: 18, design: .serif))
|
||||
.italic(excerpt.isAI)
|
||||
.foregroundStyle(Paper.ink.opacity(0.6))
|
||||
.lineLimit(2)
|
||||
@@ -80,14 +80,14 @@ struct LibraryListRow: View {
|
||||
onPodcast()
|
||||
} label: {
|
||||
Image(systemName: podcastCached ? "headphones.circle.fill" : "headphones.circle")
|
||||
.font(.system(size: 17, weight: .light))
|
||||
.font(.system(size: 25.5, weight: .light))
|
||||
.foregroundStyle(Paper.ink.opacity(podcastCached ? 0.75 : 0.3))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.sensoryFeedback(.impact(weight: .medium), trigger: podcastTapCount)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.vertical, 14)
|
||||
.contentShape(Rectangle())
|
||||
.overlay(alignment: .bottom) {
|
||||
if readingProgress > 0.02 {
|
||||
@@ -115,12 +115,12 @@ struct LibraryListRow: View {
|
||||
HStack(spacing: 5) {
|
||||
ForEach(effectiveTags, id: \.self) { tag in
|
||||
Text(tag)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.font(.system(size: 15, design: .monospaced))
|
||||
.foregroundStyle(Paper.ink.opacity(0.55))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(Paper.rule.opacity(0.6), lineWidth: 0.6)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ struct LibraryView: View {
|
||||
VStack(spacing: 14) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("Library")
|
||||
.font(.system(size: 34, weight: .regular, design: .serif))
|
||||
.font(.system(size: 51, weight: .regular, design: .serif))
|
||||
.foregroundStyle(Paper.ink)
|
||||
Spacer(minLength: 12)
|
||||
Button {
|
||||
@@ -82,7 +82,7 @@ struct LibraryView: View {
|
||||
searchFocused = searching
|
||||
} label: {
|
||||
Image(systemName: searching ? "xmark" : "magnifyingglass")
|
||||
.font(.system(size: 15, weight: .light))
|
||||
.font(.system(size: 22.5, weight: .light))
|
||||
.foregroundStyle(Paper.ink)
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ struct LibraryView: View {
|
||||
if searching {
|
||||
VStack(spacing: 5) {
|
||||
TextField("", text: $query, prompt: searchPrompt)
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.font(.system(size: 19.5, design: .monospaced))
|
||||
.foregroundStyle(Paper.ink)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
@@ -109,7 +109,7 @@ struct LibraryView: View {
|
||||
|
||||
private var searchPrompt: Text {
|
||||
Text("search the library")
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.font(.system(size: 19.5, design: .monospaced))
|
||||
.foregroundColor(Paper.ink.opacity(0.35))
|
||||
}
|
||||
|
||||
@@ -121,12 +121,12 @@ struct LibraryView: View {
|
||||
withAnimation(.spring(duration: 0.42, bounce: 0.12)) { layout = option }
|
||||
} label: {
|
||||
Image(systemName: option.symbol)
|
||||
.font(.system(size: 12, weight: .regular))
|
||||
.font(.system(size: 18, weight: .regular))
|
||||
.foregroundStyle(active ? Paper.sheet : Paper.ink.opacity(0.55))
|
||||
.frame(width: 34, height: 26)
|
||||
.frame(width: 46, height: 36)
|
||||
.background {
|
||||
if active {
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
RoundedRectangle(cornerRadius: 7)
|
||||
.fill(Paper.ink)
|
||||
.padding(2)
|
||||
.matchedGeometryEffect(id: "layoutPill", in: blocks)
|
||||
@@ -137,7 +137,7 @@ struct LibraryView: View {
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 7).stroke(Paper.rule, lineWidth: 0.6)
|
||||
RoundedRectangle(cornerRadius: 9).stroke(Paper.rule, lineWidth: 0.6)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,8 +171,8 @@ struct LibraryView: View {
|
||||
|
||||
private var cardGrid: some View {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3),
|
||||
spacing: 8
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 2),
|
||||
spacing: 10
|
||||
) {
|
||||
ForEach(Array(visibleItems.enumerated()), id: \.element.id) { index, item in
|
||||
LibraryCard(item: item)
|
||||
@@ -193,15 +193,15 @@ struct LibraryView: View {
|
||||
HStack(spacing: 12) {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(item.swatch)
|
||||
.frame(width: 26, height: 34)
|
||||
.frame(width: 34, height: 46)
|
||||
.matchedGeometryEffect(id: item.id, in: blocks)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(item.listDisplay)
|
||||
.font(.system(size: 13, design: .serif))
|
||||
.font(.system(size: 19.5, design: .serif))
|
||||
.foregroundStyle(Paper.ink)
|
||||
.lineLimit(1)
|
||||
Text(item.source)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.font(.system(size: 15, design: .monospaced))
|
||||
.foregroundStyle(Paper.ink.opacity(0.45))
|
||||
.lineLimit(1)
|
||||
}
|
||||
@@ -210,7 +210,7 @@ struct LibraryView: View {
|
||||
// shows as the subtitle — so the right column carries the
|
||||
// primary tag instead of repeating it.
|
||||
Text(item.tags.first ?? item.stamp)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.font(.system(size: 16.5, design: .monospaced))
|
||||
.foregroundStyle(Paper.ink.opacity(0.55))
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user