Library design: cards + redesigned list on the bookmarks screen, tag picker sheet #9
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user