Skip to content

Commit a873b05

Browse files
Initial release: modifier-node-cookbook
A curated collection of canonical Jetpack Compose modifiers, every one written using the Modifier.Node API. No Modifier.composed anywhere. Standalone (20): pressScale, shimmer, skeleton, hoverElevation, fadeScrollEdges, parallax, tilt, longPressProgress, dropShadow, gradientBorder, colorPulse, shake, glassmorphism, bounceOnAppear, revealOnScroll, marquee, swipeToDismiss, magnetic, pinchZoom, dragToReorder. Composites (2): interactiveCard (pressScale + tilt + hoverElevation), loadingPlaceholder (skeleton + shimmer). Cookbook library + sample app + MkDocs Material site + CI workflows + Maven Central publishing config.
0 parents  commit a873b05

124 files changed

Lines changed: 8049 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 4
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
max_line_length = 140
11+
12+
[*.{kt,kts}]
13+
ij_kotlin_allow_trailing_comma = true
14+
ij_kotlin_allow_trailing_comma_on_call_site = true
15+
ij_kotlin_imports_layout = *
16+
ktlint_standard_no-wildcard-imports = disabled
17+
ktlint_standard_filename = disabled
18+
ktlint_standard_function-naming = disabled
19+
ktlint_standard_chain-method-continuation = disabled
20+
ktlint_standard_property-naming = disabled
21+
22+
[*.{yml,yaml,md}]
23+
indent_size = 2
24+
25+
[Makefile]
26+
indent_style = tab

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
build:
15+
name: Build & test
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Set up JDK 17
24+
uses: actions/setup-java@v4
25+
with:
26+
distribution: temurin
27+
java-version: 17
28+
29+
- name: Set up Gradle
30+
uses: gradle/actions/setup-gradle@v4
31+
32+
- name: Validate Gradle wrapper
33+
uses: gradle/actions/wrapper-validation@v4
34+
35+
- name: Build, lint, ktlint, detekt, unit tests
36+
run: ./gradlew build --stacktrace

.github/workflows/docs.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'docs/**'
8+
- 'mkdocs.yml'
9+
- '.github/workflows/docs.yml'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
deploy:
17+
name: Deploy MkDocs site
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: '3.12'
27+
28+
- name: Install MkDocs Material
29+
run: |
30+
python -m pip install --upgrade pip
31+
pip install \
32+
mkdocs-material==9.5.40 \
33+
mkdocs-minify-plugin==0.8.0 \
34+
pymdown-extensions==10.11
35+
36+
- name: Build site
37+
run: mkdocs build --strict --verbose
38+
39+
- name: Deploy
40+
run: mkdocs gh-deploy --force --verbose

