mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 08:14:38 +08:00
Stack a List detail's metadata under its markdown (#1119)
* Stack a list detail's metadata under its markdown, as Raycast does * Drop metadata separators from a list detail, as Raycast does * Align a list detail's metadata label with the first line of its value
This commit is contained in:
@@ -7,11 +7,15 @@ struct ExtensionDetailBody: View {
|
||||
let metadata: RenderNode?
|
||||
let isLoading: Bool
|
||||
let assetsPath: String?
|
||||
/// A list's detail pane is too narrow for a metadata sidebar.
|
||||
var stacksMetadata = false
|
||||
|
||||
private static let stackedInset: CGFloat = 16
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
markdownPane
|
||||
if let metadata {
|
||||
markdownPane(trailing: stacksMetadata ? metadata : nil)
|
||||
if !stacksMetadata, let metadata {
|
||||
Rectangle().fill(Theme.Colors.separator).frame(width: 1)
|
||||
metadataPane(metadata)
|
||||
}
|
||||
@@ -19,7 +23,7 @@ struct ExtensionDetailBody: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private var markdownPane: some View {
|
||||
private func markdownPane(trailing metadata: RenderNode?) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: metrics.spacing.md) {
|
||||
if isLoading && (markdown ?? "").isEmpty {
|
||||
@@ -28,9 +32,14 @@ struct ExtensionDetailBody: View {
|
||||
if let markdown, !markdown.isEmpty {
|
||||
ExtensionMarkdownView(markdown: markdown)
|
||||
}
|
||||
if let metadata {
|
||||
ExtensionMetadataView(metadata: metadata, assetsPath: assetsPath, inline: true)
|
||||
.padding(.top, (markdown ?? "").isEmpty ? 0 : metrics.spacing.xxl)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, metrics.spacing.lg)
|
||||
.padding(
|
||||
.horizontal, stacksMetadata ? metrics.scaled(Self.stackedInset) : metrics.spacing.lg)
|
||||
.padding(.vertical, metrics.spacing.md)
|
||||
.hideNativeScrollers()
|
||||
}
|
||||
@@ -62,13 +71,23 @@ struct ExtensionMetadataView: View {
|
||||
@Environment(\.isDarkAppearance) private var isDark
|
||||
let metadata: RenderNode
|
||||
let assetsPath: String?
|
||||
var inline = false
|
||||
|
||||
private static let inlineRowHeight: CGFloat = 28
|
||||
|
||||
/// Stripes must count only real rows, so separators are dropped here.
|
||||
private var visibleChildren: [RenderNode] {
|
||||
inline
|
||||
? metadata.children.filter { $0.type != "Detail.Metadata.Separator" }
|
||||
: metadata.children
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: metrics.spacing.lg) {
|
||||
ForEach(metadata.children) { child in
|
||||
VStack(alignment: .leading, spacing: inline ? 0 : metrics.spacing.lg) {
|
||||
ForEach(Array(visibleChildren.enumerated()), id: \.element.id) { index, child in
|
||||
switch child.type {
|
||||
case "Detail.Metadata.Label":
|
||||
row(title: child.string("title")) {
|
||||
row(title: child.string("title"), index: index) {
|
||||
HStack(spacing: metrics.spacing.xs) {
|
||||
if let icon = child.props["icon"] {
|
||||
ExtensionIconView(
|
||||
@@ -82,7 +101,7 @@ struct ExtensionMetadataView: View {
|
||||
}
|
||||
}
|
||||
case "Detail.Metadata.Link":
|
||||
row(title: child.string("title")) {
|
||||
row(title: child.string("title"), index: index) {
|
||||
if let target = child.string("target"), let url = URL(string: target) {
|
||||
Link(child.string("text") ?? target, destination: url)
|
||||
.font(metrics.typography.rowTitle)
|
||||
@@ -91,7 +110,7 @@ struct ExtensionMetadataView: View {
|
||||
}
|
||||
}
|
||||
case "Detail.Metadata.TagList":
|
||||
row(title: child.string("title")) {
|
||||
row(title: child.string("title"), index: index) {
|
||||
ExtensionTagListView(tags: child.children, assetsPath: assetsPath)
|
||||
}
|
||||
case "Detail.Metadata.Separator":
|
||||
@@ -111,14 +130,35 @@ struct ExtensionMetadataView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func row<Content: View>(title: String?, @ViewBuilder content: () -> Content) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if let title, !title.isEmpty {
|
||||
Text(title)
|
||||
.font(metrics.typography.sectionHeader)
|
||||
private func row<Content: View>(
|
||||
title: String?, index: Int, @ViewBuilder content: () -> Content
|
||||
) -> some View {
|
||||
if inline {
|
||||
HStack(alignment: .firstTextBaseline, spacing: metrics.spacing.xl) {
|
||||
Text(title ?? "")
|
||||
.font(metrics.typography.rowTitle)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
content()
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
.padding(.horizontal, metrics.spacing.md)
|
||||
.padding(.vertical, metrics.spacing.xs)
|
||||
.frame(minHeight: metrics.scaled(Self.inlineRowHeight))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: metrics.radius.menu, style: .continuous)
|
||||
.fill(index.isMultiple(of: 2) ? ExtensionColors.detailCardFill : .clear))
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
if let title, !title.isEmpty {
|
||||
Text(title)
|
||||
.font(metrics.typography.sectionHeader)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ struct ExtensionListView: View {
|
||||
let onActivate: (Int) -> Void
|
||||
let onActions: (Int) -> Void
|
||||
|
||||
/// The row column beside a detail pane; narrower than Clipboard's so the markdown stays widest.
|
||||
private static let detailListWidth: CGFloat = 220
|
||||
private static let detailListWidth: CGFloat = 290
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -159,7 +158,8 @@ struct ExtensionListView: View {
|
||||
{
|
||||
ExtensionDetailBody(
|
||||
markdown: detail.string("markdown"), metadata: detail.node("metadata"),
|
||||
isLoading: detail.bool("isLoading") ?? false, assetsPath: assetsPath)
|
||||
isLoading: detail.bool("isLoading") ?? false, assetsPath: assetsPath,
|
||||
stacksMetadata: true)
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
|
||||
@@ -270,7 +270,8 @@ screens hold (see [palette.md](palette.md)).
|
||||
A tile may be a bare `{color}` swatch instead of an image, stated in any notation `ColorValue`
|
||||
reads — a colour picker writes `oklch()`, not hex.
|
||||
- **Detail** — markdown rendered block-by-block (headings, lists, code fences, quotes, rules, tables, fetched
|
||||
and inline images) with `AttributedString` handling inline styling, plus `Detail.Metadata`. An image
|
||||
and inline images) with `AttributedString` handling inline styling, plus `Detail.Metadata` — a
|
||||
sidebar on a Detail screen, appended below the markdown in a `List`'s detail pane. An image
|
||||
is full-width and at most 220pt tall; `?raycast-width=` / `?raycast-height=` on its URL, read by
|
||||
`ExtensionImageSize`, can only shrink it within that, never lift the cap. A rowless Detail's screen
|
||||
actions remain available through the primary ⏎ action and the ⌘K Actions panel.
|
||||
|
||||
Reference in New Issue
Block a user