Skip to content

Commit 58e5b43

Browse files
committed
feat(android): add theme attributes export for colors
Generate attrs.xml and styles.xml content from color exports with marker-based file updates. Supports batch mode via shared collector. - Add AndroidThemeAttributesExporter for generating XML content - Add ThemeAttributeNameTransformer for snake_case to PascalCase - Add MarkerFileUpdater for marker-based section replacement - Add SharedThemeAttributesCollector for batch mode coordination - Support configurable name transformation and prefix stripping - Include collision detection for duplicate attribute names
1 parent 6793538 commit 58e5b43

19 files changed

Lines changed: 2216 additions & 27 deletions

.claude/EXFIG.toon

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@ buildCommands:
267267
docsPreview: mise run docs:preview
268268
run: .build/debug/exfig
269269

270-
dependencies[11]{name,version,purpose}:
270+
dependencies[12]{name,version,purpose}:
271271
swift-argument-parser,1.5.0+,CLI framework
272+
swift-collections,1.2.x,Ordered collections
272273
Yams,5.3.0+,YAML parsing
273274
Stencil,0.15.1+,Template engine
274275
StencilSwiftKit,2.10.1+,Swift Stencil extensions

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ RetryLogger.formatRetryMessage(context)
306306
| Asset Discovery | `noAssetsFound` |
307307
| Xcode | `xcodeProjectUpdateFailed` |
308308
| Batch | `noConfigsFound`, `invalidConfigsSkipped`, `noValidConfigs`, `checkpointExpired`, `checkpointPathMismatch`, `preFetchPartialFailure` |
309+
| Theme Attrs | `themeAttributesFileNotFound`, `themeAttributesMarkerNotFound`, `themeAttributesNameCollision` |
309310
| Retry | `retrying(attempt:maxAttempts:error:delay:)` |
310311

311312
**Adding new warnings:**
@@ -733,6 +734,7 @@ exfig init -p android
733734
| Package | Version | Purpose |
734735
| --------------------- | ------- | -------------------------- |
735736
| swift-argument-parser | 1.5.0+ | CLI framework |
737+
| swift-collections | 1.2.x | Ordered collections |
736738
| Yams | 5.3.0+ | YAML parsing |
737739
| Stencil | 0.15.1+ | Template engine |
738740
| StencilSwiftKit | 2.10.1+ | Swift Stencil extensions |

CONFIG.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,32 @@ android:
226226
colors:
227227
# [optional] The package to export the Jetpack Compose color code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set
228228
composePackageName: "com.example"
229+
# [optional] Theme attributes configuration for generating attrs.xml and styles.xml
230+
themeAttributes:
231+
# Enable theme attributes export
232+
enabled: true
233+
# Path to attrs.xml (relative to mainRes)
234+
attrsFile: "../../../values/attrs.xml"
235+
# Path to styles.xml for light mode (relative to mainRes)
236+
stylesFile: "../../../values/styles.xml"
237+
# [optional] Path to styles-night.xml for dark mode (relative to mainRes)
238+
stylesNightFile: "../../../values-night/styles.xml"
239+
# Theme name used in markers (e.g., "Theme.BaseTheme.inDrive")
240+
themeName: "Theme.BaseTheme.inDrive"
241+
# [optional] Custom marker start text. Default: "FIGMA COLORS MARKER START"
242+
markerStart: "FIGMA COLORS MARKER START"
243+
# [optional] Custom marker end text. Default: "FIGMA COLORS MARKER END"
244+
markerEnd: "FIGMA COLORS MARKER END"
245+
# [optional] Auto-create files with markers if missing. Default: false
246+
autoCreateMarkers: false
247+
# [optional] Name transformation settings
248+
nameTransform:
249+
# Target case style: camelCase, PascalCase, snake_case, etc. Default: PascalCase
250+
style: PascalCase
251+
# Prefix to add to attribute names. Default: "color"
252+
prefix: "color"
253+
# [optional] Prefixes to strip from color names before transformation
254+
stripPrefixes: ["extensions_", "information_", "statement_", "additional_"]
229255
# Parameters for exporting icons
230256
# Can be a single object (legacy format) or an array of objects (new format)
231257
# Legacy format (single icons configuration):
@@ -688,6 +714,156 @@ If `figmaFrameName` is not specified in an entry, it falls back to:
688714
1. `common.images.figmaFrameName` (if defined)
689715
2. `"Illustrations"` (default)
690716

