Draw Keychain key editors on the form background
The private key and certificate fields kept TextEditor's own opaque control background, which inside a grouped Form reads as a white card sitting on the section rather than as part of it. Hiding it puts them on the same footing as the skill editor's fields. Fix the prompt alignment that removing the box exposed. The overlay carried `.padding(.top, 8)`, but TextEditor insets its text container by its line fragment padding horizontally and by nothing vertically, so the prompt stood a line below the caret it was standing in for. Measured offscreen: leading 5, top 0 — the same inset the composer prompt in ConversationViews uses, now a named constant. Both sections repeated their own title in a row above the editor next to the Choose button. With no box left for that row to attach to it read as a second heading, so the button moves to the trailing edge of the section header, matching the Targets section in the snippet editor. The certificate editor gains the accessibility label its removed label row had been standing in for, and the private key editor gains a prompt, since an empty unboxed field is otherwise an unmarked gap in the form. Verification: build succeeds; check-zh-localization.sh passes; swift-format reports only the findings this file already had on main. The failing suites in the test run (ChatComposerStateTests, ColorSystemTests, WindowStateTests) fail identically on a clean HEAD and are untouched by this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cba40eb06b
commit
3115e202c7
2 files changed
+80
-30
No files matched your search
@@ -471,17 +471,12 @@ private struct SoftwareKeyEditor: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Private Key") {
|
||||
HStack {
|
||||
Text("Private Key")
|
||||
Spacer()
|
||||
Button("Choose") { chooseFile(for: .privateKey) }
|
||||
.controlSize(.small)
|
||||
}
|
||||
TextEditor(text: $privateKey)
|
||||
.font(.caption.monospaced())
|
||||
.frame(minHeight: 150)
|
||||
.accessibilityLabel("Private Key")
|
||||
Section {
|
||||
keyTextEditor(
|
||||
text: $privateKey, minHeight: 150,
|
||||
placeholder: "Paste a private key, or drag one in"
|
||||
)
|
||||
.accessibilityLabel("Private Key")
|
||||
if let existing, let material,
|
||||
material.publicKey != existing.publicKey
|
||||
{
|
||||
@@ -493,6 +488,8 @@ private struct SoftwareKeyEditor: View {
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
SecureField("Passphrase", text: $passphrase, prompt: Text("Optional"))
|
||||
} header: {
|
||||
fileSectionHeader("Private Key") { chooseFile(for: .privateKey) }
|
||||
}
|
||||
|
||||
Section("Public Key") {
|
||||
@@ -502,30 +499,19 @@ private struct SoftwareKeyEditor: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
Section("Certificate") {
|
||||
HStack {
|
||||
Text("Certificate")
|
||||
Spacer()
|
||||
Button("Choose") { chooseFile(for: .certificate) }
|
||||
.controlSize(.small)
|
||||
}
|
||||
TextEditor(text: $certificate)
|
||||
.font(.caption.monospaced())
|
||||
.frame(minHeight: 110)
|
||||
.overlay(alignment: .topLeading) {
|
||||
if certificate.isEmpty {
|
||||
Text("Certificate (optional)")
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.tertiary)
|
||||
.padding(.top, 8).padding(.leading, 5)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
keyTextEditor(
|
||||
text: $certificate, minHeight: 110,
|
||||
placeholder: "Certificate (optional)"
|
||||
)
|
||||
.accessibilityLabel("Certificate")
|
||||
if let warning = certificateWarning {
|
||||
Label(warning, systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
} header: {
|
||||
fileSectionHeader("Certificate") { chooseFile(for: .certificate) }
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
@@ -568,6 +554,59 @@ private struct SoftwareKeyEditor: View {
|
||||
.task(id: validationToken) { await derivePublicMaterial() }
|
||||
}
|
||||
|
||||
/// A key blob field drawn on the form's own background.
|
||||
///
|
||||
/// `TextEditor` brings its own opaque control background, which inside a
|
||||
/// grouped `Form` reads as a white card sitting on the section rather than
|
||||
/// as part of it. Hiding it puts these two fields on the same footing as the
|
||||
/// skill editor's, which is the surface this one is measured against.
|
||||
///
|
||||
/// The prompt is the price of that: `TextEditor` has no `prompt:`, and once
|
||||
/// the box is gone an empty field is an unmarked gap in the form instead of
|
||||
/// a visibly empty box. It matches the editor's font so the first typed
|
||||
/// character lands exactly where the prompt started.
|
||||
private func keyTextEditor(
|
||||
text: Binding<String>, minHeight: CGFloat, placeholder: LocalizedStringKey
|
||||
) -> some View {
|
||||
TextEditor(text: text)
|
||||
.font(.caption.monospaced())
|
||||
.frame(minHeight: minHeight)
|
||||
.scrollContentBackground(.hidden)
|
||||
.overlay(alignment: .topLeading) {
|
||||
if text.wrappedValue.isEmpty {
|
||||
Text(placeholder)
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.tertiary)
|
||||
.padding(.leading, Self.textInset)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `TextEditor`'s own text-container inset, which the prompt has to match or
|
||||
/// the first typed character lands somewhere else than the prompt did.
|
||||
///
|
||||
/// Only leading: the container is inset by its line fragment padding
|
||||
/// horizontally and by nothing vertically, so the top padding this used to
|
||||
/// carry pushed the prompt a line below the caret it was standing in for.
|
||||
/// Same value the composer prompt in `ConversationViews` uses.
|
||||
private static let textInset: CGFloat = 5
|
||||
|
||||
/// The section title with its file picker at the trailing edge.
|
||||
///
|
||||
/// The button used to head a row of its own, next to a label repeating the
|
||||
/// section title. With the editor no longer drawn as a box, that row had
|
||||
/// nothing left to attach to and read as a second heading.
|
||||
private func fileSectionHeader(
|
||||
_ title: LocalizedStringKey, choose: @escaping () -> Void
|
||||
) -> some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Button("Choose", action: choose)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func commit() async {
|
||||
isWorking = true
|
||||
|
||||
@@ -7517,6 +7517,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Paste a private key, or drag one in" : {
|
||||
"comment" : "The prompt shown in an empty private key editor.",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "粘贴私钥,或将文件拖到这里"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pasted image" : {
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
|
||||
Reference in new issue
Block a user