Compare commits

1 Commits

Author SHA1 Message Date
rodydavis bc6fb3cce2 Add renovate.json 2026-06-13 06:29:39 +00:00
16 changed files with 3 additions and 654 deletions
-2
View File
@@ -12,7 +12,6 @@ npx skills add rodydavis/skills
| [async-preact-signals](./skills/async-preact-signal/SKILL.md) | Explore how to effectively manage asynchronous data with Preact Signals by creating a custom `asyncSignal` that handles loading, error, and data states without breaking the synchronous nature of signals. |
| [automate-flutter-app-releases](./skills/automate-flutter-apps/SKILL.md) | Automate your Flutter app releases to beta or production with this handy shell script that handles version bumping, formatting, cleaning, rebuilding, and deployment via Fastlane. |
| [dynamic-themes-with-codemirror](./skills/codemirror-dynamic-theme/SKILL.md) | Learn how to create a Lit web component with CodeMirror, dynamically themed using Material Design's color utilities, for a customizable code editing experience. |
| [create-flutter-app-native-ui](./skills/create-flutter-app-native-ui/SKILL.md) | Guide to bootstrap, coordinate, and maintain a headless Flutter engine integrated with 100% native platforms (SwiftUI, Jetpack Compose, HTML5 Web Components). |
| [how-to-create-html-web-components-with-dart](./skills/dart-html-web-components/SKILL.md) | Discover the power of Web Components and how to build them with both JavaScript and Dart for reusable, framework-agnostic UI elements. |
| [how-to-do-bitwise-operations-in-dart](./skills/dart_bitwise/SKILL.md) | Explore Dart's bitwise operations for both integers and booleans, including AND, OR (inclusive & exclusive), NAND, NOR, and XNOR, with practical code examples. |
| [various-ways-to-invoke-functions-in-dart](./skills/dart_function-invoking/SKILL.md) | Discover the surprising flexibility of calling Dart functions, including mixed positional and named arguments, the `.call` operator, and dynamic invocation with `Function.apply`. |
@@ -20,7 +19,6 @@ npx skills add rodydavis/skills
| [check-if-an-object-is-truthy-in-dart](./skills/dart_truthy/SKILL.md) | Learn how to extend Dart's functionality to implement JavaScript-style "truthy" checks for easier conditional logic and value evaluations. |
| [deep-linking-for-flutter-web](./skills/deep-linking-flutter-web/SKILL.md) | Learn how to implement proper URL navigation in your Flutter application, including deep linking to specific pages, handling protected routes, and creating custom transitions for a seamless user experience. |
| [displaying-html-in-flutter](./skills/display-html-in-flutter/SKILL.md) | Easily display and interact with HTML content in your Flutter app using the `easy_web_view` package, which supports both web and mobile platforms. |
| [dotfiles-management](./skills/dotfiles/SKILL.md) | Guidelines and rules for managing the user's dotfiles repository, config files, and installation scripts. |
| [how-to-export-sqlite-tables-to-create-statements](./skills/export-sqlite-dart/SKILL.md) | Learn how to export your entire SQLite database schema, including tables and indexes, into runnable CREATE statements at runtime using Flutter and the `sqlite3` package. |
| [using-fastlane-in-flutter-and-ci](./skills/fastlane-and-flutter/SKILL.md) | Automate Flutter app builds and deployments to both the App Store and Google Play using Fastlane with this step-by-step guide. |
| [lit-and-figma](./skills/figma-and-lit/SKILL.md) | Learn how to create a Figma plugin using Lit web components, including project setup, component creation, WASM integration, and building the final plugin for use in Figma. |
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
@@ -1,94 +0,0 @@
---
name: create-flutter-app-native-ui
description: Guide to bootstrap, coordinate, and maintain a headless Flutter engine integrated with 100% native platforms (SwiftUI, Jetpack Compose, HTML5 Web Components).
---
# Creating a Flutter App with 100% Native UIs
This skill provides step-by-step guidance on implementing a decoupled architectural design where a **headless Flutter engine** acts as the shared single source of truth (SSOT) running in the background, while the user interfaces are built using platform-native toolkits.
```mermaid
graph TD
subgraph Native Frontends
iOS[SwiftUI View]
Android[Jetpack Compose]
Web[Web Components / JS]
end
subgraph Headless Flutter Context
Engine[Flutter Engine]
StateManager[State Manager / Tasks]
Bridge[Platform Channels / Interop]
end
iOS <-->|Pigeon IPC| Bridge
Android <-->|Pigeon IPC| Bridge
Web <-->|dart:js_interop| Bridge
Bridge <--> StateManager
```
---
## 1. Interop Layer Setup
### Mobile (Pigeon Code-Gen)
Pigeon is used for type-safe IPC communication between Dart and Swift/Kotlin.
Create a pigeon definition file at `pigeons/tasks_api.dart`:
- Use `@HostApi` for calls originating from native code that Dart answers.
- Use `@FlutterApi` for calls originating from Dart that native platforms listen to.
Compile the interfaces using the Pigeon CLI:
```bash
dart run pigeon --input pigeons/tasks_api.dart
```
### Web (JS Interop)
Use `dart:js_interop` to bind functions to the global `window` object:
- Define `@JS('window.methodName')` setters to bind Dart triggers.
- Define `@JS('window.callbackName')` getters to notify host JS components of updates.
---
## 2. Bootstrapping Headless Engines
### iOS (AppDelegate.swift)
Instantiate `FlutterEngine` without a visual viewport controller:
1. Declare `var flutterEngine: FlutterEngine?`.
2. Instantiate `let engine = FlutterEngine(name: "my-engine")` and call `engine.run()`.
3. Set up the custom event handler store (`TaskStore`) to listen on the binary messenger:
`store.setup(binaryMessenger: engine.binaryMessenger)`.
4. Wrap your SwiftUI view using `UIHostingController` and present it in the main `UIWindow`.
### Android (MainActivity.kt)
Instantiate a headless `FlutterEngine` inside a standard Compose activity:
1. Override `onCreate` in a subclass of `ComponentActivity`.
2. Instantiate `FlutterEngine(this)` and invoke `executeDartEntrypoint(...)`.
3. Connect your Pigeon host/client endpoints using the engine's `dartExecutor.binaryMessenger`.
4. Set up Compose UI using `setContent { ... }`.
5. Call `flutterEngine.destroy()` on `onDestroy()`.
### Web (flutter_bootstrap.js)
Boot Flutter Web headlessly by specifying a hidden target container:
1. Create a `flutter_bootstrap.js` mapping:
```javascript
engineInitializer.initializeEngine({
hostElement: document.getElementById('flutter-host') // Styled with display: none
})
```
---
## 3. UI Synchronization & State Management
Native clients should observe updates from the headless engine reactively:
* **iOS**: Implement `ObservableObject` and dispatch events to SwiftUI views via `@Published` properties on `DispatchQueue.main.async`.
* **Android**: Use a Kotlin `StateFlow` updated via the Pigeon host API callback. Connect Compose views using `.collectAsState()`.
* **Web**: Create custom HTML5 Web Components dispatching custom bubbles events (`toggle-task`, `delete-task`) up the DOM hierarchy.
---
## 4. Resource & Verification Scripts
The resource folder contains CLI compilation check scripts:
- `resources/check_ios.sh`: Validates Swift compilation and framework linkage.
- `resources/check_android.sh`: Triggers Kotlin compile tasks to catch build errors before simulator deployment.
@@ -1,64 +0,0 @@
package com.example.tasks_interop
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.collectAsState
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainActivity : ComponentActivity(), TasksHostApi {
private lateinit var flutterEngine: FlutterEngine
private lateinit var flutterApi: TasksFlutterApi
private val _tasks = MutableStateFlow<List<TaskData>>(emptyList())
val tasks: StateFlow<List<TaskData>> = _tasks.asStateFlow()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
flutterEngine = FlutterEngine(this)
TasksHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this)
flutterApi = TasksFlutterApi(flutterEngine.dartExecutor.binaryMessenger)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
flutterApi.getTasks { result ->
if (result.isSuccess) {
_tasks.value = result.getOrNull() ?: emptyList()
}
}
setContent {
val tasksState = tasks.collectAsState()
TasksApp(
tasks = tasksState.value,
onAddTask = { title ->
flutterApi.addTask(title) { }
},
onToggleTask = { id ->
flutterApi.toggleTask(id) { }
},
onDeleteTask = { id ->
flutterApi.deleteTask(id) { }
}
)
}
}
override fun onTasksUpdated(tasks: List<TaskData>) {
_tasks.value = tasks
}
override fun onDestroy() {
super.onDestroy()
TasksHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, null)
flutterEngine.destroy()
}
}
@@ -1,82 +0,0 @@
package com.example.tasks_interop
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TasksApp(
tasks: List<TaskData>,
onAddTask: (String) -> Unit,
onToggleTask: (String) -> Unit,
onDeleteTask: (String) -> Unit
) {
var newTaskTitle by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = newTaskTitle,
onValueChange = { newTaskTitle = it },
modifier = Modifier.weight(1f),
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (newTaskTitle.isNotBlank()) {
onAddTask(newTaskTitle.trim())
newTaskTitle = ""
}
}
) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
LazyColumn {
items(tasks, key = { it.id }) { task ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = task.isCompleted,
onCheckedChange = { onToggleTask(task.id) }
)
Text(
text = task.title,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { onDeleteTask(task.id) }) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
}
}
}
}
}
@@ -1,29 +0,0 @@
import Flutter
import UIKit
import SwiftUI
@main
@objc class AppDelegate: FlutterAppDelegate {
var flutterEngine: FlutterEngine?
let taskStore = TaskStore()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let engine = FlutterEngine(name: "headless-tasks-engine")
engine.run()
self.flutterEngine = engine
GeneratedPluginRegistrant.register(with: engine)
taskStore.setup(binaryMessenger: engine.binaryMessenger)
let window = UIWindow(frame: UIScreen.main.bounds)
let contentView = ContentView(store: taskStore)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -1,70 +0,0 @@
import SwiftUI
struct ContentView: View {
@ObservedObject var store: TaskStore
@State private var newTaskTitle: String = ""
var body: some View {
NavigationView {
VStack(spacing: 0) {
if store.tasks.isEmpty {
VStack(spacing: 20) {
Image(systemName: "checklist")
.font(.system(size: 80))
.foregroundColor(.indigo.opacity(0.4))
Text("No Tasks Yet")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.primary)
}
.frame(maxHeight: .infinity)
} else {
List {
ForEach(store.tasks) { task in
TaskRow(task: task) {
store.toggleTask(id: task.id)
}
}
.onDelete(perform: deleteTasks)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.listStyle(.plain)
}
Divider()
HStack(spacing: 12) {
TextField("Enter task title...", text: $newTaskTitle)
.padding(12)
.background(Color(.secondarySystemBackground))
.cornerRadius(10)
.onSubmit(addTask)
Button(action: addTask) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 38))
.foregroundColor(.indigo)
}
.disabled(newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
.padding()
}
.navigationTitle("Tasks")
}
}
private func addTask() {
let trimmed = newTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
store.addTask(title: trimmed)
newTaskTitle = ""
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
let task = store.tasks[index]
store.deleteTask(id: task.id)
}
}
}
@@ -1,29 +0,0 @@
import SwiftUI
extension TaskData: Identifiable {}
struct TaskRow: View {
let task: TaskData
let onToggle: () -> Void
var body: some View {
HStack(spacing: 16) {
Button(action: onToggle) {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.system(size: 24))
.foregroundColor(task.isCompleted ? .indigo : .gray)
}
.buttonStyle(.plain)
Text(task.title)
.strikethrough(task.isCompleted, color: .gray)
.foregroundColor(task.isCompleted ? .gray : .primary)
Spacer()
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(Color(.systemBackground))
.cornerRadius(12)
}
}
@@ -1,37 +0,0 @@
import Foundation
import Flutter
class TaskStore: NSObject, ObservableObject, TasksHostApi {
@Published var tasks: [TaskData] = []
private var flutterApi: TasksFlutterApi?
func setup(binaryMessenger: FlutterBinaryMessenger) {
TasksHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: self)
self.flutterApi = TasksFlutterApi(binaryMessenger: binaryMessenger)
self.flutterApi?.getTasks { [weak self] result in
if case .success(let list) = result {
DispatchQueue.main.async {
self?.tasks = list
}
}
}
}
func onTasksUpdated(tasks: [TaskData]) throws {
DispatchQueue.main.async {
self.tasks = tasks
}
}
func addTask(title: String) {
flutterApi?.addTask(title: title) { _ in }
}
func toggleTask(id: String) {
flutterApi?.toggleTask(id: id) { _ in }
}
func deleteTask(id: String) {
flutterApi?.deleteTask(id: id) { _ in }
}
}
@@ -1,33 +0,0 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/src/tasks_api.g.dart',
swiftOut: 'ios/Runner/TasksApi.g.swift',
kotlinOut: 'android/app/src/main/kotlin/com/example/tasks_interop/TasksApi.g.kt',
kotlinOptions: KotlinOptions(package: 'com.example.tasks_interop'),
))
class TaskData {
final String id;
final String title;
final bool isCompleted;
TaskData({
required this.id,
required this.title,
required this.isCompleted,
});
}
@HostApi()
abstract class TasksHostApi {
void onTasksUpdated(List<TaskData> tasks);
}
@FlutterApi()
abstract class TasksFlutterApi {
List<TaskData> getTasks();
void addTask(String title);
void toggleTask(String id);
void deleteTask(String id);
}
@@ -1,98 +0,0 @@
class TaskItem extends HTMLElement {
constructor() {
super();
this._id = '';
this._title = '';
this._completed = false;
}
connectedCallback() {
this.render();
}
static get observedAttributes() {
return ['task-id', 'title', 'completed'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'task-id') this._id = newValue;
if (name === 'title') this._title = newValue;
if (name === 'completed') this._completed = newValue === 'true';
this.render();
}
render() {
this.innerHTML = `
<div>
<input type="checkbox" ${this._completed ? 'checked' : ''}>
<span>${this._title}</span>
<button class="delete-btn">Delete</button>
</div>
`;
this.querySelector('input').addEventListener('change', () => {
this.dispatchEvent(new CustomEvent('toggle-task', {
bubbles: true,
detail: { id: this._id }
}));
});
this.querySelector('.delete-btn').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('delete-task', {
bubbles: true,
detail: { id: this._id }
}));
});
}
}
customElements.define('task-item', TaskItem);
class TaskApp extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div>
<form id="task-form">
<input type="text" placeholder="Add a new task..." required>
<button type="submit">Add</button>
</form>
<div id="task-list"></div>
</div>
`;
this.querySelector('#task-form').addEventListener('submit', (e) => {
e.preventDefault();
const val = this.querySelector('input').value.trim();
if (val && window.addTaskOnWeb) {
window.addTaskOnWeb(val);
this.querySelector('input').value = '';
}
});
this.addEventListener('toggle-task', (e) => {
if (window.toggleTaskOnWeb) window.toggleTaskOnWeb(e.detail.id);
});
this.addEventListener('delete-task', (e) => {
if (window.deleteTaskOnWeb) window.deleteTaskOnWeb(e.detail.id);
});
}
setTasks(tasks) {
const list = this.querySelector('#task-list');
list.innerHTML = '';
tasks.forEach(t => {
const item = document.createElement('task-item');
item.setAttribute('task-id', t.id);
item.setAttribute('title', t.title);
item.setAttribute('completed', t.isCompleted ? 'true' : 'false');
list.appendChild(item);
});
}
}
customElements.define('task-app', TaskApp);
window.onTasksUpdatedOnWeb = function(tasks) {
const app = document.querySelector('task-app');
if (app) app.setTasks(tasks);
};
@@ -1,12 +0,0 @@
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine({
hostElement: document.getElementById('flutter-host')
}).then(function(appRunner) {
appRunner.runApp();
});
}
});
@@ -1,33 +0,0 @@
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
echo -e "${YELLOW}Starting Android Kotlin compilation check...${NC}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$(dirname "$(dirname "$SCRIPT_DIR")")")"
ANDROID_DIR="$PROJECT_ROOT/android"
if [ ! -d "$ANDROID_DIR" ]; then
echo -e "${RED}Error: android directory not found at $ANDROID_DIR${NC}"
exit 1
fi
echo "Running gradle compileDebugKotlin..."
set +e
"$ANDROID_DIR/gradlew" -p "$ANDROID_DIR" compileDebugKotlin --quiet
RESULT=$?
set -e
if [ $RESULT -eq 0 ]; then
echo -e "${GREEN}✓ Android Compilation Succeeded! No errors found.${NC}"
exit 0
else
echo -e "${RED}✗ Android compilation failed with exit code $RESULT.${NC}"
exit $RESULT
fi
@@ -1,38 +0,0 @@
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
echo -e "${YELLOW}Starting iOS compilation check...${NC}"
if [ -d "/Applications/Xcode-16.2.app" ]; then
echo "Using Xcode 16.2 toolchain..."
export DEVELOPER_DIR="/Applications/Xcode-16.2.app/Contents/Developer"
else
echo "Using system default Xcode toolchain..."
fi
echo "Running xcodebuild..."
set +e
xcodebuild \
-workspace ios/Runner.xcworkspace \
-scheme Runner \
-configuration Debug \
-sdk iphonesimulator \
build \
-quiet
RESULT=$?
set -e
if [ $RESULT -eq 0 ]; then
echo -e "${GREEN}✓ iOS Compilation Succeeded! No errors found.${NC}"
exit 0
else
echo -e "${YELLOW}Xcode build finished with code $RESULT.${NC}"
echo "If the only failing step in the output is 'CompileAssetCatalog', your Swift and interop codebase compiles successfully."
exit $RESULT
fi
-33
View File
@@ -1,33 +0,0 @@
---
name: dotfiles-management
description: Guidelines and rules for managing the user's dotfiles repository, config files, and installation scripts.
---
# Dotfiles Management Skill
Use this skill when modifying system configurations, shell profiles, editor settings, or the installer scripts within the dotfiles repository.
## Repository Location
The dotfiles repository is stored at:
`/Users/rodydavis/dev/git/dotfiles`
## Rules
- When updating or modifying configuration files (like `.zshrc`, `.gitconfig`, `.gitignore_global`, or Zed settings), always make changes inside the dotfiles repository rather than directly editing files in the home directory (`~`).
- After modifying configurations, run the `./install.sh` script to link/sync them.
- Always run the installer in dry-run mode (`./install.sh --dry-run` or `-d`) first to verify changes.
## Bootstrapping & Installation Guidelines
### 1. SSH & Git Clones
* When cloning private repositories in the background, check if the host is in `~/.ssh/known_hosts` first. Use `ssh-keyscan` to populate it before running `git clone` to prevent the task from being suspended on interactive host key prompts.
### 2. Homebrew Bundle & Tap Trust
* **Tap Trust:** Modern Homebrew requires explicit tap trust approval. When running `brew bundle` in unattended background scripts, prepend `HOMEBREW_NO_REQUIRE_TAP_TRUST=1` to bypass interactive prompts.
* **Sudo Casks:** Avoid including casks that require administrative (`sudo`) privileges (e.g. `basictex`) in unattended installations. Comment them out in the `Brewfile` and instruct the user to install them manually in their terminal.
* **Deprecated/Private Taps:** Keep the `Brewfile` clean of deprecated taps (like `homebrew/cask-fonts`) or private/unauthorized taps (like `robotsandpencils/made`) to avoid failing the entire bundle installation.
### 3. Node.js/NPM Runtime
* Installing `nvm` via Homebrew does not install a Node runtime. Before executing global NPM package installations, initialize `nvm` and run `nvm install --lts` to verify a node/npm binary is active.
### 4. Java & Android SDK Configuration
* Homebrew's `openjdk` is keg-only. To ensure Java is detected by Android CLI and Flutter tools, configure `.zshrc` to export `/opt/homebrew/opt/openjdk/bin` in the `PATH` and set `JAVA_HOME` to the Homebrew JDK path.