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