refactor(android): split tokens into Dark/Light/Flat + LocalFabledSwordTheme + light mode
Sub-task 1 of the design-drift correction. Adds light theme support
and adopts the Flutter pattern of consuming tokens directly via a
theme-extension equivalent (CompositionLocal data class) rather than
fitting everything into Material's ColorScheme roles.
Files:
- theme/FabledSwordTokens.kt — split the monolith into three
objects: FabledSwordDarkTokens (#14171A obsidian, etc.),
FabledSwordLightTokens (#F8F5EE obsidian, #14171A parchment — note
the semantic inversion, names are roles not literal colors),
FabledSwordFlatTokens (moss/bronze/oxblood/warning/error/info/
accent/onAction + radii). The old FabledSwordTokens object is
kept as a @Deprecated alias re-exporting dark+flat — existing call
sites compile with a warning until migrated.
- theme/FabledSwordTheme.kt — NEW. Data class holding the full
14-color set + radii, with Dark/Light companion factories.
LocalFabledSwordTheme CompositionLocal exposes the active variant.
- theme/MinstrelTheme.kt — both darkColorScheme and lightColorScheme
defined; MinstrelTheme composable accepts darkOverride and falls
back to isSystemInDarkTheme(). Provides LocalFabledSwordTheme +
keeps LocalActionColors for back-compat.
- widget files (AlbumCard, MiniPlayer, NowPlayingScreen,
ActionColors) migrated to import FabledSwordFlatTokens directly
for radii / action colors (mode-independent).
Sub-tasks 2-4 (shell, Home, Library tabs) will follow, each its own
commit. Detail screens stay as stubs until their feature phases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ import com.composables.icons.lucide.Disc3
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import androidx.compose.material3.Icon
|
||||
import com.fabledsword.minstrel.models.AlbumRef
|
||||
import com.fabledsword.minstrel.theme.FabledSwordTokens
|
||||
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||
|
||||
/**
|
||||
* One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp
|
||||
@@ -49,7 +49,7 @@ fun AlbumCard(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(144.dp)
|
||||
.clip(RoundedCornerShape(FabledSwordTokens.radiusSm)),
|
||||
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (album.coverUrl.isEmpty()) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Music
|
||||
import com.composables.icons.lucide.Pause
|
||||
import com.composables.icons.lucide.Play
|
||||
import com.fabledsword.minstrel.theme.FabledSwordTokens
|
||||
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||
|
||||
private const val MINI_BAR_HEIGHT_DP = 64
|
||||
private const val MINI_BAR_TONAL_ELEVATION_DP = 4
|
||||
@@ -69,7 +69,7 @@ fun MiniPlayer(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(COVER_SIZE_DP.dp)
|
||||
.clip(RoundedCornerShape(FabledSwordTokens.radiusSm)),
|
||||
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.composables.icons.lucide.Play
|
||||
import com.composables.icons.lucide.SkipBack
|
||||
import com.composables.icons.lucide.SkipForward
|
||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||
import com.fabledsword.minstrel.theme.FabledSwordTokens
|
||||
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||
import com.fabledsword.minstrel.theme.LocalActionColors
|
||||
|
||||
private const val COVER_MAX_WIDTH_DP = 320
|
||||
@@ -101,7 +101,7 @@ private fun CoverPlaceholder() {
|
||||
.widthIn(max = COVER_MAX_WIDTH_DP.dp)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(FabledSwordTokens.radiusLg)),
|
||||
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusLg)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Semantic action colors. The FabledSword design system reserves Moss/Bronze/
|
||||
* Oxblood for action buttons; the forest-teal accent (`FabledSwordTokens.accent`)
|
||||
* Oxblood for action buttons; the forest-teal accent (`FabledSwordFlatTokens.accent`)
|
||||
* is for surfaces / highlights / non-action emphasis only and must NEVER be
|
||||
* used as a button color.
|
||||
*
|
||||
@@ -21,9 +21,9 @@ data class ActionColors(
|
||||
|
||||
val LocalActionColors = staticCompositionLocalOf {
|
||||
ActionColors(
|
||||
primary = FabledSwordTokens.moss,
|
||||
secondary = FabledSwordTokens.bronze,
|
||||
destructive = FabledSwordTokens.oxblood,
|
||||
onAction = FabledSwordTokens.onAction,
|
||||
primary = FabledSwordFlatTokens.moss,
|
||||
secondary = FabledSwordFlatTokens.bronze,
|
||||
destructive = FabledSwordFlatTokens.oxblood,
|
||||
onAction = FabledSwordFlatTokens.onAction,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.fabledsword.minstrel.theme
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
/**
|
||||
* Mode-resolved snapshot of the FabledSword design system, exposed via
|
||||
* [LocalFabledSwordTheme]. Mirrors the Flutter `FabledSwordTheme`
|
||||
* ThemeExtension — most Flutter widgets read `Theme.of(context).extension<
|
||||
* FabledSwordTheme>()!.parchment` directly rather than via `colorScheme`,
|
||||
* and the Android port follows the same convention to keep token
|
||||
* semantics centralized rather than scattered across Material role mappings.
|
||||
*
|
||||
* UI code reads tokens via `LocalFabledSwordTheme.current.parchment`,
|
||||
* `.iron`, `.accent`, etc. The MinstrelTheme composable swaps between
|
||||
* the dark and light instances based on system mode.
|
||||
*/
|
||||
data class FabledSwordTheme(
|
||||
// Surface ladder (lightest text → darkest bg).
|
||||
val parchment: Color,
|
||||
val vellum: Color,
|
||||
val ash: Color,
|
||||
val pewter: Color,
|
||||
val slate: Color,
|
||||
val iron: Color,
|
||||
val obsidian: Color,
|
||||
|
||||
// Mode-independent action + status colors.
|
||||
val moss: Color,
|
||||
val bronze: Color,
|
||||
val oxblood: Color,
|
||||
val warning: Color,
|
||||
val error: Color,
|
||||
val info: Color,
|
||||
val accent: Color,
|
||||
val onAction: Color,
|
||||
|
||||
// Radii.
|
||||
val radiusSm: Dp,
|
||||
val radiusMd: Dp,
|
||||
val radiusLg: Dp,
|
||||
val radiusXl: Dp,
|
||||
) {
|
||||
companion object {
|
||||
val Dark: FabledSwordTheme = FabledSwordTheme(
|
||||
parchment = FabledSwordDarkTokens.parchment,
|
||||
vellum = FabledSwordDarkTokens.vellum,
|
||||
ash = FabledSwordDarkTokens.ash,
|
||||
pewter = FabledSwordDarkTokens.pewter,
|
||||
slate = FabledSwordDarkTokens.slate,
|
||||
iron = FabledSwordDarkTokens.iron,
|
||||
obsidian = FabledSwordDarkTokens.obsidian,
|
||||
moss = FabledSwordFlatTokens.moss,
|
||||
bronze = FabledSwordFlatTokens.bronze,
|
||||
oxblood = FabledSwordFlatTokens.oxblood,
|
||||
warning = FabledSwordFlatTokens.warning,
|
||||
error = FabledSwordFlatTokens.error,
|
||||
info = FabledSwordFlatTokens.info,
|
||||
accent = FabledSwordFlatTokens.accent,
|
||||
onAction = FabledSwordFlatTokens.onAction,
|
||||
radiusSm = FabledSwordFlatTokens.radiusSm,
|
||||
radiusMd = FabledSwordFlatTokens.radiusMd,
|
||||
radiusLg = FabledSwordFlatTokens.radiusLg,
|
||||
radiusXl = FabledSwordFlatTokens.radiusXl,
|
||||
)
|
||||
|
||||
val Light: FabledSwordTheme = FabledSwordTheme(
|
||||
parchment = FabledSwordLightTokens.parchment,
|
||||
vellum = FabledSwordLightTokens.vellum,
|
||||
ash = FabledSwordLightTokens.ash,
|
||||
pewter = FabledSwordLightTokens.pewter,
|
||||
slate = FabledSwordLightTokens.slate,
|
||||
iron = FabledSwordLightTokens.iron,
|
||||
obsidian = FabledSwordLightTokens.obsidian,
|
||||
moss = FabledSwordFlatTokens.moss,
|
||||
bronze = FabledSwordFlatTokens.bronze,
|
||||
oxblood = FabledSwordFlatTokens.oxblood,
|
||||
warning = FabledSwordFlatTokens.warning,
|
||||
error = FabledSwordFlatTokens.error,
|
||||
info = FabledSwordFlatTokens.info,
|
||||
accent = FabledSwordFlatTokens.accent,
|
||||
onAction = FabledSwordFlatTokens.onAction,
|
||||
radiusSm = FabledSwordFlatTokens.radiusSm,
|
||||
radiusMd = FabledSwordFlatTokens.radiusMd,
|
||||
radiusLg = FabledSwordFlatTokens.radiusLg,
|
||||
radiusXl = FabledSwordFlatTokens.radiusXl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads as `LocalFabledSwordTheme.current.X` from anywhere inside a
|
||||
* [MinstrelTheme] composable scope. Default = dark; the MinstrelTheme
|
||||
* composable overrides this based on system mode.
|
||||
*/
|
||||
val LocalFabledSwordTheme = staticCompositionLocalOf { FabledSwordTheme.Dark }
|
||||
@@ -4,39 +4,96 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* FabledSword design-system tokens. Pure data — no theme wiring.
|
||||
* Raw color values for the dark surface cohort. Source of truth:
|
||||
* `flutter_client/shared/fabledsword.tokens.json` (dark block).
|
||||
*
|
||||
* Source of truth: `flutter_client/shared/fabledsword.tokens.json`. When that
|
||||
* file changes, mirror the new hex values here. (Native v1 doesn't run the
|
||||
* Flutter `tool/gen_tokens.dart` generator — manual sync until a cross-language
|
||||
* codegen pipeline lands.)
|
||||
*
|
||||
* Mirrors flutter_client/lib/theme/tokens.dart, dark surfaces + flat. Light
|
||||
* theme support deferred to post-v1; the user's primary mode is dark.
|
||||
* Token names are **semantic roles**, not literal colors. In dark
|
||||
* mode, `parchment` is light text on dark surfaces (#E8E4D8); in
|
||||
* [FabledSwordLightTokens], `parchment` is dark text on light
|
||||
* surfaces (#14171A). Consume tokens through [LocalFabledSwordTheme],
|
||||
* never reference these raw objects from UI code — the mode-switch
|
||||
* stops working otherwise.
|
||||
*/
|
||||
object FabledSwordTokens {
|
||||
// Dark surface tokens.
|
||||
val obsidian = Color(0xFF14171A)
|
||||
val iron = Color(0xFF1E2228)
|
||||
val slate = Color(0xFF2C313A)
|
||||
val pewter = Color(0xFF3F4651)
|
||||
object FabledSwordDarkTokens {
|
||||
val obsidian = Color(0xFF14171A)
|
||||
val iron = Color(0xFF1E2228)
|
||||
val slate = Color(0xFF2C313A)
|
||||
val pewter = Color(0xFF3F4651)
|
||||
val parchment = Color(0xFFE8E4D8)
|
||||
val vellum = Color(0xFFC2BFB4)
|
||||
val ash = Color(0xFF9C9A92)
|
||||
val vellum = Color(0xFFC2BFB4)
|
||||
val ash = Color(0xFF9C9A92)
|
||||
}
|
||||
|
||||
// Flat (theme-mode-independent) tokens.
|
||||
val moss = Color(0xFF4A5D3F) // primary action
|
||||
val bronze = Color(0xFF8B7355) // secondary action
|
||||
val oxblood = Color(0xFF6B2118) // destructive action
|
||||
val warning = Color(0xFF8B6F1E)
|
||||
val error = Color(0xFFC04A1F)
|
||||
val info = Color(0xFF3D5A6E)
|
||||
val accent = Color(0xFF4A6B5C) // forest-teal — NEVER for actions
|
||||
/**
|
||||
* Raw color values for the light surface cohort. Mirrors
|
||||
* `fabledsword.tokens.json`'s light block.
|
||||
*
|
||||
* Same names as [FabledSwordDarkTokens] but inverted semantics — see
|
||||
* that doc-comment.
|
||||
*/
|
||||
object FabledSwordLightTokens {
|
||||
val obsidian = Color(0xFFF8F5EE)
|
||||
val iron = Color(0xFFECE6D5)
|
||||
val slate = Color(0xFFDCD3BD)
|
||||
val pewter = Color(0xFFB8AE94)
|
||||
val parchment = Color(0xFF14171A)
|
||||
val vellum = Color(0xFF2C313A)
|
||||
val ash = Color(0xFF5C6068)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode-independent (flat) tokens. Action colors live here; they're
|
||||
* the same in light + dark. Per the design system: Moss / Bronze /
|
||||
* Oxblood are action colors; the forest-teal accent is for
|
||||
* highlights / non-action emphasis and must NEVER be used as a
|
||||
* button color.
|
||||
*/
|
||||
object FabledSwordFlatTokens {
|
||||
val moss = Color(0xFF4A5D3F)
|
||||
val bronze = Color(0xFF8B7355)
|
||||
val oxblood = Color(0xFF6B2118)
|
||||
val warning = Color(0xFF8B6F1E)
|
||||
val error = Color(0xFFC04A1F)
|
||||
val info = Color(0xFF3D5A6E)
|
||||
val accent = Color(0xFF4A6B5C)
|
||||
val onAction = Color(0xFFE8E4D8)
|
||||
|
||||
// Corner radii.
|
||||
val radiusSm = 4.dp
|
||||
val radiusMd = 8.dp
|
||||
val radiusLg = 12.dp
|
||||
val radiusXl = 16.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-compat alias mirroring the Flutter `FabledSwordTokens` class
|
||||
* (dark surfaces + flat). Existing call sites that read
|
||||
* `FabledSwordTokens.iron` continue to work but bind to dark-mode
|
||||
* values regardless of system mode. New code should read from
|
||||
* `LocalFabledSwordTheme.current` instead. Removed in a follow-up
|
||||
* once all widgets are migrated.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use LocalFabledSwordTheme.current — see FabledSwordTheme.kt",
|
||||
level = DeprecationLevel.WARNING,
|
||||
)
|
||||
object FabledSwordTokens {
|
||||
val obsidian = FabledSwordDarkTokens.obsidian
|
||||
val iron = FabledSwordDarkTokens.iron
|
||||
val slate = FabledSwordDarkTokens.slate
|
||||
val pewter = FabledSwordDarkTokens.pewter
|
||||
val parchment = FabledSwordDarkTokens.parchment
|
||||
val vellum = FabledSwordDarkTokens.vellum
|
||||
val ash = FabledSwordDarkTokens.ash
|
||||
val moss = FabledSwordFlatTokens.moss
|
||||
val bronze = FabledSwordFlatTokens.bronze
|
||||
val oxblood = FabledSwordFlatTokens.oxblood
|
||||
val warning = FabledSwordFlatTokens.warning
|
||||
val error = FabledSwordFlatTokens.error
|
||||
val info = FabledSwordFlatTokens.info
|
||||
val accent = FabledSwordFlatTokens.accent
|
||||
val onAction = FabledSwordFlatTokens.onAction
|
||||
val radiusSm = FabledSwordFlatTokens.radiusSm
|
||||
val radiusMd = FabledSwordFlatTokens.radiusMd
|
||||
val radiusLg = FabledSwordFlatTokens.radiusLg
|
||||
val radiusXl = FabledSwordFlatTokens.radiusXl
|
||||
}
|
||||
|
||||
@@ -1,49 +1,96 @@
|
||||
package com.fabledsword.minstrel.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
||||
/**
|
||||
* Dark Material 3 color scheme assembled from FabledSword tokens. The accent
|
||||
* (forest-teal) becomes the M3 `primary`; surfaces/backgrounds come from the
|
||||
* dark token cohort. Action colors do NOT come from this scheme — see
|
||||
* `LocalActionColors`.
|
||||
* The Flutter theme's `ColorScheme` role mapping (theme_data.dart):
|
||||
* primary = accent (forest-teal) — same both modes
|
||||
* onPrimary = onAction
|
||||
* surface = iron
|
||||
* onSurface = parchment
|
||||
* secondary = moss
|
||||
* error = error
|
||||
* scaffoldBackground = obsidian
|
||||
*
|
||||
* Light theme is deferred to post-v1 (mirrors Flutter primary mode).
|
||||
* Reproduced here for both modes. Note that token names ARE semantic
|
||||
* roles — `iron` is "raised surface" in both modes (#1E2228 dark vs
|
||||
* #ECE6D5 light), not literal "near-black".
|
||||
*/
|
||||
private val MinstrelDarkColorScheme = darkColorScheme(
|
||||
primary = FabledSwordTokens.accent,
|
||||
onPrimary = FabledSwordTokens.onAction,
|
||||
primaryContainer = FabledSwordTokens.accent.copy(alpha = 0.25f),
|
||||
onPrimaryContainer = FabledSwordTokens.parchment,
|
||||
secondary = FabledSwordTokens.bronze,
|
||||
onSecondary = FabledSwordTokens.onAction,
|
||||
background = FabledSwordTokens.obsidian,
|
||||
onBackground = FabledSwordTokens.parchment,
|
||||
surface = FabledSwordTokens.iron,
|
||||
onSurface = FabledSwordTokens.parchment,
|
||||
surfaceVariant = FabledSwordTokens.slate,
|
||||
onSurfaceVariant = FabledSwordTokens.ash,
|
||||
error = FabledSwordTokens.error,
|
||||
onError = FabledSwordTokens.onAction,
|
||||
outline = FabledSwordTokens.pewter,
|
||||
outlineVariant = FabledSwordTokens.slate,
|
||||
)
|
||||
private fun darkMaterialColorScheme() =
|
||||
darkColorScheme(
|
||||
primary = FabledSwordFlatTokens.accent,
|
||||
onPrimary = FabledSwordFlatTokens.onAction,
|
||||
primaryContainer = FabledSwordFlatTokens.accent.copy(alpha = 0.25f),
|
||||
onPrimaryContainer = FabledSwordDarkTokens.parchment,
|
||||
secondary = FabledSwordFlatTokens.moss,
|
||||
onSecondary = FabledSwordFlatTokens.onAction,
|
||||
background = FabledSwordDarkTokens.obsidian,
|
||||
onBackground = FabledSwordDarkTokens.parchment,
|
||||
surface = FabledSwordDarkTokens.iron,
|
||||
onSurface = FabledSwordDarkTokens.parchment,
|
||||
surfaceVariant = FabledSwordDarkTokens.slate,
|
||||
onSurfaceVariant = FabledSwordDarkTokens.ash,
|
||||
error = FabledSwordFlatTokens.error,
|
||||
onError = FabledSwordFlatTokens.onAction,
|
||||
outline = FabledSwordDarkTokens.pewter,
|
||||
outlineVariant = FabledSwordDarkTokens.slate,
|
||||
)
|
||||
|
||||
private fun lightMaterialColorScheme() =
|
||||
lightColorScheme(
|
||||
primary = FabledSwordFlatTokens.accent,
|
||||
onPrimary = FabledSwordFlatTokens.onAction,
|
||||
primaryContainer = FabledSwordFlatTokens.accent.copy(alpha = 0.18f),
|
||||
onPrimaryContainer = FabledSwordLightTokens.parchment,
|
||||
secondary = FabledSwordFlatTokens.moss,
|
||||
onSecondary = FabledSwordFlatTokens.onAction,
|
||||
background = FabledSwordLightTokens.obsidian,
|
||||
onBackground = FabledSwordLightTokens.parchment,
|
||||
surface = FabledSwordLightTokens.iron,
|
||||
onSurface = FabledSwordLightTokens.parchment,
|
||||
surfaceVariant = FabledSwordLightTokens.slate,
|
||||
onSurfaceVariant = FabledSwordLightTokens.ash,
|
||||
error = FabledSwordFlatTokens.error,
|
||||
onError = FabledSwordFlatTokens.onAction,
|
||||
outline = FabledSwordLightTokens.pewter,
|
||||
outlineVariant = FabledSwordLightTokens.slate,
|
||||
)
|
||||
|
||||
/**
|
||||
* Theme entry point. Selects dark vs light based on system mode and
|
||||
* provides both the Material 3 [MaterialTheme] and the
|
||||
* [LocalFabledSwordTheme] for token-level reads. [LocalActionColors]
|
||||
* is kept for back-compat — same values; consumers should migrate to
|
||||
* `LocalFabledSwordTheme.current.moss` etc. in a later cleanup.
|
||||
*
|
||||
* `darkOverride` lets callers force one mode (Phase 11 Settings will
|
||||
* carry a user-controlled override).
|
||||
*/
|
||||
@Composable
|
||||
fun MinstrelTheme(content: @Composable () -> Unit) {
|
||||
fun MinstrelTheme(
|
||||
darkOverride: Boolean? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val isDark = darkOverride ?: isSystemInDarkTheme()
|
||||
val colorScheme = if (isDark) darkMaterialColorScheme() else lightMaterialColorScheme()
|
||||
val fabledTheme = if (isDark) FabledSwordTheme.Dark else FabledSwordTheme.Light
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MinstrelDarkColorScheme,
|
||||
typography = MinstrelTypography,
|
||||
colorScheme = colorScheme,
|
||||
typography = MinstrelTypography,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalFabledSwordTheme provides fabledTheme,
|
||||
LocalActionColors provides ActionColors(
|
||||
primary = FabledSwordTokens.moss,
|
||||
secondary = FabledSwordTokens.bronze,
|
||||
destructive = FabledSwordTokens.oxblood,
|
||||
onAction = FabledSwordTokens.onAction,
|
||||
primary = FabledSwordFlatTokens.moss,
|
||||
secondary = FabledSwordFlatTokens.bronze,
|
||||
destructive = FabledSwordFlatTokens.oxblood,
|
||||
onAction = FabledSwordFlatTokens.onAction,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user