Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9

Merged
admin merged 11 commits from feat/library-browse-prototype into master 2026-07-30 04:03:03 +00:00
4 changed files with 48 additions and 45 deletions
Showing only changes of commit cdbec4aaf8 - Show all commits
+5 -2
View File
@@ -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)
+16 -16
View File
@@ -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
+13 -13
View File
@@ -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)
)
}
+14 -14
View File
@@ -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)
}