.github/workflows/publish.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: 'Version to publish (e.g. 0.1.0)'
11+
required: true
12+
13+
jobs:
14+
publish:
15+
name: Publish to Maven Central
16+
runs-on: ubuntu-latest
17+
environment: maven-central
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Set up JDK 17
25+
uses: actions/setup-java@v4
26+
with:
27+
distribution: temurin
28+
java-version: 17
29+
30+
- name: Set up Gradle
31+
uses: gradle/actions/setup-gradle@v4
32+
33+
- name: Resolve version from tag or workflow input
34+
id: version
35+
run: |
36+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
37+
echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
38+
else
39+
# Strip leading 'v' from the tag
40+
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
41+
fi
42+
43+
- name: Override VERSION_NAME with release version (drops -SNAPSHOT)
44+
run: |
45+
sed -i "s|^VERSION_NAME=.*|VERSION_NAME=${{ steps.version.outputs.version }}|" gradle.properties
46+
grep "^VERSION_NAME=" gradle.properties
47+
48+
- name: Publish to Central Portal
49+
env:
50+
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
51+
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
52+
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
53+
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }}
54+
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}
55+
run: ./gradlew :cookbook:publishAndReleaseToMavenCentral --stacktrace --no-configuration-cache
56+
57+
- name: Create GitHub release
58+
if: startsWith(github.ref, 'refs/tags/')
59+
uses: softprops/action-gh-release@v2
60+
with:
61+
generate_release_notes: true
62+
name: ${{ steps.version.outputs.version }}

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.gradle/
2+
build/
3+
out/
4+
.idea/
5+
*.iml
6+
local.properties
7+
captures/
8+
.cxx/
9+
.kotlin/
10+
*.hprof
11+
.DS_Store
12+
gradle/wrapper/gradle-wrapper.jar.lock
13+
.gradle-cache/
14+
15+
# Paparazzi failure artifacts
16+
**/src/test/snapshots/failures/
17+
18+
# Secrets
19+
*.gpg
20+
*.asc
21+
*.jks
22+
*.keystore
23+
24+
# IDE
25+
.vscode/
26+
*.swp

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4+
5+
## [0.1.0] - 2026-04-27
6+
7+
Initial release. Twenty production-ready standalone modifiers and two composites, all built on `Modifier.Node`. No `Modifier.composed` anywhere.
8+
9+
**Standalone**
10+
11+
- `Modifier.pressScale(scale, animationSpec)`: scales the receiver down on press, springs back.
12+
- `Modifier.shimmer(colors, angleDegrees, durationMillis, repeatMode)`: animated gradient sweep.
13+
- `Modifier.skeleton(shape, color)`: opaque placeholder shape.
14+
- `Modifier.hoverElevation(restingDp, hoveredDp, shape, animationSpec)`: animated elevation on hover.
15+
- `Modifier.fadeScrollEdges(scrollState, fadeLength)`: leading/trailing fade based on scroll.
16+
- `Modifier.parallax(scrollState, factor)`: child translates relative to a parent scroll.
17+
- `Modifier.tilt(maxAngleDeg, animationSpec)`: pointer-following 3D tilt.
18+
- `Modifier.longPressProgress(durationMs, strokeWidth, color, onComplete)`: radial hold progress.
19+
- `Modifier.dropShadow(color, blur, offset, shape)`: custom drop shadow untied from Material elevation.
20+
- `Modifier.gradientBorder(brush, width, animationSpec)`: animated gradient stroke around the receiver.
21+
- `Modifier.colorPulse(colors, durationMs)`: periodic color cycling for attention/badges.
22+
- `Modifier.shake(trigger, intensity, durationMs)`: single-shot horizontal shake driven by a trigger key.
23+
- `Modifier.glassmorphism(blurRadius, tint)`: frosted glass. `RenderEffect` blur on API 31+, translucent tint fallback below.
24+
- `Modifier.bounceOnAppear(initialScale, spring)`: single-shot entry bounce when the node first attaches.
25+
- `Modifier.revealOnScroll(translationY, threshold, durationMs)`: fade + translate in as the element enters the viewport.
26+
- `Modifier.marquee(direction, gap, fadeEdges, fadeColor, velocity)`: auto-scroll on overflow with edge fades and configurable velocity.
27+
- `Modifier.swipeToDismiss(onDismiss, threshold, axis)`: drag past threshold dismisses, otherwise springs back.
28+
- `Modifier.magnetic(anchors, snapThreshold)`: drag snaps to the nearest anchor on release.
29+
- `Modifier.pinchZoom(minScale, maxScale)`: two-finger zoom with pan support, pivots around the gesture centroid.
30+
- `Modifier.dragToReorder(state, key)`: gesture-driven reorder for list items, paired with a `ReorderableState` helper.
31+
32+
**Composite**
33+
34+
- `Modifier.interactiveCard(...)`: `pressScale + tilt + hoverElevation` with sensible defaults.
35+
- `Modifier.loadingPlaceholder(shape)`: `skeleton + shimmer` in one call.
36+
37+
Sample app with one screen per modifier. MkDocs Material doc site. CI on PRs, doc deploy on `main`, Maven Central publish on tag.
38+
39+
[0.1.0]: https://github.com/ChristophyBarth/modifier-node-cookbook/releases/tag/v0.1.0

