icon: Icon-Composer-App-Icon + macOS-Build grün

- AppIcon.icon (Icon Composer, blaues W) als App-Icon integriert.
  In project.yml als Target-Source mit type:file → XcodeGen erkennt
  .icon nativ als wrapper.icon. Alter forest-grüner Platzhalter
  (AppIcon.appiconset) entfernt. actool baut Icon für iOS + macOS.
- macOS-Build repariert (war pre-existing rot seit β-3/β-5/β-6):
  iOS-only SwiftUI-Modifier mit #if os(iOS) gegated
  (textInputAutocapitalization, keyboardType, navigationBarDrawer,
  tabViewBottomAccessory, .buttonStyle(.glass)); .topBarTrailing →
  cross-platform .primaryAction; .bottomBar-Toolbar gekapselt;
  iOS-only Extensions mit platformFilter:iOS an den embed-Deps.
- Verifiziert: iOS-Sim + macOS BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-25 17:52:12 +02:00
parent fbd758d96e
commit edc60056ea
15 changed files with 173 additions and 90 deletions

View file

@ -0,0 +1,24 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M321.374 512V870.102L130.387 512V153.898L321.374 512Z" stroke="url(#paint0_linear_1454_2099)" stroke-width="57.2962"/>
<path d="M703.341 512V870.102L512.354 512V153.898L703.341 512Z" stroke="url(#paint1_linear_1454_2099)" stroke-width="57.2962"/>
<path d="M703.333 512V870.102L894.32 512V153.898L703.333 512Z" stroke="url(#paint2_linear_1454_2099)" stroke-width="57.2962"/>
<path d="M321.366 512V870.102L512.354 512V153.898L321.366 512Z" stroke="url(#paint3_linear_1454_2099)" stroke-width="57.2962"/>
<defs>
<linearGradient id="paint0_linear_1454_2099" x1="225.88" y1="153.898" x2="225.88" y2="870.102" gradientUnits="userSpaceOnUse">
<stop stop-color="#29BBFF"/>
<stop offset="1" stop-color="#0C597C"/>
</linearGradient>
<linearGradient id="paint1_linear_1454_2099" x1="607.847" y1="153.898" x2="607.847" y2="870.102" gradientUnits="userSpaceOnUse">
<stop stop-color="#29BBFF"/>
<stop offset="1" stop-color="#0C597C"/>
</linearGradient>
<linearGradient id="paint2_linear_1454_2099" x1="798.827" y1="153.898" x2="798.827" y2="870.102" gradientUnits="userSpaceOnUse">
<stop stop-color="#29BBFF"/>
<stop offset="1" stop-color="#0C597C"/>
</linearGradient>
<linearGradient id="paint3_linear_1454_2099" x1="416.86" y1="153.898" x2="416.86" y2="870.102" gradientUnits="userSpaceOnUse">
<stop stop-color="#29BBFF"/>
<stop offset="1" stop-color="#0C597C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

47
AppIcon.icon/icon.json Normal file
View file

@ -0,0 +1,47 @@
{
"fill" : "automatic",
"groups" : [
{
"layers" : [
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
},
{
"layers" : [
{
"image-name" : "wordeck-logo 2.svg",
"name" : "wordeck-logo 2",
"position" : {
"scale" : 0.9,
"translation-in-points" : [
0,
20
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View file

@ -167,27 +167,35 @@ private extension View {
/// Streifen über der TabBar auf Entdecken/Account).
@ViewBuilder
func decksCreateAccessory(visible: Bool, onTap: @escaping () -> Void) -> some View {
if #available(iOS 26.0, *), visible {
tabViewBottomAccessory {
DeckCreateAccessoryPill(action: onTap)
// `tabViewBottomAccessory` + `.buttonStyle(.glass)` sind iOS-26-only
// und auf macOS gar nicht verfügbar ganz ausklammern.
#if os(iOS)
if #available(iOS 26.0, *), visible {
tabViewBottomAccessory {
DeckCreateAccessoryPill(action: onTap)
}
} else {
self
}
} else {
#else
self
}
#endif
}
}
@available(iOS 26.0, *)
private struct DeckCreateAccessoryPill: View {
let action: () -> Void
#if os(iOS)
@available(iOS 26.0, *)
private struct DeckCreateAccessoryPill: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label("Neues Deck", systemImage: "plus")
.font(.subheadline.weight(.semibold))
var body: some View {
Button(action: action) {
Label("Neues Deck", systemImage: "plus")
.font(.subheadline.weight(.semibold))
}
.buttonStyle(.glass)
.tint(WordeckTheme.primary)
.accessibilityLabel("Neues Deck erstellen")
}
.buttonStyle(.glass)
.tint(WordeckTheme.primary)
.accessibilityLabel("Neues Deck erstellen")
}
}
#endif

View file

@ -56,7 +56,9 @@ struct DeckListView: View {
pendingShares = PendingShareStore.readAll()
})
}
#if os(iOS)
.toolbar { toolbar }
#endif
.refreshable {
await store?.refresh()
}
@ -309,27 +311,30 @@ struct DeckListView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
// Auf iOS 26 übernimmt das `.tabViewBottomAccessory` aus RootView die
// Neues Deck"-Pille. Doppelten +"-Button im Liquid-Glass-Layout
// vermeiden bottomBar-Button nur auf iOS < 26 zeigen.
if #unavailable(iOS 26.0) {
ToolbarItemGroup(placement: .bottomBar) {
Button {
authGate.require(reason: "deck-create-toolbar") {
showCreate = true
// `.bottomBar`-Placement existiert nur auf iOS; auf macOS gibt es keine
// Bottom-Toolbar die Neues Deck"-Aktion ist dort über das Detail-UI
// erreichbar. Auf iOS 26 übernimmt zudem `.tabViewBottomAccessory` aus
// RootView die Pille, deshalb der `#unavailable(iOS 26.0)`-Gate.
#if os(iOS)
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
if #unavailable(iOS 26.0) {
ToolbarItemGroup(placement: .bottomBar) {
Button {
authGate.require(reason: "deck-create-toolbar") {
showCreate = true
}
} label: {
Label("Deck hinzufügen", systemImage: "plus")
.labelStyle(.iconOnly)
.foregroundStyle(WordeckTheme.primary)
}
} label: {
Label("Deck hinzufügen", systemImage: "plus")
.labelStyle(.iconOnly)
.foregroundStyle(WordeckTheme.primary)
.accessibilityLabel("Deck hinzufügen")
Spacer()
}
.accessibilityLabel("Deck hinzufügen")
Spacer()
}
}
}
#endif
}
// swiftlint:enable type_body_length

View file

@ -27,7 +27,9 @@ struct CSVImportFormSections: View {
if !rows.isEmpty {
Section("Deck-Name") {
TextField("Deck-Name", text: $deckName)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
}
Section {

View file

@ -140,8 +140,10 @@ struct CardEditorView: View {
)
.lineLimit(3 ... 8)
.autocorrectionDisabled()
.textInputAutocapitalization(.sentences)
.monospaced()
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
.monospaced()
}
Section {
let count = Cloze.subIndexCount(clozeText)

View file

@ -371,7 +371,9 @@ private struct ManualFormSections: View {
var body: some View {
Section("Name") {
TextField("Deck-Name", text: $name)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
}
Section("Beschreibung") {
@ -450,7 +452,9 @@ private struct AITextFormSections: View {
axis: .vertical
)
.lineLimit(3 ... 6)
.textInputAutocapitalization(.sentences)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
} header: {
Text("Thema")
} footer: {
@ -484,9 +488,13 @@ private struct AISharedSections: View {
Section {
TextField("https://…", text: $url)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled(true)
#if os(iOS)
.keyboardType(.URL)
#endif
} header: {
Text("Zusätzliche URL (optional)")
} footer: {

View file

@ -20,21 +20,25 @@ struct BrowseView: View {
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.searchable(
text: $queryText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Decks suchen"
)
.onSubmit(of: .search) {
store?.browseQuery = queryText
Task { await store?.browse() }
}
.task {
if store == nil {
store = MarketplaceStore(auth: auth)
await store?.browse()
}
#if os(iOS)
.searchable(
text: $queryText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Decks suchen"
)
#else
.searchable(text: $queryText, prompt: "Decks suchen")
#endif
.onSubmit(of: .search) {
store?.browseQuery = queryText
Task { await store?.browse() }
}
.task {
if store == nil {
store = MarketplaceStore(auth: auth)
await store?.browse()
}
}
}
@ViewBuilder

View file

@ -166,7 +166,9 @@ struct MarketplacePublishView: View {
private var authorSection: some View {
Section {
TextField("Author-Slug (URL)", text: $authorSlug)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled(true)
TextField("Anzeigename", text: $authorDisplayName)
TextField("Bio (optional)", text: $authorBio, axis: .vertical)
@ -182,10 +184,14 @@ struct MarketplacePublishView: View {
private var deckMetadataSection: some View {
Section {
TextField("Slug (URL)", text: $slug)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled(true)
TextField("Titel", text: $title)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
TextField("Beschreibung", text: $deckDescription, axis: .vertical)
.lineLimit(2 ... 6)
Picker("Sprache", selection: $language) {
@ -230,9 +236,13 @@ struct MarketplacePublishView: View {
private var versionSection: some View {
Section {
TextField("SemVer (z.B. 1.0.0)", text: $semver)
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.autocorrectionDisabled(true)
#if os(iOS)
.keyboardType(.numbersAndPunctuation)
#endif
TextField("Changelog (optional)", text: $changelog, axis: .vertical)
.lineLimit(2 ... 4)
} header: {

View file

@ -36,7 +36,7 @@ struct PublicDeckView: View {
#endif
.toolbar {
if detail != nil {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
moderationMenu
}
}

View file

@ -33,7 +33,9 @@ struct ReportDeckSheet: View {
Section {
TextField("Optional: Details", text: $message, axis: .vertical)
.lineLimit(3 ... 6)
#if os(iOS)
.textInputAutocapitalization(.sentences)
#endif
} header: {
Text("Beschreibung")
} footer: {

View file

@ -26,7 +26,7 @@ struct StudySessionView: View {
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .primaryAction) {
if let session, case .studying = session.phase {
Text("\(session.remaining)")
.font(.subheadline.weight(.semibold))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View file

@ -1,38 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -47,14 +47,23 @@ targets:
product: ManaEventSync
- package: ManaSwiftUI
product: ManaAuthUI
# Beide Extensions sind iOS-only (supportedDestinations: [iOS]).
# platformFilter verhindert, dass der macOS-Build versucht, die
# iOS-.appex einzubetten („embedded content built for iOS" Fehler).
- target: WordeckWidgetExtension
embed: true
platformFilter: iOS
- target: WordeckShareExtension
embed: true
platformFilter: iOS
sources:
- path: Sources/App
- path: Sources/Features
- path: Sources/Core
# Icon Composer App-Icon (Xcode 26). Als einzelne Datei referenziert,
# damit XcodeGen nicht in das .icon-Bundle hineinrekursiert.
- path: AppIcon.icon
type: file
- path: Sources/Resources
excludes:
- "Info.plist"