717+
## Android Theme Attributes
718+
719+
ExFig supports generating Android theme attributes (`attrs.xml` and `styles.xml`) that reference exported color
720+
resources. This is useful for creating theme-aware apps where colors are accessed via theme attributes rather than direct
721+
resource references.
722+
723+
### Overview
724+
725+
Theme attributes allow your app to reference colors like `?attr/colorBackgroundPrimary` instead of
726+
`@color/background_primary`. This enables:
727+
728+
- Theme switching at runtime
729+
- Centralized color management through themes
730+
- Separation between design tokens and theme-specific values
731+
732+
### Generated Output
733+
734+
**attrs.xml** (attribute declarations):
735+
736+
```xml
737+
<resources>
738+
<!-- FIGMA COLORS MARKER START: Theme.BaseTheme.inDrive -->
739+
<attr name="colorBackgroundPrimary" format="color" />
740+
<attr name="colorBackgroundSecondary" format="color" />
741+
<attr name="colorTextPrimary" format="color" />
742+
<!-- FIGMA COLORS MARKER END: Theme.BaseTheme.inDrive -->
743+
</resources>
744+
```
745+
746+
**styles.xml** (theme values):
747+
748+
```xml
749+
<resources>
750+
<style name="Theme.BaseTheme.inDrive" parent="Theme.MaterialComponents.DayNight">
751+
<!-- FIGMA COLORS MARKER START: Theme.BaseTheme.inDrive -->
752+
<item name="colorBackgroundPrimary">@color/background_primary</item>
753+
<item name="colorBackgroundSecondary">@color/background_secondary</item>
754+
<item name="colorTextPrimary">@color/text_primary</item>
755+
<!-- FIGMA COLORS MARKER END: Theme.BaseTheme.inDrive -->
756+
</style>
757+
</resources>
758+
```
759+
760+
### Configuration
761+
762+
```yaml
763+
android:
764+
mainRes: "./main/res/figma/color/base"
765+
colors:
766+
themeAttributes:
767+
enabled: true
768+
attrsFile: "../../../values/attrs.xml"
769+
stylesFile: "../../../values/styles.xml"
770+
stylesNightFile: "../../../values-night/styles.xml"
771+
themeName: "Theme.BaseTheme.inDrive"
772+
markerStart: "FIGMA COLORS MARKER START"
773+
markerEnd: "FIGMA COLORS MARKER END"
774+
autoCreateMarkers: false
775+
nameTransform:
776+
style: PascalCase
777+
prefix: "color"
778+
stripPrefixes: ["extensions_", "information_", "statement_", "additional_"]
779+
```
780+
781+
### Configuration Options
782+
783+
| Field | Required | Default | Description |
784+
| ------------------- | -------- | ----------------------------- | ------------------------------------------ |
785+
| `enabled` | No | `false` | Enable theme attributes export |
786+
| `attrsFile` | Yes\* | — | Path to attrs.xml (relative to `mainRes`) |
787+
| `stylesFile` | Yes\* | — | Path to styles.xml (relative to `mainRes`) |
788+
| `stylesNightFile` | No | — | Path to styles-night.xml for dark mode |
789+
| `themeName` | Yes | — | Theme name used in markers |
790+
| `markerStart` | No | `"FIGMA COLORS MARKER START"` | Custom marker start text |
791+
| `markerEnd` | No | `"FIGMA COLORS MARKER END"` | Custom marker end text |
792+
| `autoCreateMarkers` | No | `false` | Auto-create files with markers if missing |
793+
| `nameTransform` | No | — | Name transformation settings |
794+
795+
\* Required when `enabled: true`
796+
797+
### Name Transformation
798+
799+
The `nameTransform` section controls how color names are converted to theme attribute names:
800+
801+
| Setting | Default | Description |
802+
| --------------- | ------------ | ----------------------------------------------- |
803+
| `style` | `PascalCase` | Target case style (camelCase, PascalCase, etc.) |
804+
| `prefix` | `"color"` | Prefix added to all attribute names |
805+
| `stripPrefixes` | `[]` | Prefixes to remove from color names |
806+
807+
**Examples:**
808+
809+
| Original Name | With `stripPrefixes: ["extensions_"]` | Result |
810+
| ----------------------------- | ------------------------------------- | ------------------------- |
811+
| `background_primary` | No strip | `colorBackgroundPrimary` |
812+
| `extensions_background_error` | Strips `extensions_` | `colorBackgroundError` |
813+
| `text_and_icon_primary` | No strip | `colorTextAndIconPrimary` |
814+
815+
### Marker-Based Updates
816+
817+
ExFig uses XML comment markers to update only specific sections of your files. This allows you to:
818+
819+
- Keep manual content outside markers
820+
- Have multiple themes in the same file (each with its own markers)
821+
- Safely run exports without losing custom code
822+
823+
**Marker format:**
824+
825+
```xml
826+
<!-- MARKER_START: ThemeName -->
827+
... generated content ...
828+
<!-- MARKER_END: ThemeName -->
829+
```
830+
831+
**Multiple themes in one file:**
832+
833+
```xml
834+
<resources>
835+
<!-- FIGMA COLORS MARKER START: Theme.Light -->
836+
<attr name="colorBackground" format="color" />
837+
<!-- FIGMA COLORS MARKER END: Theme.Light -->
838+
839+
<!-- FIGMA COLORS MARKER START: Theme.Dark -->
840+
<attr name="colorBackground" format="color" />
841+
<!-- FIGMA COLORS MARKER END: Theme.Dark -->
842+
</resources>
843+
```
844+
845+
### Batch Mode
846+
847+
When running `exfig batch`, theme attributes from multiple configs can target the same `attrs.xml` and `styles.xml`
848+
files. ExFig automatically:
849+
850+
1. Collects all theme attributes during batch processing
851+
2. Groups by target file
852+
3. Updates each theme's marker section separately
853+
4. Writes merged results after all configs complete
854+
855+
This ensures no race conditions and proper merging of contributions from different configs.
856+
857+
### Error Handling
858+
859+
| Error | Cause | Solution |
860+
| -------------------- | -------------------------------------- | ----------------------------------------------- |
861+
| File not found | Target file doesn't exist | Create file manually or use `autoCreateMarkers` |
862+
| Marker not found | Markers missing in target file | Add markers manually or use `autoCreateMarkers` |
863+
| Markers out of order | End marker appears before start marker | Fix marker order in file |
864+
865+
When `autoCreateMarkers: true`, ExFig creates missing files with a minimal template containing the markers.
866+
691867
## CLI Options for Version Tracking
692868