CONTRIBUTING.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Contributing
2+
3+
Thanks for considering a contribution. The cookbook has a deliberately narrow scope: a small, curated set of canonical Compose modifiers, all built on `Modifier.Node`. We'd rather ship 12 great modifiers than 50 mediocre ones.
4+
5+
## Before you start
6+
7+
- Open an issue first if you're proposing a new modifier. We'll talk through scope, name, and whether it belongs in `cookbook` (general-purpose) or somewhere else.
8+
- Bug fixes and doc improvements: jump straight to a PR.
9+
10+
## The bar for a new modifier
11+
12+
Every modifier in the cookbook ships:
13+
14+
1. **A `Modifier.xxx(...)` extension** that validates inputs and returns `this then SomeElement(...)`.
15+
2. **A `ModifierNodeElement` subclass** as a `data class` (so `equals`/`hashCode` are correct), with a meaningful `update()` that mutates the existing node, never recreates.
16+
3. **A node class** mixing in the right `Modifier.Node` mixin(s). Pick the cheapest mixin that does the job.
17+
4. **KDoc** on the public function: paragraph description, parameter docs, a `@sample` reference to a sample function in `cookbook/.../samples/`.
18+
5. **A unit test** in `cookbook/src/test/...`. Use `createComposeRule` + Robolectric. Cover at minimum: parameter validation, parameter-change-without-recreation, and one behaviour assertion.
19+
6. **A sample-app screen** in `sample/.../screens/` and entry in `CookbookCatalog.catalogEntries()`.
20+
7. **A doc page** in `docs/modifiers/<name>.md` with usage, parameters, and design notes (which node subtype, why, any subtleties).
21+
22+
## Quality gates
23+
24+
The CI build must be green. Locally:
25+
26+
```bash
27+
./gradlew build
28+
```
29+
30+
That runs `ktlint`, `detekt`, lint, unit tests, and assembles both modules.
31+
32+
Conventions:
33+
34+
- **No `Modifier.composed`** anywhere in `cookbook/`. That's the entire point of the project.
35+
- **No `LaunchedEffect`** in modifier impls; use the node's `coroutineScope`.
36+
- **No reflection.** No `kotlin-reflect`, no `Class.forName`.
37+
- **Public API is `@Stable`** (or `@Immutable` for value classes).
38+
- **`explicitApi()` is on** in `cookbook`: every public symbol needs an explicit visibility modifier.
39+
- Animations driven by `Animatable` / `AnimationState`, cancelled implicitly by node detach.
40+
41+
## Repo secrets (for maintainers)
42+
43+
Publishing to Maven Central is wired through GitHub Actions and reads these repo secrets:
44+
45+
| Secret | Purpose |
46+
|---|---|
47+
| `MAVEN_CENTRAL_USERNAME` | Central Portal user token name |
48+
| `MAVEN_CENTRAL_PASSWORD` | Central Portal user token password |
49+
| `SIGNING_IN_MEMORY_KEY` | ASCII-armored private GPG key (`gpg --armor --export-secret-keys <KEY_ID>`) |
50+
| `SIGNING_IN_MEMORY_KEY_ID` | The 8-character short key ID (e.g. `0A1B2C3D`) |
51+
| `SIGNING_IN_MEMORY_KEY_PASSWORD` | Passphrase for the GPG key |
52+
53+
Pushing a `vX.Y.Z` tag triggers `.github/workflows/publish.yml`, which strips `-SNAPSHOT` from `gradle.properties`, signs, and uploads to the Central Portal staging bundle (auto-released).
54+
55+
## Style
56+
57+
- Kotlin official style. ktlint enforces.
58+
- `data class` for elements; small `internal class` for the node.
59+
- Names: file = `Xxx.kt` (PascalCase), package = `xxx` (all lowercase, no separators), public function = `Modifier.xxx` (camelCase).
60+
- Tests: `xxx_does_yyy()` (snake_case method names; backticks-with-spaces are nicer to read but ktlint and the Robolectric runner complain about characters like `[`/`]`).
61+
62+
## License
63+
64+
By contributing you agree your contribution is licensed under the Apache 2.0 License.

0 commit comments

Comments
 (0)