693869
In addition to the YAML configuration, you can control version tracking via CLI flags. Version tracking works for all

Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let package = Package(
1212
.executable(name: "exfig", targets: ["ExFig"]),
1313
],
1414
dependencies: [
15+
.package(url: "https://github.com/apple/swift-collections", "1.2.0" ..< "1.3.0"),
1516
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
1617
.package(url: "https://github.com/jpsim/Yams.git", from: "5.3.0"),
1718
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"),
@@ -81,7 +82,13 @@ let package = Package(
8182
// Exports resources to Android project
8283
.target(
8384
name: "AndroidExport",
84-
dependencies: ["ExFigCore", "SVGKit", "Stencil", "StencilSwiftKit"],
85+
dependencies: [
86+
"ExFigCore",
87+
"SVGKit",
88+
"Stencil",
89+
"StencilSwiftKit",
90+
.product(name: "OrderedCollections", package: "swift-collections"),
91+
],
8592
resources: [
8693
.copy("Resources/"),
8794
]
@@ -143,7 +150,9 @@ let package = Package(
143150
.testTarget(
144151
name: "AndroidExportTests",
145152
dependencies: [
146-
"AndroidExport", .product(name: "CustomDump", package: "swift-custom-dump"),
153+
"AndroidExport",
154+
.product(name: "CustomDump", package: "swift-custom-dump"),
155+
.product(name: "OrderedCollections", package: "swift-collections"),
147156
]
148157
),
149158
.testTarget(

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![CI](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml)
66
[![Release](https://github.com/alexey1312/ExFig/actions/workflows/release.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/release.yml)
77
[![Docs](https://github.com/alexey1312/ExFig/actions/workflows/deploy-docc.yml/badge.svg)](https://alexey1312.github.io/ExFig/documentation/exfig)
8-
![Coverage](https://img.shields.io/badge/coverage-49.56%25-yellow)
8+
![Coverage](https://img.shields.io/badge/coverage-49.33%25-yellow)
99
[![License](https://img.shields.io/github/license/alexey1312/ExFig.svg)](LICENSE)
1010

1111
Command-line utility to export colors, typography, icons, and images from Figma to Xcode, Android Studio, Flutter, and

0 commit comments

Comments
 (0)