Compare commits
20 Commits
2b880be833
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 09cbb308d2 | |||
| c120b3d4d2 | |||
| 3381945cea | |||
| d12aabdccc | |||
| d38a348a06 | |||
| 42873f4d2d | |||
| c1bb006292 | |||
| 9bdaaeb295 | |||
| 6596ec2d9b | |||
| 0103aa8c16 | |||
| 37b13cfd6a | |||
| 29d27dccfb | |||
| cca27aa971 | |||
| 75bbb322d3 | |||
| 1a79b5fa9f | |||
| 9288aef5c7 | |||
| 29a2dfc606 | |||
| 2999325de9 | |||
| 06584ffedc | |||
| 90f91bd017 |
@@ -1,7 +1,6 @@
|
||||
keys:
|
||||
- &host_tahani age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm
|
||||
- &host_michael age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j
|
||||
- &host_mindy age1dqt3znmzcgghsjjzzax0pf0eyu95h0p7kaf5v988ysjv7fl7lumsatl048
|
||||
- &host_jason age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2
|
||||
- &host_chidi age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3
|
||||
creation_rules:
|
||||
@@ -10,6 +9,5 @@ creation_rules:
|
||||
- age:
|
||||
- *host_tahani
|
||||
- *host_michael
|
||||
- *host_mindy
|
||||
- *host_jason
|
||||
- *host_chidi
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
### Local Development
|
||||
```bash
|
||||
nix run .#build # Build current host config
|
||||
nix run .#build -- <hostname> # Build specific host (chidi, jason, michael, mindy, tahani)
|
||||
nix run .#build -- <hostname> # Build specific host (chidi, jason, michael, tahani)
|
||||
nix run .#apply # Build and apply locally (darwin-rebuild/nixos-rebuild switch)
|
||||
nix flake check # Validate flake
|
||||
```
|
||||
@@ -13,7 +13,7 @@ nix flake check # Validate flake
|
||||
### Remote Deployment (NixOS only)
|
||||
```bash
|
||||
colmena build # Build all NixOS hosts
|
||||
colmena apply --on <host> # Deploy to specific NixOS host (michael, mindy, tahani)
|
||||
colmena apply --on <host> # Deploy to specific NixOS host (michael, tahani)
|
||||
colmena apply # Deploy to all NixOS hosts
|
||||
```
|
||||
|
||||
|
||||
90
flake.lock
generated
90
flake.lock
generated
@@ -183,11 +183,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768135262,
|
||||
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -254,11 +254,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770263241,
|
||||
"narHash": "sha256-R1WFtIvp38hS9x63dnijdJw1KyIiy30KGea6e6N7LHs=",
|
||||
"lastModified": 1770586272,
|
||||
"narHash": "sha256-Ucci8mu8QfxwzyfER2DQDbvW9t1BnTUJhBmY7ybralo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "04e5203db66417d548ae1ff188a9f591836dfaa7",
|
||||
"rev": "b1f916ba052341edc1f80d4b2399f1092a4873ca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -270,11 +270,11 @@
|
||||
"homebrew-cask": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770271594,
|
||||
"narHash": "sha256-XEPxvvmaoWyWCs82LZceFK5bLWIESfFDhFBQVUmK08Q=",
|
||||
"lastModified": 1770623639,
|
||||
"narHash": "sha256-LNLzbnhp5IEizTMMapF2FtLVD21sFzBfVgXcwNz7fKU=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-cask",
|
||||
"rev": "fc2b8497ec7b27da2f4f978ebfc4c5fa5597dbbc",
|
||||
"rev": "c3bb7aedf0881187cbeb55ad2873240feba21603",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -286,11 +286,11 @@
|
||||
"homebrew-core": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770273195,
|
||||
"narHash": "sha256-MI/KGf/RFUEFgq8ponNGHD2Nkx8a6b9GbTWuPK5WG2o=",
|
||||
"lastModified": 1770627860,
|
||||
"narHash": "sha256-ihOndNFECGtZhkrtynP8nDJ8fbSxhNd2zWcq3CLDnQA=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-core",
|
||||
"rev": "218d5a275335518e26a8b35dc857a8fef84e09cf",
|
||||
"rev": "a12e59e6d202fc64aee013f8574c043a4c00a271",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -306,11 +306,11 @@
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770263632,
|
||||
"narHash": "sha256-ooCSBCQyN1y4qMhEinfCL33ElxGo2IVEuxCm5+JLgHQ=",
|
||||
"lastModified": 1770616720,
|
||||
"narHash": "sha256-NY7yFg3ZG0fzseC4SK/TQjgaODczuvCDtJZNsBmN2QU=",
|
||||
"owner": "numtide",
|
||||
"repo": "llm-agents.nix",
|
||||
"rev": "042cf156c273d1abdf33198a4a49bf692d0bb635",
|
||||
"rev": "09019dadd541051fc11f5008b56f4e8a14d2df4c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -382,11 +382,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770169770,
|
||||
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
|
||||
"lastModified": 1770537093,
|
||||
"narHash": "sha256-pF1quXG5wsgtyuPOHcLfYg/ft/QMr8NnX0i6tW2187s=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "aa290c9891fa4ebe88f8889e59633d20cc06a5f2",
|
||||
"rev": "fef9403a3e4d31b0a23f0bacebbec52c248fbb51",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -413,11 +413,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1770279138,
|
||||
"narHash": "sha256-3qKiqBdGxo+E0noMoK9Po5L71Js4jbbnvrtg0Qqhw80=",
|
||||
"lastModified": 1770627848,
|
||||
"narHash": "sha256-pWVT4wjh+HKIdvGhph0vU1Kh48OSaSutPGpXxGNxSxw=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e9e02ba9d43c306cd4bc1c4b9192e7ac7a255228",
|
||||
"rev": "fe776c9fe2c37f51546bb50ced285ea2a365e7d9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -429,11 +429,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1768875095,
|
||||
"narHash": "sha256-dYP3DjiL7oIiiq3H65tGIXXIT1Waiadmv93JS0sS+8A=",
|
||||
"lastModified": 1770380644,
|
||||
"narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ed142ab1b3a092c4d149245d0c4126a5d7ea00b0",
|
||||
"rev": "ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -466,11 +466,11 @@
|
||||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770025103,
|
||||
"narHash": "sha256-Qlb19PP0n6s+v1MywVjROV5XwsCvA58XXiXHk0Govb4=",
|
||||
"lastModified": 1770627083,
|
||||
"narHash": "sha256-Js8WrUwQ3lLRjWb8jGGE5npRN96E4mtPwyuNDuCDkcg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "31c3b3687dc85e3fbbf5c44728a5ee231608f8a9",
|
||||
"rev": "d354487c4692de3d0918170c45bde05175b12e30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -482,11 +482,11 @@
|
||||
"nono": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770230267,
|
||||
"narHash": "sha256-EUSXFyhPBkffuukLvr4Q3LythNds2ca5wJotW0APSY8=",
|
||||
"lastModified": 1770553882,
|
||||
"narHash": "sha256-yEXw+rtuhoZvx1eO2Q+qPeGpvVbyASh7D9YEVAteoo8=",
|
||||
"owner": "lukehinds",
|
||||
"repo": "nono",
|
||||
"rev": "42df275918579adeb3243a9f9e085f052e19199c",
|
||||
"rev": "e80983bb6a4058335e96e02eeabe17314f771a9c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -495,14 +495,31 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"openusage": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770543295,
|
||||
"narHash": "sha256-DvgEPZhFm06igalUPgnQ8VLkl0gk/3rm+lbEJ2/s7gM=",
|
||||
"owner": "robinebers",
|
||||
"repo": "openusage",
|
||||
"rev": "22a7bd5f7856397400e60dd787ad82b23c763969",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "robinebers",
|
||||
"ref": "v0.5.1",
|
||||
"repo": "openusage",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"overseer": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1770238325,
|
||||
"narHash": "sha256-aLqorFsy0iSDbqAf316GXnZUopwsIlKXHjzmL8QRat8=",
|
||||
"lastModified": 1770303305,
|
||||
"narHash": "sha256-NM1haQAk1mWdmewgIv6tzApaIQxWKrIrri0+uXHY3Zc=",
|
||||
"owner": "dmmulroy",
|
||||
"repo": "overseer",
|
||||
"rev": "3edebbe09895a19190953408f7e22f92921e1aa6",
|
||||
"rev": "5880d97939744ff72eb552c671da2fae1789041e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -526,6 +543,7 @@
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixvim": "nixvim",
|
||||
"nono": "nono",
|
||||
"openusage": "openusage",
|
||||
"overseer": "overseer",
|
||||
"sops-nix": "sops-nix",
|
||||
"zjstatus": "zjstatus"
|
||||
@@ -576,11 +594,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770145881,
|
||||
"narHash": "sha256-ktjWTq+D5MTXQcL9N6cDZXUf9kX8JBLLBLT0ZyOTSYY=",
|
||||
"lastModified": 1770526836,
|
||||
"narHash": "sha256-xbvX5Ik+0inJcLJtJ/AajAt7xCk6FOCrm5ogpwwvVDg=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "17eea6f3816ba6568b8c81db8a4e6ca438b30b7c",
|
||||
"rev": "d6e0e666048a5395d6ea4283143b7c9ac704720d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
url = "github:dmmulroy/overseer";
|
||||
flake = false;
|
||||
};
|
||||
openusage = {
|
||||
url = "github:robinebers/openusage/v0.5.1";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs @ {flake-parts, ...}:
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
../../profiles/bash.nix
|
||||
../../profiles/bat.nix
|
||||
../../profiles/direnv.nix
|
||||
../../profiles/eza.nix
|
||||
../../profiles/fish.nix
|
||||
../../profiles/nushell.nix
|
||||
../../profiles/fzf.nix
|
||||
../../profiles/ghostty.nix
|
||||
../../profiles/git.nix
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
../../profiles/bash.nix
|
||||
../../profiles/bat.nix
|
||||
../../profiles/direnv.nix
|
||||
../../profiles/eza.nix
|
||||
../../profiles/fish.nix
|
||||
../../profiles/nushell.nix
|
||||
../../profiles/fzf.nix
|
||||
../../profiles/ghostty.nix
|
||||
../../profiles/git.nix
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
home-manager.users.${user} = {
|
||||
imports = [
|
||||
../../profiles/fish.nix
|
||||
../../profiles/nushell.nix
|
||||
../../profiles/home.nix
|
||||
../../profiles/ssh.nix
|
||||
inputs.nixvim.homeModules.nixvim
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
../../profiles/bash.nix
|
||||
../../profiles/bat.nix
|
||||
../../profiles/direnv.nix
|
||||
../../profiles/eza.nix
|
||||
../../profiles/fish.nix
|
||||
../../profiles/nushell.nix
|
||||
../../profiles/fzf.nix
|
||||
../../profiles/git.nix
|
||||
../../profiles/home.nix
|
||||
|
||||
132
overlays/openusage.nix
Normal file
132
overlays/openusage.nix
Normal file
@@ -0,0 +1,132 @@
|
||||
{inputs}: final: prev: let
|
||||
version = "0.5.1";
|
||||
in {
|
||||
openusage =
|
||||
prev.rustPlatform.buildRustPackage (finalAttrs: {
|
||||
pname = "openusage";
|
||||
inherit version;
|
||||
|
||||
src = inputs.openusage;
|
||||
|
||||
cargoRoot = "src-tauri";
|
||||
cargoLock = {
|
||||
lockFile = "${inputs.openusage}/src-tauri/Cargo.lock";
|
||||
outputHashes = {
|
||||
"tauri-nspanel-2.1.0" = "sha256-PLACEHOLDER";
|
||||
"tauri-plugin-aptabase-1.0.0" = "sha256-PLACEHOLDER";
|
||||
};
|
||||
};
|
||||
buildAndTestSubdir = finalAttrs.cargoRoot;
|
||||
|
||||
node_modules =
|
||||
prev.stdenv.mkDerivation {
|
||||
inherit (finalAttrs) src version;
|
||||
pname = "${finalAttrs.pname}-node_modules";
|
||||
|
||||
impureEnvVars =
|
||||
prev.lib.fetchers.proxyImpureEnvVars
|
||||
++ [
|
||||
"GIT_PROXY_COMMAND"
|
||||
"SOCKS_SERVER"
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
prev.bun
|
||||
prev.writableTmpDirAsHomeHook
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
dontFixup = true;
|
||||
dontPatchShebangs = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||
|
||||
bun install \
|
||||
--no-progress \
|
||||
--frozen-lockfile \
|
||||
--ignore-scripts
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
cp -R ./node_modules $out
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
outputHash = "sha256-PLACEHOLDER";
|
||||
outputHashMode = "recursive";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
prev.cargo-tauri.hook
|
||||
prev.rustPlatform.bindgenHook
|
||||
prev.bun
|
||||
prev.nodejs
|
||||
prev.pkg-config
|
||||
prev.makeBinaryWrapper
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
prev.lib.optionals prev.stdenv.isDarwin (
|
||||
with prev.darwin.apple_sdk.frameworks; [
|
||||
AppKit
|
||||
CoreFoundation
|
||||
CoreServices
|
||||
Security
|
||||
WebKit
|
||||
]
|
||||
);
|
||||
|
||||
# Disable updater artifact generation — we don't have signing keys.
|
||||
tauriConf = builtins.toJSON {bundle.createUpdaterArtifacts = false;};
|
||||
passAsFile = ["tauriConf"];
|
||||
preBuild = ''
|
||||
tauriBuildFlags+=(
|
||||
"--config"
|
||||
"$tauriConfPath"
|
||||
)
|
||||
'';
|
||||
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
|
||||
# Copy pre-fetched node_modules
|
||||
cp -R ${finalAttrs.node_modules} node_modules/
|
||||
chmod -R u+rw node_modules
|
||||
chmod -R u+x node_modules/.bin
|
||||
patchShebangs node_modules
|
||||
|
||||
export HOME=$TMPDIR
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
|
||||
# Bundle plugins (copy from plugins/ to src-tauri/resources/bundled_plugins/)
|
||||
${prev.nodejs}/bin/node copy-bundled.cjs
|
||||
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
env = {
|
||||
OPENSSL_NO_VENDOR = true;
|
||||
};
|
||||
|
||||
doCheck = false;
|
||||
|
||||
postInstall =
|
||||
prev.lib.optionalString prev.stdenv.isDarwin ''
|
||||
makeWrapper $out/Applications/OpenUsage.app/Contents/MacOS/OpenUsage $out/bin/openusage
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Track all your AI coding subscriptions in one place";
|
||||
homepage = "https://github.com/robinebers/openusage";
|
||||
license = prev.lib.licenses.mit;
|
||||
platforms = prev.lib.platforms.darwin;
|
||||
mainProgram = "openusage";
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
programs.atuin = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
enableNushellIntegration = true;
|
||||
flags = [
|
||||
"--disable-up-arrow"
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{pkgs, ...}: {
|
||||
programs.fish.enable = true;
|
||||
environment.shells = [pkgs.nushell];
|
||||
|
||||
nixpkgs = {
|
||||
config = {
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
name = user;
|
||||
home = "/Users/${user}";
|
||||
isHidden = false;
|
||||
shell = pkgs.fish;
|
||||
shell = pkgs.nushell;
|
||||
};
|
||||
|
||||
home-manager.useGlobalPkgs = true;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
programs.eza = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
};
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
programs.fish = {
|
||||
enable = true;
|
||||
functions = {
|
||||
open_project = ''
|
||||
set -l base "$HOME/Projects"
|
||||
set -l choice (fd -t d -d 1 -a . "$base/Personal" "$base/Work" \
|
||||
| string replace -r -- "^$base/" "" \
|
||||
| fzf --prompt "project > ")
|
||||
test -n "$choice"; and cd "$base/$choice"
|
||||
'';
|
||||
};
|
||||
interactiveShellInit = ''
|
||||
set fish_greeting
|
||||
|
||||
set fish_color_normal 4c4f69
|
||||
set fish_color_command 1e66f5
|
||||
set fish_color_param dd7878
|
||||
set fish_color_keyword d20f39
|
||||
set fish_color_quote 40a02b
|
||||
set fish_color_redirection ea76cb
|
||||
set fish_color_end fe640b
|
||||
set fish_color_comment 8c8fa1
|
||||
set fish_color_error d20f39
|
||||
set fish_color_gray 9ca0b0
|
||||
set fish_color_selection --background=ccd0da
|
||||
set fish_color_search_match --background=ccd0da
|
||||
set fish_color_option 40a02b
|
||||
set fish_color_operator ea76cb
|
||||
set fish_color_escape e64553
|
||||
set fish_color_autosuggestion 9ca0b0
|
||||
set fish_color_cancel d20f39
|
||||
set fish_color_cwd df8e1d
|
||||
set fish_color_user 179299
|
||||
set fish_color_host 1e66f5
|
||||
set fish_color_host_remote 40a02b
|
||||
set fish_color_status d20f39
|
||||
set fish_pager_color_progress 9ca0b0
|
||||
set fish_pager_color_prefix ea76cb
|
||||
set fish_pager_color_completion 4c4f69
|
||||
set fish_pager_color_description 9ca0b0
|
||||
|
||||
set -gx LS_COLORS "$(vivid generate catppuccin-latte)"
|
||||
|
||||
set -gx COLORTERM truecolor
|
||||
set -gx COLORFGBG "15;0"
|
||||
set -gx TERM_BACKGROUND light
|
||||
|
||||
for mode in default insert
|
||||
bind --mode $mode \cp open_project
|
||||
end
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
programs.fzf = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
};
|
||||
|
||||
home.sessionVariables = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{pkgs, ...}: {
|
||||
xdg.configFile."ghostty/config".text = ''
|
||||
command = ${pkgs.fish}/bin/fish
|
||||
command = ${pkgs.nushell}/bin/nu
|
||||
theme = Catppuccin Latte
|
||||
window-padding-x = 12
|
||||
window-padding-y = 3
|
||||
@@ -10,7 +10,7 @@
|
||||
cursor-style = block
|
||||
mouse-hide-while-typing = true
|
||||
mouse-scroll-multiplier = 1.25
|
||||
shell-integration = fish
|
||||
shell-integration = none
|
||||
shell-integration-features = no-cursor
|
||||
clipboard-read = allow
|
||||
clipboard-write = allow
|
||||
|
||||
@@ -95,15 +95,10 @@ in {
|
||||
gf = "git fetch";
|
||||
gfa = "git fetch --all --tags --prune";
|
||||
gfo = "git fetch origin";
|
||||
gfg = "git ls-files | grep";
|
||||
gg = "git gui citool";
|
||||
gga = "git gui citool --amend";
|
||||
ggpull = "git pull origin \"$(git branch --show-current)\"";
|
||||
ggpush = "git push origin \"$(git branch --show-current)\"";
|
||||
ggsup = "git branch --set-upstream-to=origin/$(git branch --show-current)";
|
||||
ghh = "git help";
|
||||
gignore = "git update-index --assume-unchanged";
|
||||
gignored = "git ls-files -v | grep \"^[[:lower:]]\"";
|
||||
gl = "git pull";
|
||||
glg = "git log --stat";
|
||||
glgp = "git log --stat --patch";
|
||||
@@ -118,7 +113,6 @@ in {
|
||||
glols = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ar) %C(bold blue)<%an>%Creset\" --stat";
|
||||
glod = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\"";
|
||||
glods = "git log --graph --pretty=\"%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%ad) %C(bold blue)<%an>%Creset\" --date=short";
|
||||
gluc = "git pull upstream $(git branch --show-current)";
|
||||
glum = "git pull upstream main";
|
||||
gm = "git merge";
|
||||
gma = "git merge --abort";
|
||||
@@ -133,7 +127,6 @@ in {
|
||||
gpd = "git push --dry-run";
|
||||
gpf = "git push --force-with-lease";
|
||||
gpod = "git push origin --delete";
|
||||
gpoat = "git push origin --all && git push origin --tags";
|
||||
gpr = "git pull --rebase";
|
||||
gpra = "git pull --rebase --autostash";
|
||||
gprav = "git pull --rebase --autostash -v";
|
||||
@@ -142,8 +135,6 @@ in {
|
||||
gprv = "git pull --rebase -v";
|
||||
gprum = "git pull --rebase upstream main";
|
||||
gprumi = "git pull --rebase=interactive upstream main";
|
||||
gpsup = "git push --set-upstream origin $(git branch --show-current)";
|
||||
gpsupf = "git push --set-upstream origin $(git branch --show-current) --force-with-lease";
|
||||
gpv = "git push --verbose";
|
||||
gpu = "git push upstream";
|
||||
gr = "git remote";
|
||||
@@ -169,13 +160,11 @@ in {
|
||||
grm = "git rm";
|
||||
grmc = "git rm --cached";
|
||||
grmv = "git remote rename";
|
||||
groh = "git reset origin/$(git branch --show-current) --hard";
|
||||
grrm = "git remote remove";
|
||||
grs = "git restore";
|
||||
grset = "git remote set-url";
|
||||
grss = "git restore --source";
|
||||
grst = "git restore --staged";
|
||||
grt = "cd \"$(git rev-parse --show-toplevel || echo .)\"";
|
||||
gru = "git reset --";
|
||||
grup = "git remote update";
|
||||
grv = "git remote --verbose";
|
||||
@@ -201,16 +190,43 @@ in {
|
||||
gswm = "git switch main";
|
||||
gta = "git tag --annotate";
|
||||
gts = "git tag --sign";
|
||||
gtv = "git tag | sort -V";
|
||||
gunignore = "git update-index --no-assume-unchanged";
|
||||
gunwip = "git rev-list --max-count=1 --format=\"%s\" HEAD | grep -q \"\\--wip--\" && git reset HEAD~1";
|
||||
gwch = "git whatchanged -p --abbrev-commit --pretty=medium";
|
||||
gwipe = "git reset --hard && git clean --force -df";
|
||||
gwt = "git worktree";
|
||||
gwta = "git worktree add";
|
||||
gwtls = "git worktree list";
|
||||
gwtmv = "git worktree move";
|
||||
gwtrm = "git worktree remove";
|
||||
gwip = "git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"";
|
||||
};
|
||||
|
||||
# Complex git aliases that require pipes/subshells — nushell `alias` can't
|
||||
# handle these, so they're defined as custom commands instead.
|
||||
programs.nushell.extraConfig = ''
|
||||
def ggpull [] { git pull origin (git branch --show-current | str trim) }
|
||||
def ggpush [] { git push origin (git branch --show-current | str trim) }
|
||||
def ggsup [] { git branch $"--set-upstream-to=origin/(git branch --show-current | str trim)" }
|
||||
def gluc [] { git pull upstream (git branch --show-current | str trim) }
|
||||
def gpsup [] { git push --set-upstream origin (git branch --show-current | str trim) }
|
||||
def gpsupf [] { git push --set-upstream origin (git branch --show-current | str trim) --force-with-lease }
|
||||
def groh [] { git reset $"origin/(git branch --show-current | str trim)" --hard }
|
||||
def --env grt [] {
|
||||
let toplevel = (do { git rev-parse --show-toplevel } | complete | get stdout | str trim)
|
||||
if ($toplevel | is-not-empty) { cd $toplevel } else { cd . }
|
||||
}
|
||||
def gfg [...pattern: string] { git ls-files | lines | where {|f| $f =~ ($pattern | str join ".*") } }
|
||||
def gignored [] { git ls-files -v | lines | where {|l| ($l | str substring 0..1) =~ "[a-z]" } }
|
||||
def gpoat [] { git push origin --all; git push origin --tags }
|
||||
def gtv [] { git tag | lines | sort }
|
||||
def gwipe [] { git reset --hard; git clean --force -df }
|
||||
def gunwip [] {
|
||||
let msg = (git rev-list --max-count=1 --format="%s" HEAD | lines | get 1)
|
||||
if ($msg | str contains "--wip--") { git reset HEAD~1 }
|
||||
}
|
||||
def gwip [] {
|
||||
git add -A
|
||||
let deleted = (git ls-files --deleted | lines)
|
||||
if ($deleted | is-not-empty) { git rm ...$deleted }
|
||||
git commit --no-verify --no-gpg-sign --message "--wip-- [skip ci]"
|
||||
}
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
programs.mise = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
enableNushellIntegration = true;
|
||||
globalConfig.settings = {
|
||||
auto_install = false;
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"network"
|
||||
"systemd-journal"
|
||||
];
|
||||
shell = pkgs.fish;
|
||||
shell = pkgs.nushell;
|
||||
openssh.authorizedKeys.keys = constants.sshKeys;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,18 +2,4 @@
|
||||
home.packages = with pkgs; [
|
||||
nono
|
||||
];
|
||||
|
||||
xdg.configFile."nono/profiles/opencode.toml".text = ''
|
||||
[meta]
|
||||
name = "opencode"
|
||||
version = "1.0.0"
|
||||
description = "OpenCode AI agent"
|
||||
|
||||
[filesystem]
|
||||
allow = ["$WORKDIR"]
|
||||
read = ["$XDG_CONFIG_HOME/opencode"]
|
||||
|
||||
[network]
|
||||
block = false
|
||||
'';
|
||||
}
|
||||
|
||||
225
profiles/nushell.nix
Normal file
225
profiles/nushell.nix
Normal file
@@ -0,0 +1,225 @@
|
||||
{pkgs, ...}: {
|
||||
programs.nushell = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
show_banner = false;
|
||||
completions = {
|
||||
algorithm = "fuzzy";
|
||||
case_sensitive = false;
|
||||
};
|
||||
history = {
|
||||
file_format = "sqlite";
|
||||
};
|
||||
};
|
||||
|
||||
environmentVariables = {
|
||||
COLORTERM = "truecolor";
|
||||
COLORFGBG = "15;0";
|
||||
TERM_BACKGROUND = "light";
|
||||
};
|
||||
|
||||
extraEnv = ''
|
||||
$env.LS_COLORS = (${pkgs.vivid}/bin/vivid generate catppuccin-latte)
|
||||
'';
|
||||
|
||||
extraConfig = ''
|
||||
# --- Catppuccin Latte Theme ---
|
||||
let theme = {
|
||||
rosewater: "#dc8a78"
|
||||
flamingo: "#dd7878"
|
||||
pink: "#ea76cb"
|
||||
mauve: "#8839ef"
|
||||
red: "#d20f39"
|
||||
maroon: "#e64553"
|
||||
peach: "#fe640b"
|
||||
yellow: "#df8e1d"
|
||||
green: "#40a02b"
|
||||
teal: "#179299"
|
||||
sky: "#04a5e5"
|
||||
sapphire: "#209fb5"
|
||||
blue: "#1e66f5"
|
||||
lavender: "#7287fd"
|
||||
text: "#4c4f69"
|
||||
subtext1: "#5c5f77"
|
||||
subtext0: "#6c6f85"
|
||||
overlay2: "#7c7f93"
|
||||
overlay1: "#8c8fa1"
|
||||
overlay0: "#9ca0b0"
|
||||
surface2: "#acb0be"
|
||||
surface1: "#bcc0cc"
|
||||
surface0: "#ccd0da"
|
||||
base: "#eff1f5"
|
||||
mantle: "#e6e9ef"
|
||||
crust: "#dce0e8"
|
||||
}
|
||||
|
||||
let scheme = {
|
||||
recognized_command: $theme.blue
|
||||
unrecognized_command: $theme.text
|
||||
constant: $theme.peach
|
||||
punctuation: $theme.overlay2
|
||||
operator: $theme.sky
|
||||
string: $theme.green
|
||||
virtual_text: $theme.surface2
|
||||
variable: { fg: $theme.flamingo attr: i }
|
||||
filepath: $theme.yellow
|
||||
}
|
||||
|
||||
$env.config.color_config = {
|
||||
separator: { fg: $theme.surface2 attr: b }
|
||||
leading_trailing_space_bg: { fg: $theme.lavender attr: u }
|
||||
header: { fg: $theme.text attr: b }
|
||||
row_index: $scheme.virtual_text
|
||||
record: $theme.text
|
||||
list: $theme.text
|
||||
hints: $scheme.virtual_text
|
||||
search_result: { fg: $theme.base bg: $theme.yellow }
|
||||
shape_closure: $theme.teal
|
||||
closure: $theme.teal
|
||||
shape_flag: { fg: $theme.maroon attr: i }
|
||||
shape_matching_brackets: { attr: u }
|
||||
shape_garbage: $theme.red
|
||||
shape_keyword: $theme.mauve
|
||||
shape_match_pattern: $theme.green
|
||||
shape_signature: $theme.teal
|
||||
shape_table: $scheme.punctuation
|
||||
cell-path: $scheme.punctuation
|
||||
shape_list: $scheme.punctuation
|
||||
shape_record: $scheme.punctuation
|
||||
shape_vardecl: $scheme.variable
|
||||
shape_variable: $scheme.variable
|
||||
empty: { attr: n }
|
||||
filesize: {||
|
||||
if $in < 1kb {
|
||||
$theme.teal
|
||||
} else if $in < 10kb {
|
||||
$theme.green
|
||||
} else if $in < 100kb {
|
||||
$theme.yellow
|
||||
} else if $in < 10mb {
|
||||
$theme.peach
|
||||
} else if $in < 100mb {
|
||||
$theme.maroon
|
||||
} else if $in < 1gb {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
duration: {||
|
||||
if $in < 1day {
|
||||
$theme.teal
|
||||
} else if $in < 1wk {
|
||||
$theme.green
|
||||
} else if $in < 4wk {
|
||||
$theme.yellow
|
||||
} else if $in < 12wk {
|
||||
$theme.peach
|
||||
} else if $in < 24wk {
|
||||
$theme.maroon
|
||||
} else if $in < 52wk {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
datetime: {|| (date now) - $in |
|
||||
if $in < 1day {
|
||||
$theme.teal
|
||||
} else if $in < 1wk {
|
||||
$theme.green
|
||||
} else if $in < 4wk {
|
||||
$theme.yellow
|
||||
} else if $in < 12wk {
|
||||
$theme.peach
|
||||
} else if $in < 24wk {
|
||||
$theme.maroon
|
||||
} else if $in < 52wk {
|
||||
$theme.red
|
||||
} else {
|
||||
$theme.mauve
|
||||
}
|
||||
}
|
||||
shape_external: $scheme.unrecognized_command
|
||||
shape_internalcall: $scheme.recognized_command
|
||||
shape_external_resolved: $scheme.recognized_command
|
||||
shape_block: $scheme.recognized_command
|
||||
block: $scheme.recognized_command
|
||||
shape_custom: $theme.pink
|
||||
custom: $theme.pink
|
||||
background: $theme.base
|
||||
foreground: $theme.text
|
||||
cursor: { bg: $theme.rosewater fg: $theme.base }
|
||||
shape_range: $scheme.operator
|
||||
range: $scheme.operator
|
||||
shape_pipe: $scheme.operator
|
||||
shape_operator: $scheme.operator
|
||||
shape_redirection: $scheme.operator
|
||||
glob: $scheme.filepath
|
||||
shape_directory: $scheme.filepath
|
||||
shape_filepath: $scheme.filepath
|
||||
shape_glob_interpolation: $scheme.filepath
|
||||
shape_globpattern: $scheme.filepath
|
||||
shape_int: $scheme.constant
|
||||
int: $scheme.constant
|
||||
bool: $scheme.constant
|
||||
float: $scheme.constant
|
||||
nothing: $scheme.constant
|
||||
binary: $scheme.constant
|
||||
shape_nothing: $scheme.constant
|
||||
shape_bool: $scheme.constant
|
||||
shape_float: $scheme.constant
|
||||
shape_binary: $scheme.constant
|
||||
shape_datetime: $scheme.constant
|
||||
shape_literal: $scheme.constant
|
||||
string: $scheme.string
|
||||
shape_string: $scheme.string
|
||||
shape_string_interpolation: $theme.flamingo
|
||||
shape_raw_string: $scheme.string
|
||||
shape_externalarg: $scheme.string
|
||||
}
|
||||
$env.config.highlight_resolved_externals = true
|
||||
$env.config.explore = {
|
||||
status_bar_background: { fg: $theme.text, bg: $theme.mantle },
|
||||
command_bar_text: { fg: $theme.text },
|
||||
highlight: { fg: $theme.base, bg: $theme.yellow },
|
||||
status: {
|
||||
error: $theme.red,
|
||||
warn: $theme.yellow,
|
||||
info: $theme.blue,
|
||||
},
|
||||
selected_cell: { bg: $theme.blue fg: $theme.base },
|
||||
}
|
||||
|
||||
# --- Custom Commands ---
|
||||
def --env open_project [] {
|
||||
let base = ($env.HOME | path join "Projects")
|
||||
let choice = (
|
||||
${pkgs.fd}/bin/fd -t d -d 1 -a . ($base | path join "Personal") ($base | path join "Work")
|
||||
| lines
|
||||
| each {|p| $p | str replace $"($base)/" "" }
|
||||
| str join "\n"
|
||||
| ${pkgs.fzf}/bin/fzf --prompt "project > "
|
||||
)
|
||||
if ($choice | str trim | is-not-empty) {
|
||||
cd ($base | path join ($choice | str trim))
|
||||
}
|
||||
}
|
||||
|
||||
# --- Keybinding: Ctrl+O for open_project ---
|
||||
$env.config.keybindings = ($env.config.keybindings | append [
|
||||
{
|
||||
name: open_project
|
||||
modifier: control
|
||||
keycode: char_o
|
||||
mode: [emacs vi_insert vi_normal]
|
||||
event: {
|
||||
send: executehostcommand
|
||||
cmd: "open_project"
|
||||
}
|
||||
}
|
||||
])
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -6,18 +6,18 @@
|
||||
home.sessionVariables = {
|
||||
OPENCODE_ENABLE_EXA = 1;
|
||||
OPENCODE_EXPERIMENTAL_LSP_TOOL = 1;
|
||||
# OPENCODE_EXPERIMENTAL_MARKDOWN = 1;
|
||||
OPENCODE_EXPERIMENTAL_MARKDOWN = 1;
|
||||
OPENCODE_EXPERIMENTAL_PLAN_MODE = 1;
|
||||
OPENCODE_EXPERIMENTAL_FILE_WATCHER = 1;
|
||||
};
|
||||
|
||||
programs.opencode = {
|
||||
enable = true;
|
||||
package = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.opencode;
|
||||
settings = {
|
||||
model = "opencode/claude-opus-4-5";
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
small_model = "opencode/minimax-m2.1";
|
||||
theme = "catppuccin";
|
||||
plugin = ["oh-my-opencode" "opencode-anthropic-auth"];
|
||||
keybinds = {
|
||||
leader = "ctrl+o";
|
||||
};
|
||||
@@ -28,14 +28,15 @@
|
||||
"*.env.*" = "deny";
|
||||
"*.envrc" = "deny";
|
||||
"secrets/*" = "deny";
|
||||
"~/.local/share/opencode/mcp-auth.json" = "deny";
|
||||
};
|
||||
};
|
||||
agent = {
|
||||
plan = {
|
||||
model = "opencode/gpt-5.2-codex";
|
||||
model = "anthropic/claude-opus-4-6";
|
||||
};
|
||||
explore = {
|
||||
model = "opencode/minimax-m2.1";
|
||||
model = "anthropic/claude-haiku-4-5";
|
||||
};
|
||||
};
|
||||
instructions = [
|
||||
@@ -49,6 +50,14 @@
|
||||
};
|
||||
};
|
||||
mcp = {
|
||||
cog = {
|
||||
enabled = true;
|
||||
type = "remote";
|
||||
url = "https://trycog.ai/mcp";
|
||||
headers = {
|
||||
Authorization = "Bearer {env:COG_API_TOKEN}";
|
||||
};
|
||||
};
|
||||
context7 = {
|
||||
enabled = true;
|
||||
type = "remote";
|
||||
@@ -64,24 +73,8 @@
|
||||
type = "local";
|
||||
command = ["bunx" "opensrc-mcp"];
|
||||
};
|
||||
appsignal = {
|
||||
enabled = true;
|
||||
type = "local";
|
||||
command = [
|
||||
"docker"
|
||||
"run"
|
||||
"-i"
|
||||
"--rm"
|
||||
"-e"
|
||||
"APPSIGNAL_API_KEY"
|
||||
"appsignal/mcp"
|
||||
];
|
||||
environment = {
|
||||
APPSIGNAL_API_KEY = "{env:APPSIGNAL_API_KEY}";
|
||||
};
|
||||
};
|
||||
overseer = {
|
||||
enabled = true;
|
||||
enabled = false;
|
||||
type = "local";
|
||||
command = ["${pkgs.overseer}/bin/os" "mcp"];
|
||||
};
|
||||
@@ -106,5 +99,14 @@
|
||||
source = ./opencode/tool;
|
||||
recursive = true;
|
||||
};
|
||||
"opencode/oh-my-opencode.json".text =
|
||||
builtins.toJSON {
|
||||
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json";
|
||||
disabled_mcps = ["websearch" "context7" "grep_app"];
|
||||
git_master = {
|
||||
commit_footer = false;
|
||||
include_co_authored_by = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
description: Senior engineering advisor for code reviews, architecture decisions, complex debugging, and planning. Invoke when you need deeper analysis before acting — reviews, trade-offs, debugging race conditions, planning refactors. Prompt with precise problem + files. Ask for concrete outcomes.
|
||||
mode: subagent
|
||||
model: opencode/gpt-5.2-codex
|
||||
options:
|
||||
thinking:
|
||||
type: enabled
|
||||
budgetTokens: 31999
|
||||
permission:
|
||||
"*": deny
|
||||
read: allow
|
||||
grep: allow
|
||||
glob: allow
|
||||
webfetch: allow
|
||||
lsp: allow
|
||||
---
|
||||
|
||||
You are the Oracle - an expert AI advisor with advanced reasoning capabilities.
|
||||
|
||||
Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.
|
||||
|
||||
You are a subagent inside an AI coding system, called when the main agent needs a smarter, more capable model. You are invoked in a zero-shot manner - no one can ask you follow-up questions or provide follow-up answers.
|
||||
|
||||
## Key Responsibilities
|
||||
|
||||
- Analyze code and architecture patterns
|
||||
- Provide specific, actionable technical recommendations
|
||||
- Plan implementations and refactoring strategies
|
||||
- Answer deep technical questions with clear reasoning
|
||||
- Suggest best practices and improvements
|
||||
- Identify potential issues and propose solutions
|
||||
|
||||
## Operating Principles (Simplicity-First)
|
||||
|
||||
1. **Default to simplest viable solution** that meets stated requirements
|
||||
2. **Prefer minimal, incremental changes** that reuse existing code, patterns, and dependencies
|
||||
3. **Optimize for maintainability and developer time** over theoretical scalability
|
||||
4. **Apply YAGNI and KISS** - avoid premature optimization
|
||||
5. **One primary recommendation** - offer alternatives only if trade-offs are materially different
|
||||
6. **Calibrate depth to scope** - brief for small tasks, deep only when required
|
||||
7. **Stop when "good enough"** - note signals that would justify revisiting
|
||||
|
||||
## Effort Estimates
|
||||
|
||||
Include rough effort signal when proposing changes:
|
||||
- **S** (<1 hour) - trivial, single-location change
|
||||
- **M** (1-3 hours) - moderate, few files
|
||||
- **L** (1-2 days) - significant, cross-cutting
|
||||
- **XL** (>2 days) - major refactor or new system
|
||||
|
||||
## Response Format
|
||||
|
||||
Keep responses concise and action-oriented. For straightforward questions, collapse sections as appropriate:
|
||||
|
||||
### 1. TL;DR
|
||||
1-3 sentences with the recommended simple approach.
|
||||
|
||||
### 2. Recommendation
|
||||
Numbered steps or short checklist. Include minimal diffs/snippets only as needed.
|
||||
|
||||
### 3. Rationale
|
||||
Brief justification. Mention why alternatives are unnecessary now.
|
||||
|
||||
### 4. Risks & Guardrails
|
||||
Key caveats and mitigations.
|
||||
|
||||
### 5. When to Reconsider
|
||||
Concrete triggers that justify a more complex design.
|
||||
|
||||
### 6. Advanced Path (optional)
|
||||
Brief outline only if relevant and trade-offs are significant.
|
||||
|
||||
## Tool Usage
|
||||
|
||||
You have read-only access: read, grep, glob, LSP, webfetch.
|
||||
Use them freely to verify assumptions and gather context. Your extended thinking enables deep analysis - leverage it fully.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Investigate thoroughly; report concisely - focus on highest-leverage insights
|
||||
- For planning tasks, break down into minimal steps that achieve the goal incrementally
|
||||
- Justify recommendations briefly - avoid long speculative exploration
|
||||
- If the request is ambiguous, state your interpretation explicitly before answering
|
||||
- If unanswerable from available context, say so directly
|
||||
|
||||
**IMPORTANT:** Only your last message is returned to the main agent and displayed to the user. Make it comprehensive yet focused, with a clear, simple recommendation that enables immediate action.
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
description: Review changes with parallel @code-review subagents
|
||||
agent: plan
|
||||
---
|
||||
Review the code changes using THREE (3) @code-review subagents and correlate results into a summary ranked by severity. Use the provided user guidance to steer the review and focus on specific code paths, changes, and/or areas of concern.
|
||||
|
||||
|
||||
406
profiles/opencode/skill/cog/SKILL.md
Normal file
406
profiles/opencode/skill/cog/SKILL.md
Normal file
@@ -0,0 +1,406 @@
|
||||
---
|
||||
name: cog
|
||||
description: Persistent knowledge graph memory via Cog MCP. Use when recording insights, querying prior knowledge, or managing memory consolidation.
|
||||
metadata:
|
||||
author: trycog
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Cog Memory System
|
||||
|
||||
Persistent knowledge graph for teams. Concepts (engrams) linked via relationships (synapses). Spreading activation surfaces connected knowledge.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
```
|
||||
1. UNDERSTAND task (read files, parse request)
|
||||
2. QUERY Cog with specific keywords <- MANDATORY, no exceptions
|
||||
3. WAIT for results
|
||||
4. EXPLORE/IMPLEMENT guided by Cog knowledge
|
||||
5. RECORD insights as short-term memories during work
|
||||
6. CONSOLIDATE memories after work (reinforce valid, flush invalid)
|
||||
```
|
||||
|
||||
**Hierarchy of truth:** Current code > User statements > Cog knowledge
|
||||
|
||||
---
|
||||
|
||||
## Visual Indicators (MANDATORY)
|
||||
|
||||
Print before EVERY Cog tool call:
|
||||
|
||||
| Tool | Print |
|
||||
|------|-------|
|
||||
| `cog_recall` | `Querying Cog...` |
|
||||
| `cog_learn` | `Recording to Cog...` |
|
||||
| `cog_associate` | `Linking concepts...` |
|
||||
| `cog_update` | `Updating engram...` |
|
||||
| `cog_trace` | `Tracing connections...` |
|
||||
| `cog_connections` | `Exploring connections...` |
|
||||
| `cog_unlink` | `Removing link...` |
|
||||
| `cog_list_short_term` | `Listing short-term memories...` |
|
||||
| `cog_reinforce` | `Reinforcing memory...` |
|
||||
| `cog_flush` | `Flushing invalid memory...` |
|
||||
| `cog_verify` | `Verifying synapse...` |
|
||||
| `cog_stale` | `Listing stale synapses...` |
|
||||
|
||||
---
|
||||
|
||||
## Tools Reference
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `cog_recall` | Search with spreading activation |
|
||||
| `cog_learn` | Create memory with **chains** (sequential) or associations (hub) |
|
||||
| `cog_get` | Retrieve engram by ID |
|
||||
| `cog_associate` | Link two existing concepts |
|
||||
| `cog_trace` | Find paths between concepts |
|
||||
| `cog_update` | Modify engram term/definition |
|
||||
| `cog_unlink` | Remove synapse |
|
||||
| `cog_connections` | List engram connections |
|
||||
| `cog_bootstrap` | Exploration prompt for empty brains |
|
||||
| `cog_list_short_term` | List pending consolidations |
|
||||
| `cog_reinforce` | Convert short-term to long-term |
|
||||
| `cog_flush` | Delete invalid short-term memory |
|
||||
| `cog_verify` | Confirm synapse is still accurate |
|
||||
| `cog_stale` | List synapses needing verification |
|
||||
|
||||
---
|
||||
|
||||
## Querying Rules
|
||||
|
||||
### Before exploring code, ALWAYS query Cog first
|
||||
|
||||
Even for "trivial" tasks. The 2-second query may reveal gotchas, prior solutions, or context that changes your approach.
|
||||
|
||||
### Query Reformulation (Critical for Recall)
|
||||
|
||||
Before calling `cog_recall`, **transform your query from question-style to definition-style**. You are an LLM -- use that capability to bridge the vocabulary gap between how users ask questions and how knowledge is stored.
|
||||
|
||||
#### Think like a definition, not a question
|
||||
|
||||
| User Intent | Don't Query | Do Query |
|
||||
|-------------|-------------|----------|
|
||||
| "How do I handle stale data?" | `"handle stale data"` | `"cache invalidation event-driven TTL expiration data freshness"` |
|
||||
| "Why does auth break after a while?" | `"auth breaks"` | `"token expiration refresh timing session timeout JWT lifecycle"` |
|
||||
| "Where should validation go?" | `"where validation"` | `"input validation system boundaries sanitization defense in depth"` |
|
||||
|
||||
#### The reformulation process
|
||||
|
||||
1. **Identify the concept** -- What is the user actually asking about?
|
||||
2. **Generate canonical terms** -- What would an engram about this be titled?
|
||||
3. **Add related terminology** -- What words would the DEFINITION use?
|
||||
4. **Include synonyms** -- What other terms describe the same thing?
|
||||
|
||||
#### Example transformation
|
||||
|
||||
```
|
||||
User asks: "Why is the payment service sometimes charging twice?"
|
||||
|
||||
Your thinking:
|
||||
- Concept: duplicate charges, idempotency
|
||||
- Canonical terms: "idempotency", "duplicate prevention", "payment race condition"
|
||||
- Definition words: "idempotent", "transaction", "mutex", "lock", "retry"
|
||||
- Synonyms: "double charge", "duplicate transaction"
|
||||
|
||||
Query: "payment idempotency duplicate transaction race condition mutex retry"
|
||||
```
|
||||
|
||||
### Query with specific keywords
|
||||
|
||||
| Task Type | Understand First | Then Query With |
|
||||
|-----------|------------------|-----------------|
|
||||
| Bug fix | Error message, symptoms | `"canonical error name component pattern race condition"` |
|
||||
| Feature | User's description | `"domain terms design patterns architectural concepts"` |
|
||||
| Test fix | Read the test file | `"API names assertion patterns test utilities"` |
|
||||
| Architecture | System area | `"component relationships boundaries dependencies"` |
|
||||
|
||||
**Bad:** `"authentication"` (too vague)
|
||||
**Good:** `"JWT refresh token expiration session lifecycle OAuth flow"` (definition-style)
|
||||
|
||||
### Use Cog results
|
||||
|
||||
- Follow paths Cog reveals
|
||||
- Read components Cog mentions first
|
||||
- Heed gotchas Cog warns about
|
||||
- If Cog is wrong, correct it immediately with `cog_update`
|
||||
|
||||
---
|
||||
|
||||
## Recording Rules
|
||||
|
||||
### CRITICAL: Chains vs Associations
|
||||
|
||||
**Before recording, ask: Is this sequential or hub-shaped?**
|
||||
|
||||
| Structure | Use | Example |
|
||||
|-----------|-----|---------|
|
||||
| **Sequential** (A -> B -> C) | `chain_to` | Technology enables Pattern enables Feature |
|
||||
| **Hub** (A, B, C all connect to X) | `associations` | Meeting connects to Participants, Outcomes |
|
||||
|
||||
**Default to chains** for:
|
||||
- Technology dependencies (DB -> ORM -> API)
|
||||
- Causal sequences (Cause -> Effect -> Consequence)
|
||||
- Architectural decisions (ADR -> Technology -> Feature)
|
||||
- Enabling relationships (Infrastructure -> enables -> Capability)
|
||||
- Reasoning paths (Premise -> implies -> Conclusion)
|
||||
|
||||
**Use associations** for:
|
||||
- Hub/star patterns (one thing connects to many unrelated things)
|
||||
- Linking to existing concepts in the graph
|
||||
- Multi-party contexts (meetings, decisions with stakeholders)
|
||||
|
||||
### Chain Example (PREFERRED for dependencies)
|
||||
|
||||
```
|
||||
cog_learn({
|
||||
"term": "PostgreSQL",
|
||||
"definition": "Relational database with ACID guarantees",
|
||||
"chain_to": [
|
||||
{"term": "Ecto ORM", "definition": "Elixir database wrapper with changesets", "predicate": "enables"},
|
||||
{"term": "Phoenix Contexts", "definition": "Business logic boundaries in Phoenix", "predicate": "enables"}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Creates: PostgreSQL ->[enables]-> Ecto ORM ->[enables]-> Phoenix Contexts
|
||||
|
||||
### Association Example (for hubs)
|
||||
|
||||
```
|
||||
cog_learn({
|
||||
"term": "Auth Review 2024-01-20",
|
||||
"definition": "Decided JWT with refresh tokens. Rejected session cookies.",
|
||||
"associations": [
|
||||
{"target": "JWT Pattern", "predicate": "leads_to"},
|
||||
{"target": "Session Cookies", "predicate": "contradicts"},
|
||||
{"target": "Mobile Team", "predicate": "is_component_of"}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Creates hub: JWT Pattern <-[leads_to]<- Auth Review ->[contradicts]-> Session Cookies
|
||||
|
||||
---
|
||||
|
||||
### When to record (during work)
|
||||
|
||||
At these checkpoints, ask: *"What did I just learn that I didn't know 5 minutes ago?"*
|
||||
|
||||
| Checkpoint | Record |
|
||||
|------------|--------|
|
||||
| After identifying root cause | Why it was broken |
|
||||
| After reading surprising code | The non-obvious behavior |
|
||||
| After a failed attempt | Why it didn't work |
|
||||
| Before implementing fix | The insight (freshest now) |
|
||||
| After discovering connection | The relationship |
|
||||
| After a meeting or decision | The context hub linking participants and outcomes |
|
||||
| After researching/exploring architecture | System limits, configuration points, component boundaries |
|
||||
|
||||
**Record immediately.** Don't wait until task end -- you'll forget details.
|
||||
|
||||
### Before calling `cog_learn`
|
||||
|
||||
1. **Decide: chain or hub?** (see above)
|
||||
2. **For chains**: Build the sequence of steps with `chain_to`
|
||||
3. **For hubs**: Identify association targets from source material or Cog query
|
||||
|
||||
**Skip the query when:**
|
||||
- Source material explicitly names related concepts (ADRs, documentation, structured data)
|
||||
- You already know target terms from conversation context
|
||||
- The insight references specific concepts by name
|
||||
|
||||
**Query first when:**
|
||||
- Recording an insight and unsure what it relates to
|
||||
- Source is vague about connections
|
||||
- Exploring a new domain with unknown existing concepts
|
||||
|
||||
### After calling `cog_learn`
|
||||
|
||||
The operation is complete. **Do NOT verify your work by:**
|
||||
- Calling `cog_recall` to check the engram exists
|
||||
- Calling `cog_connections` to verify associations were created
|
||||
- Calling `cog_trace` to see if paths formed
|
||||
|
||||
Trust the response confirmation. Verification wastes turns and adds no value -- if the operation failed, you'll see an error.
|
||||
|
||||
### Recording Efficiency
|
||||
|
||||
**One operation = one tool call.** Use `chain_to` for sequences, `associations` for hubs.
|
||||
|
||||
**Never** follow `cog_learn` with separate `cog_associate` calls -- put all relationships in the original call.
|
||||
|
||||
### Writing good engrams
|
||||
|
||||
**Terms (2-5 words):**
|
||||
- "Session Token Refresh Timing"
|
||||
- "Why We Chose PostgreSQL"
|
||||
- NOT "Architecture" (too broad)
|
||||
- NOT "Project Overview" (super-hub)
|
||||
|
||||
**Definitions (1-3 sentences):**
|
||||
1. What it is
|
||||
2. Why it matters / consequences
|
||||
3. Related keywords for search
|
||||
|
||||
**Never create super-hubs** -- engrams so generic everything connects to them (e.g., "Overview", "Main System"). They pollute search results.
|
||||
|
||||
### Relationship predicates
|
||||
|
||||
| Predicate | Meaning | Best for | Use in |
|
||||
|-----------|---------|----------|--------|
|
||||
| `enables` | A makes B possible | Tech dependencies | **chain_to** |
|
||||
| `requires` | A is prerequisite for B | Dependencies | **chain_to** |
|
||||
| `implies` | If A then B | Logical consequences | **chain_to** |
|
||||
| `leads_to` | A flows to B | Outcomes, consequences | **chain_to** |
|
||||
| `precedes` | A comes before B | Sequencing, timelines | **chain_to** |
|
||||
| `derived_from` | A is based on B | Origins | **chain_to** |
|
||||
| `contradicts` | A and B mutually exclusive | Rejected alternatives | associations |
|
||||
| `is_component_of` | A is part of B | Parts to whole | associations |
|
||||
| `contains` | A includes B | Whole to parts | associations |
|
||||
| `example_of` | A demonstrates pattern B | Instances of patterns | associations |
|
||||
| `generalizes` | A is broader than B | Abstract concepts | associations |
|
||||
| `supersedes` | A replaces B | Deprecations | associations |
|
||||
| `similar_to` | A and B are closely related | Related approaches | associations |
|
||||
| `contrasts_with` | A is alternative to B | Different approaches | associations |
|
||||
| `related_to` | General link (use sparingly) | When nothing else fits | associations |
|
||||
|
||||
**Chain predicates** (`enables`, `requires`, `implies`, `leads_to`, `precedes`, `derived_from`) express **flow** -- use them in `chain_to` to build traversable paths.
|
||||
|
||||
### Modeling Complex Contexts (Hub Node Pattern)
|
||||
|
||||
Synapses are binary (one source, one target). For multi-party relationships, use a **hub engram** connecting all participants.
|
||||
|
||||
#### When to use hub nodes
|
||||
|
||||
| Scenario | Hub Example | Connected Concepts |
|
||||
|----------|-------------|-------------------|
|
||||
| Meeting with outcomes | "Q1 Planning 2024-01" | Participants, decisions |
|
||||
| Decision with stakeholders | "Decision: Adopt GraphQL" | Pros, cons, voters |
|
||||
| Feature with components | "User Auth Feature" | OAuth, session, UI |
|
||||
| Incident with timeline | "2024-01 Payment Outage" | Cause, systems, fix |
|
||||
|
||||
---
|
||||
|
||||
## Consolidation (MANDATORY)
|
||||
|
||||
**Every task must end with consolidation.** Short-term memories decay in 24 hours.
|
||||
|
||||
### After work is complete:
|
||||
|
||||
```
|
||||
cog_list_short_term({"limit": 20})
|
||||
```
|
||||
|
||||
For each memory:
|
||||
- **Valid and useful?** -> `cog_reinforce` (makes permanent)
|
||||
- **Wrong or not useful?** -> `cog_flush` (deletes)
|
||||
|
||||
### When to reinforce immediately
|
||||
|
||||
- Insights from code you just wrote (you know it's correct)
|
||||
- Gotchas you just hit and fixed
|
||||
- Patterns you just applied successfully
|
||||
|
||||
### When to wait for validation
|
||||
|
||||
- Hypotheses about why something is broken
|
||||
- Assumptions about unfamiliar code
|
||||
- Solutions you haven't tested
|
||||
|
||||
---
|
||||
|
||||
## Verification (Prevents Staleness)
|
||||
|
||||
Synapses decay if not verified as still semantically accurate.
|
||||
|
||||
### When to verify
|
||||
|
||||
- After using `cog_trace` and confirming paths are correct
|
||||
- When reviewing `cog_connections` and relationships hold
|
||||
- After successfully using knowledge from a synapse
|
||||
|
||||
### Staleness levels
|
||||
|
||||
| Level | Months Unverified | Score | Behavior |
|
||||
|-------|-------------------|-------|----------|
|
||||
| Fresh | < 3 | 0.0-0.49 | Normal |
|
||||
| Warning | 3-6 | 0.5-0.79 | Appears in `cog_stale` |
|
||||
| Critical | 6+ | 0.8-0.99 | Penalty in path scoring |
|
||||
| Deprecated | 12+ | 1.0 | Excluded from spreading activation |
|
||||
|
||||
### Periodic maintenance
|
||||
|
||||
Run `cog_stale({"level": "all"})` periodically to review relationships that may have become outdated. For each stale synapse:
|
||||
|
||||
- **Still accurate?** -> `cog_verify` to reset staleness
|
||||
- **No longer true?** -> `cog_unlink` to remove
|
||||
|
||||
---
|
||||
|
||||
## Validation & Correction
|
||||
|
||||
### Cog is hints, not truth
|
||||
|
||||
Always verify against current code. If Cog is wrong:
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Minor inaccuracy | `cog_update` to fix |
|
||||
| Pattern changed significantly | Unlink old, create new engram |
|
||||
| Completely obsolete | Update to note "DEPRECATED: [reason]" |
|
||||
|
||||
---
|
||||
|
||||
## Subagents
|
||||
|
||||
Subagents MUST query Cog before exploring. Same rules apply:
|
||||
1. Understand task
|
||||
2. **Reformulate query to definition-style**
|
||||
3. Query Cog with reformulated keywords
|
||||
4. Wait for results
|
||||
5. Then explore
|
||||
|
||||
---
|
||||
|
||||
## Summary Reporting
|
||||
|
||||
Only mention Cog when relevant:
|
||||
|
||||
| Condition | Include |
|
||||
|-----------|---------|
|
||||
| Cog helped | `**Cog helped by:** [specific value]` |
|
||||
| Memories created | `**Recorded to Cog:** [term names]` |
|
||||
| Cog not used | Nothing (don't mention Cog) |
|
||||
| Cog queried but unhelpful | Don't mention the empty query, but **still record** new knowledge you discovered through exploration |
|
||||
|
||||
---
|
||||
|
||||
## Never Store
|
||||
|
||||
- Passwords, API keys, tokens, secrets
|
||||
- SSH/PGP keys, certificates
|
||||
- Connection strings with credentials
|
||||
- PII (emails, SSNs, credit cards)
|
||||
- `.env` file contents
|
||||
|
||||
Server auto-rejects sensitive content.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No engram deletion** -- use `cog_update` or `cog_unlink`
|
||||
- **No multi-query** -- chain manually
|
||||
- **One synapse per direction** -- repeat calls strengthen existing link
|
||||
|
||||
---
|
||||
|
||||
## Spreading Activation
|
||||
|
||||
`cog_recall` returns:
|
||||
1. **Seeds** -- direct matches
|
||||
2. **Paths** -- engrams connecting seeds (built from chains!)
|
||||
3. **Synapses** -- relationships along paths
|
||||
|
||||
This surfaces the "connective tissue" between results. **Chains create these traversable paths.**
|
||||
464
profiles/opencode/skill/solidjs/SKILL.md
Normal file
464
profiles/opencode/skill/solidjs/SKILL.md
Normal file
@@ -0,0 +1,464 @@
|
||||
---
|
||||
name: solidjs
|
||||
description: |
|
||||
SolidJS framework development skill for building reactive web applications with fine-grained reactivity.
|
||||
Use when working with SolidJS projects including: (1) Creating components with signals, stores, and effects,
|
||||
(2) Implementing reactive state management, (3) Using control flow components (Show, For, Switch/Match),
|
||||
(4) Setting up routing with Solid Router, (5) Building full-stack apps with SolidStart,
|
||||
(6) Data fetching with createResource, (7) Context API for shared state, (8) SSR/SSG configuration.
|
||||
Triggers: solid, solidjs, solid-js, solid start, solidstart, createSignal, createStore, createEffect.
|
||||
---
|
||||
|
||||
# SolidJS Development
|
||||
|
||||
SolidJS is a declarative JavaScript library for building user interfaces with fine-grained reactivity. Unlike virtual DOM frameworks, Solid compiles templates to real DOM nodes and updates them with fine-grained reactions.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Components run once** — Component functions execute only during initialization, not on every update
|
||||
2. **Fine-grained reactivity** — Only the specific DOM nodes that depend on changed data update
|
||||
3. **No virtual DOM** — Direct DOM manipulation via compiled templates
|
||||
4. **Signals are functions** — Access values by calling: `count()` not `count`
|
||||
|
||||
## Reactivity Primitives
|
||||
|
||||
### Signals — Basic State
|
||||
|
||||
```tsx
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
const [count, setCount] = createSignal(0);
|
||||
|
||||
// Read value (getter)
|
||||
console.log(count()); // 0
|
||||
|
||||
// Update value (setter)
|
||||
setCount(1);
|
||||
setCount(prev => prev + 1); // Functional update
|
||||
```
|
||||
|
||||
**Options:**
|
||||
```tsx
|
||||
const [value, setValue] = createSignal(initialValue, {
|
||||
equals: false, // Always trigger updates, even if value unchanged
|
||||
name: "debugName" // For devtools
|
||||
});
|
||||
```
|
||||
|
||||
### Effects — Side Effects
|
||||
|
||||
```tsx
|
||||
import { createEffect } from "solid-js";
|
||||
|
||||
createEffect(() => {
|
||||
console.log("Count changed:", count());
|
||||
// Runs after render, re-runs when dependencies change
|
||||
});
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- Initial run: after render, before browser paint
|
||||
- Subsequent runs: when tracked dependencies change
|
||||
- Never runs during SSR or hydration
|
||||
- Use `onCleanup` for cleanup logic
|
||||
|
||||
### Memos — Derived/Cached Values
|
||||
|
||||
```tsx
|
||||
import { createMemo } from "solid-js";
|
||||
|
||||
const doubled = createMemo(() => count() * 2);
|
||||
|
||||
// Access like signal
|
||||
console.log(doubled()); // Cached, only recalculates when count changes
|
||||
```
|
||||
|
||||
Use memos when:
|
||||
- Derived value is expensive to compute
|
||||
- Derived value is accessed multiple times
|
||||
- You want to prevent downstream updates when result unchanged
|
||||
|
||||
### Resources — Async Data
|
||||
|
||||
```tsx
|
||||
import { createResource } from "solid-js";
|
||||
|
||||
const [user, { mutate, refetch }] = createResource(userId, fetchUser);
|
||||
|
||||
// In JSX
|
||||
<Show when={!user.loading} fallback={<Loading />}>
|
||||
<div>{user()?.name}</div>
|
||||
</Show>
|
||||
|
||||
// Resource properties
|
||||
user.loading // boolean
|
||||
user.error // error if failed
|
||||
user.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
|
||||
user.latest // last successful value
|
||||
```
|
||||
|
||||
## Stores — Complex State
|
||||
|
||||
For nested objects/arrays with fine-grained updates:
|
||||
|
||||
```tsx
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
const [state, setState] = createStore({
|
||||
user: { name: "John", age: 30 },
|
||||
todos: []
|
||||
});
|
||||
|
||||
// Path syntax updates
|
||||
setState("user", "name", "Jane");
|
||||
setState("todos", todos => [...todos, newTodo]);
|
||||
setState("todos", 0, "completed", true);
|
||||
|
||||
// Produce for immer-like updates
|
||||
import { produce } from "solid-js/store";
|
||||
setState(produce(s => {
|
||||
s.user.age++;
|
||||
s.todos.push(newTodo);
|
||||
}));
|
||||
```
|
||||
|
||||
**Store utilities:**
|
||||
- `produce` — Immer-like mutations
|
||||
- `reconcile` — Diff and patch data (for API responses)
|
||||
- `unwrap` — Get raw non-reactive object
|
||||
|
||||
## Components
|
||||
|
||||
### Basic Component
|
||||
|
||||
```tsx
|
||||
import { Component } from "solid-js";
|
||||
|
||||
const MyComponent: Component<{ name: string }> = (props) => {
|
||||
return <div>Hello, {props.name}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Props Handling
|
||||
|
||||
```tsx
|
||||
import { splitProps, mergeProps } from "solid-js";
|
||||
|
||||
// Default props
|
||||
const merged = mergeProps({ size: "medium" }, props);
|
||||
|
||||
// Split props (for spreading)
|
||||
const [local, others] = splitProps(props, ["class", "onClick"]);
|
||||
return <button class={local.class} {...others} />;
|
||||
```
|
||||
|
||||
**Props rules:**
|
||||
- Props are reactive getters — don't destructure at top level
|
||||
- Use `props.value` in JSX, not `const { value } = props`
|
||||
|
||||
### Children Helper
|
||||
|
||||
```tsx
|
||||
import { children } from "solid-js";
|
||||
|
||||
const Wrapper: Component = (props) => {
|
||||
const resolved = children(() => props.children);
|
||||
|
||||
createEffect(() => {
|
||||
console.log("Children:", resolved());
|
||||
});
|
||||
|
||||
return <div>{resolved()}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Control Flow Components
|
||||
|
||||
### Show — Conditional Rendering
|
||||
|
||||
```tsx
|
||||
import { Show } from "solid-js";
|
||||
|
||||
<Show when={user()} fallback={<Login />}>
|
||||
{(user) => <Profile user={user()} />}
|
||||
</Show>
|
||||
```
|
||||
|
||||
### For — List Rendering (keyed by reference)
|
||||
|
||||
```tsx
|
||||
import { For } from "solid-js";
|
||||
|
||||
<For each={items()} fallback={<Empty />}>
|
||||
{(item, index) => (
|
||||
<div>{index()}: {item.name}</div>
|
||||
)}
|
||||
</For>
|
||||
```
|
||||
|
||||
**Note:** `index` is a signal, `item` is the value.
|
||||
|
||||
### Index — List Rendering (keyed by index)
|
||||
|
||||
```tsx
|
||||
import { Index } from "solid-js";
|
||||
|
||||
<Index each={items()}>
|
||||
{(item, index) => (
|
||||
<input value={item().text} />
|
||||
)}
|
||||
</Index>
|
||||
```
|
||||
|
||||
**Note:** `item` is a signal, `index` is the value. Better for primitive arrays or inputs.
|
||||
|
||||
### Switch/Match — Multiple Conditions
|
||||
|
||||
```tsx
|
||||
import { Switch, Match } from "solid-js";
|
||||
|
||||
<Switch fallback={<Default />}>
|
||||
<Match when={state() === "loading"}>
|
||||
<Loading />
|
||||
</Match>
|
||||
<Match when={state() === "error"}>
|
||||
<Error />
|
||||
</Match>
|
||||
<Match when={state() === "success"}>
|
||||
<Success />
|
||||
</Match>
|
||||
</Switch>
|
||||
```
|
||||
|
||||
### Dynamic — Dynamic Component
|
||||
|
||||
```tsx
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
<Dynamic component={selected()} someProp="value" />
|
||||
```
|
||||
|
||||
### Portal — Render Outside DOM Hierarchy
|
||||
|
||||
```tsx
|
||||
import { Portal } from "solid-js/web";
|
||||
|
||||
<Portal mount={document.body}>
|
||||
<Modal />
|
||||
</Portal>
|
||||
```
|
||||
|
||||
### ErrorBoundary — Error Handling
|
||||
|
||||
```tsx
|
||||
import { ErrorBoundary } from "solid-js";
|
||||
|
||||
<ErrorBoundary fallback={(err, reset) => (
|
||||
<div>
|
||||
Error: {err.message}
|
||||
<button onClick={reset}>Retry</button>
|
||||
</div>
|
||||
)}>
|
||||
<RiskyComponent />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Suspense — Async Loading
|
||||
|
||||
```tsx
|
||||
import { Suspense } from "solid-js";
|
||||
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AsyncComponent />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
## Context API
|
||||
|
||||
```tsx
|
||||
import { createContext, useContext } from "solid-js";
|
||||
|
||||
// Create context
|
||||
const CounterContext = createContext<{
|
||||
count: () => number;
|
||||
increment: () => void;
|
||||
}>();
|
||||
|
||||
// Provider component
|
||||
export function CounterProvider(props) {
|
||||
const [count, setCount] = createSignal(0);
|
||||
|
||||
return (
|
||||
<CounterContext.Provider value={{
|
||||
count,
|
||||
increment: () => setCount(c => c + 1)
|
||||
}}>
|
||||
{props.children}
|
||||
</CounterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Consumer hook
|
||||
export function useCounter() {
|
||||
const ctx = useContext(CounterContext);
|
||||
if (!ctx) throw new Error("useCounter must be used within CounterProvider");
|
||||
return ctx;
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```tsx
|
||||
import { onMount, onCleanup } from "solid-js";
|
||||
|
||||
function MyComponent() {
|
||||
onMount(() => {
|
||||
console.log("Mounted");
|
||||
const handler = () => {};
|
||||
window.addEventListener("resize", handler);
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", handler);
|
||||
});
|
||||
});
|
||||
|
||||
return <div>Content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Refs
|
||||
|
||||
```tsx
|
||||
let inputRef: HTMLInputElement;
|
||||
|
||||
<input ref={inputRef} />
|
||||
<input ref={(el) => { /* el is the DOM element */ }} />
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
```tsx
|
||||
// Standard events (lowercase)
|
||||
<button onClick={handleClick}>Click</button>
|
||||
<button onClick={(e) => handleClick(e)}>Click</button>
|
||||
|
||||
// Delegated events (on:)
|
||||
<input on:input={handleInput} />
|
||||
|
||||
// Native events (on:) - not delegated
|
||||
<div on:scroll={handleScroll} />
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Conditional Classes
|
||||
|
||||
```tsx
|
||||
import { clsx } from "clsx"; // or classList
|
||||
|
||||
<div class={clsx("base", { active: isActive() })} />
|
||||
<div classList={{ active: isActive(), disabled: isDisabled() }} />
|
||||
```
|
||||
|
||||
### Batch Updates
|
||||
|
||||
```tsx
|
||||
import { batch } from "solid-js";
|
||||
|
||||
batch(() => {
|
||||
setName("John");
|
||||
setAge(30);
|
||||
// Effects run once after batch completes
|
||||
});
|
||||
```
|
||||
|
||||
### Untrack
|
||||
|
||||
```tsx
|
||||
import { untrack } from "solid-js";
|
||||
|
||||
createEffect(() => {
|
||||
console.log(count()); // tracked
|
||||
console.log(untrack(() => other())); // not tracked
|
||||
});
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
```tsx
|
||||
import type { Component, ParentComponent, JSX } from "solid-js";
|
||||
|
||||
// Basic component
|
||||
const Button: Component<{ label: string }> = (props) => (
|
||||
<button>{props.label}</button>
|
||||
);
|
||||
|
||||
// With children
|
||||
const Layout: ParentComponent<{ title: string }> = (props) => (
|
||||
<div>
|
||||
<h1>{props.title}</h1>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Event handler types
|
||||
const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (e) => {
|
||||
console.log(e.currentTarget);
|
||||
};
|
||||
```
|
||||
|
||||
## Project Setup
|
||||
|
||||
```bash
|
||||
# Create new project
|
||||
npm create solid@latest my-app
|
||||
|
||||
# With template
|
||||
npx degit solidjs/templates/ts my-app
|
||||
|
||||
# SolidStart
|
||||
npm create solid@latest my-app -- --template solidstart
|
||||
```
|
||||
|
||||
**vite.config.ts:**
|
||||
```ts
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()]
|
||||
});
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
1. **Destructuring props** — Breaks reactivity
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const { name } = props;
|
||||
|
||||
// ✅ Good
|
||||
props.name
|
||||
```
|
||||
|
||||
2. **Accessing signals outside tracking scope**
|
||||
```tsx
|
||||
// ❌ Won't update
|
||||
console.log(count());
|
||||
|
||||
// ✅ Will update
|
||||
createEffect(() => console.log(count()));
|
||||
```
|
||||
|
||||
3. **Forgetting to call signal getters**
|
||||
```tsx
|
||||
// ❌ Passes the function
|
||||
<div>{count}</div>
|
||||
|
||||
// ✅ Passes the value
|
||||
<div>{count()}</div>
|
||||
```
|
||||
|
||||
4. **Using array index as key** — Use `<For>` for reference-keyed, `<Index>` for index-keyed
|
||||
|
||||
5. **Side effects during render** — Use `createEffect` or `onMount`
|
||||
777
profiles/opencode/skill/solidjs/references/api_reference.md
Normal file
777
profiles/opencode/skill/solidjs/references/api_reference.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# SolidJS API Reference
|
||||
|
||||
Complete reference for all SolidJS primitives, utilities, and component APIs.
|
||||
|
||||
## Basic Reactivity
|
||||
|
||||
### createSignal
|
||||
|
||||
```tsx
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
const [getter, setter] = createSignal<T>(initialValue, options?);
|
||||
|
||||
// Options
|
||||
interface SignalOptions<T> {
|
||||
equals?: false | ((prev: T, next: T) => boolean);
|
||||
name?: string;
|
||||
internal?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```tsx
|
||||
const [count, setCount] = createSignal(0);
|
||||
const [user, setUser] = createSignal<User | null>(null);
|
||||
|
||||
// Always update
|
||||
const [data, setData] = createSignal(obj, { equals: false });
|
||||
|
||||
// Custom equality
|
||||
const [items, setItems] = createSignal([], {
|
||||
equals: (a, b) => a.length === b.length
|
||||
});
|
||||
|
||||
// Setter forms
|
||||
setCount(5); // Direct value
|
||||
setCount(prev => prev + 1); // Functional update
|
||||
```
|
||||
|
||||
### createEffect
|
||||
|
||||
```tsx
|
||||
import { createEffect } from "solid-js";
|
||||
|
||||
createEffect<T>(fn: (prev: T) => T, initialValue?: T, options?);
|
||||
|
||||
// Options
|
||||
interface EffectOptions {
|
||||
name?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```tsx
|
||||
// Basic
|
||||
createEffect(() => {
|
||||
console.log("Count:", count());
|
||||
});
|
||||
|
||||
// With previous value
|
||||
createEffect((prev) => {
|
||||
console.log("Changed from", prev, "to", count());
|
||||
return count();
|
||||
}, count());
|
||||
|
||||
// With cleanup
|
||||
createEffect(() => {
|
||||
const handler = () => {};
|
||||
window.addEventListener("resize", handler);
|
||||
onCleanup(() => window.removeEventListener("resize", handler));
|
||||
});
|
||||
```
|
||||
|
||||
### createMemo
|
||||
|
||||
```tsx
|
||||
import { createMemo } from "solid-js";
|
||||
|
||||
const getter = createMemo<T>(fn: (prev: T) => T, initialValue?: T, options?);
|
||||
|
||||
// Options
|
||||
interface MemoOptions<T> {
|
||||
equals?: false | ((prev: T, next: T) => boolean);
|
||||
name?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```tsx
|
||||
const doubled = createMemo(() => count() * 2);
|
||||
const filtered = createMemo(() => items().filter(i => i.active));
|
||||
|
||||
// Previous value
|
||||
const delta = createMemo((prev) => count() - prev, 0);
|
||||
```
|
||||
|
||||
### createResource
|
||||
|
||||
```tsx
|
||||
import { createResource } from "solid-js";
|
||||
|
||||
const [resource, { mutate, refetch }] = createResource(
|
||||
source?, // Optional reactive source
|
||||
fetcher, // (source, info) => Promise<T>
|
||||
options?
|
||||
);
|
||||
|
||||
// Resource properties
|
||||
resource() // T | undefined
|
||||
resource.loading // boolean
|
||||
resource.error // any
|
||||
resource.state // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
|
||||
resource.latest // T | undefined (last successful value)
|
||||
|
||||
// Options
|
||||
interface ResourceOptions<T> {
|
||||
initialValue?: T;
|
||||
name?: string;
|
||||
deferStream?: boolean;
|
||||
ssrLoadFrom?: "initial" | "server";
|
||||
storage?: (init: T) => [Accessor<T>, Setter<T>];
|
||||
onHydrated?: (key, info: { value: T }) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```tsx
|
||||
// Without source
|
||||
const [users] = createResource(fetchUsers);
|
||||
|
||||
// With source
|
||||
const [user] = createResource(userId, fetchUser);
|
||||
|
||||
// With options
|
||||
const [data] = createResource(id, fetchData, {
|
||||
initialValue: [],
|
||||
deferStream: true
|
||||
});
|
||||
|
||||
// Actions
|
||||
mutate(newValue); // Update locally
|
||||
refetch(); // Re-fetch
|
||||
refetch(customInfo); // Pass to fetcher's info.refetching
|
||||
```
|
||||
|
||||
## Stores
|
||||
|
||||
### createStore
|
||||
|
||||
```tsx
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
const [store, setStore] = createStore<T>(initialValue);
|
||||
```
|
||||
|
||||
**Update patterns:**
|
||||
```tsx
|
||||
const [state, setState] = createStore({
|
||||
user: { name: "John", age: 30 },
|
||||
todos: [{ id: 1, text: "Learn Solid", done: false }]
|
||||
});
|
||||
|
||||
// Path syntax
|
||||
setState("user", "name", "Jane");
|
||||
setState("user", "age", a => a + 1);
|
||||
setState("todos", 0, "done", true);
|
||||
|
||||
// Array operations
|
||||
setState("todos", t => [...t, newTodo]);
|
||||
setState("todos", todos.length, newTodo);
|
||||
|
||||
// Multiple paths
|
||||
setState("todos", { from: 0, to: 2 }, "done", true);
|
||||
setState("todos", [0, 2, 4], "done", true);
|
||||
setState("todos", i => i.done, "done", false);
|
||||
|
||||
// Object merge (shallow)
|
||||
setState("user", { age: 31 }); // Keeps other properties
|
||||
```
|
||||
|
||||
### produce
|
||||
|
||||
```tsx
|
||||
import { produce } from "solid-js/store";
|
||||
|
||||
setState(produce(draft => {
|
||||
draft.user.age++;
|
||||
draft.todos.push({ id: 2, text: "New", done: false });
|
||||
draft.todos[0].done = true;
|
||||
}));
|
||||
```
|
||||
|
||||
### reconcile
|
||||
|
||||
```tsx
|
||||
import { reconcile } from "solid-js/store";
|
||||
|
||||
// Replace with diff (minimal updates)
|
||||
setState("todos", reconcile(newTodosFromAPI));
|
||||
|
||||
// Options
|
||||
reconcile(data, { key: "id", merge: true });
|
||||
```
|
||||
|
||||
### unwrap
|
||||
|
||||
```tsx
|
||||
import { unwrap } from "solid-js/store";
|
||||
|
||||
const raw = unwrap(store); // Non-reactive plain object
|
||||
```
|
||||
|
||||
### createMutable
|
||||
|
||||
```tsx
|
||||
import { createMutable } from "solid-js/store";
|
||||
|
||||
const state = createMutable({
|
||||
count: 0,
|
||||
user: { name: "John" }
|
||||
});
|
||||
|
||||
// Direct mutation (like MobX)
|
||||
state.count++;
|
||||
state.user.name = "Jane";
|
||||
```
|
||||
|
||||
### modifyMutable
|
||||
|
||||
```tsx
|
||||
import { modifyMutable, reconcile, produce } from "solid-js/store";
|
||||
|
||||
modifyMutable(state, reconcile(newData));
|
||||
modifyMutable(state, produce(s => { s.count++ }));
|
||||
```
|
||||
|
||||
## Component APIs
|
||||
|
||||
### children
|
||||
|
||||
```tsx
|
||||
import { children } from "solid-js";
|
||||
|
||||
const resolved = children(() => props.children);
|
||||
|
||||
// Access
|
||||
resolved(); // JSX.Element | JSX.Element[]
|
||||
resolved.toArray(); // Always array
|
||||
```
|
||||
|
||||
### createContext / useContext
|
||||
|
||||
```tsx
|
||||
import { createContext, useContext } from "solid-js";
|
||||
|
||||
const MyContext = createContext<T>(defaultValue?);
|
||||
|
||||
// Provider
|
||||
<MyContext.Provider value={value}>
|
||||
{children}
|
||||
</MyContext.Provider>
|
||||
|
||||
// Consumer
|
||||
const value = useContext(MyContext);
|
||||
```
|
||||
|
||||
### createUniqueId
|
||||
|
||||
```tsx
|
||||
import { createUniqueId } from "solid-js";
|
||||
|
||||
const id = createUniqueId(); // "0", "1", etc.
|
||||
```
|
||||
|
||||
### lazy
|
||||
|
||||
```tsx
|
||||
import { lazy } from "solid-js";
|
||||
|
||||
const LazyComponent = lazy(() => import("./Component"));
|
||||
|
||||
// Use with Suspense
|
||||
<Suspense fallback={<Loading />}>
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### onMount
|
||||
|
||||
```tsx
|
||||
import { onMount } from "solid-js";
|
||||
|
||||
onMount(() => {
|
||||
// Runs once after initial render
|
||||
console.log("Mounted");
|
||||
});
|
||||
```
|
||||
|
||||
### onCleanup
|
||||
|
||||
```tsx
|
||||
import { onCleanup } from "solid-js";
|
||||
|
||||
// In component
|
||||
onCleanup(() => {
|
||||
console.log("Cleaning up");
|
||||
});
|
||||
|
||||
// In effect
|
||||
createEffect(() => {
|
||||
const sub = subscribe();
|
||||
onCleanup(() => sub.unsubscribe());
|
||||
});
|
||||
```
|
||||
|
||||
## Reactive Utilities
|
||||
|
||||
### batch
|
||||
|
||||
```tsx
|
||||
import { batch } from "solid-js";
|
||||
|
||||
batch(() => {
|
||||
setA(1);
|
||||
setB(2);
|
||||
setC(3);
|
||||
// Effects run once after batch
|
||||
});
|
||||
```
|
||||
|
||||
### untrack
|
||||
|
||||
```tsx
|
||||
import { untrack } from "solid-js";
|
||||
|
||||
createEffect(() => {
|
||||
console.log(a()); // Tracked
|
||||
console.log(untrack(() => b())); // Not tracked
|
||||
});
|
||||
```
|
||||
|
||||
### on
|
||||
|
||||
```tsx
|
||||
import { on } from "solid-js";
|
||||
|
||||
// Explicit dependencies
|
||||
createEffect(on(count, (value, prev) => {
|
||||
console.log("Count changed:", prev, "->", value);
|
||||
}));
|
||||
|
||||
// Multiple dependencies
|
||||
createEffect(on([a, b], ([a, b], [prevA, prevB]) => {
|
||||
console.log("Changed");
|
||||
}));
|
||||
|
||||
// Defer first run
|
||||
createEffect(on(count, (v) => console.log(v), { defer: true }));
|
||||
```
|
||||
|
||||
### mergeProps
|
||||
|
||||
```tsx
|
||||
import { mergeProps } from "solid-js";
|
||||
|
||||
const merged = mergeProps(
|
||||
{ size: "medium", color: "blue" }, // Defaults
|
||||
props // Overrides
|
||||
);
|
||||
```
|
||||
|
||||
### splitProps
|
||||
|
||||
```tsx
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
const [local, others] = splitProps(props, ["class", "onClick"]);
|
||||
// local.class, local.onClick
|
||||
// others contains everything else
|
||||
|
||||
const [a, b, rest] = splitProps(props, ["foo"], ["bar"]);
|
||||
```
|
||||
|
||||
### createRoot
|
||||
|
||||
```tsx
|
||||
import { createRoot } from "solid-js";
|
||||
|
||||
const dispose = createRoot((dispose) => {
|
||||
const [count, setCount] = createSignal(0);
|
||||
// Use signals...
|
||||
return dispose;
|
||||
});
|
||||
|
||||
// Later
|
||||
dispose();
|
||||
```
|
||||
|
||||
### getOwner / runWithOwner
|
||||
|
||||
```tsx
|
||||
import { getOwner, runWithOwner } from "solid-js";
|
||||
|
||||
const owner = getOwner();
|
||||
|
||||
// Later, in async code
|
||||
runWithOwner(owner, () => {
|
||||
createEffect(() => {
|
||||
// This effect has proper ownership
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### mapArray
|
||||
|
||||
```tsx
|
||||
import { mapArray } from "solid-js";
|
||||
|
||||
const mapped = mapArray(
|
||||
() => items(),
|
||||
(item, index) => ({ ...item, doubled: item.value * 2 })
|
||||
);
|
||||
```
|
||||
|
||||
### indexArray
|
||||
|
||||
```tsx
|
||||
import { indexArray } from "solid-js";
|
||||
|
||||
const mapped = indexArray(
|
||||
() => items(),
|
||||
(item, index) => <div>{index}: {item().name}</div>
|
||||
);
|
||||
```
|
||||
|
||||
### observable
|
||||
|
||||
```tsx
|
||||
import { observable } from "solid-js";
|
||||
|
||||
const obs = observable(signal);
|
||||
obs.subscribe((value) => console.log(value));
|
||||
```
|
||||
|
||||
### from
|
||||
|
||||
```tsx
|
||||
import { from } from "solid-js";
|
||||
|
||||
// Convert observable/subscribable to signal
|
||||
const signal = from(rxObservable);
|
||||
const signal = from((set) => {
|
||||
const unsub = subscribe(set);
|
||||
return unsub;
|
||||
});
|
||||
```
|
||||
|
||||
### catchError
|
||||
|
||||
```tsx
|
||||
import { catchError } from "solid-js";
|
||||
|
||||
catchError(
|
||||
() => riskyOperation(),
|
||||
(err) => console.error("Error:", err)
|
||||
);
|
||||
```
|
||||
|
||||
## Secondary Primitives
|
||||
|
||||
### createComputed
|
||||
|
||||
```tsx
|
||||
import { createComputed } from "solid-js";
|
||||
|
||||
// Like createEffect but runs during render phase
|
||||
createComputed(() => {
|
||||
setDerived(source() * 2);
|
||||
});
|
||||
```
|
||||
|
||||
### createRenderEffect
|
||||
|
||||
```tsx
|
||||
import { createRenderEffect } from "solid-js";
|
||||
|
||||
// Runs before paint (for DOM measurements)
|
||||
createRenderEffect(() => {
|
||||
const height = element.offsetHeight;
|
||||
});
|
||||
```
|
||||
|
||||
### createDeferred
|
||||
|
||||
```tsx
|
||||
import { createDeferred } from "solid-js";
|
||||
|
||||
// Returns value after idle time
|
||||
const deferred = createDeferred(() => expensiveComputation(), {
|
||||
timeoutMs: 1000
|
||||
});
|
||||
```
|
||||
|
||||
### createReaction
|
||||
|
||||
```tsx
|
||||
import { createReaction } from "solid-js";
|
||||
|
||||
const track = createReaction(() => {
|
||||
console.log("Something changed");
|
||||
});
|
||||
|
||||
track(() => count()); // Start tracking
|
||||
```
|
||||
|
||||
### createSelector
|
||||
|
||||
```tsx
|
||||
import { createSelector } from "solid-js";
|
||||
|
||||
const isSelected = createSelector(selectedId);
|
||||
|
||||
<For each={items()}>
|
||||
{(item) => (
|
||||
<div class={isSelected(item.id) ? "selected" : ""}>
|
||||
{item.name}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Show
|
||||
|
||||
```tsx
|
||||
<Show when={condition()} fallback={<Fallback />}>
|
||||
<Content />
|
||||
</Show>
|
||||
|
||||
// With callback (narrowed type)
|
||||
<Show when={user()}>
|
||||
{(user) => <div>{user().name}</div>}
|
||||
</Show>
|
||||
```
|
||||
|
||||
### For
|
||||
|
||||
```tsx
|
||||
<For each={items()} fallback={<Empty />}>
|
||||
{(item, index) => <div>{index()}: {item.name}</div>}
|
||||
</For>
|
||||
```
|
||||
|
||||
### Index
|
||||
|
||||
```tsx
|
||||
<Index each={items()} fallback={<Empty />}>
|
||||
{(item, index) => <input value={item().text} />}
|
||||
</Index>
|
||||
```
|
||||
|
||||
### Switch / Match
|
||||
|
||||
```tsx
|
||||
<Switch fallback={<Default />}>
|
||||
<Match when={state() === "loading"}>
|
||||
<Loading />
|
||||
</Match>
|
||||
<Match when={state() === "error"}>
|
||||
<Error />
|
||||
</Match>
|
||||
</Switch>
|
||||
```
|
||||
|
||||
### Dynamic
|
||||
|
||||
```tsx
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
<Dynamic component={selected()} prop={value} />
|
||||
<Dynamic component="div" class="dynamic">Content</Dynamic>
|
||||
```
|
||||
|
||||
### Portal
|
||||
|
||||
```tsx
|
||||
import { Portal } from "solid-js/web";
|
||||
|
||||
<Portal mount={document.body}>
|
||||
<Modal />
|
||||
</Portal>
|
||||
```
|
||||
|
||||
### ErrorBoundary
|
||||
|
||||
```tsx
|
||||
<ErrorBoundary fallback={(err, reset) => (
|
||||
<div>
|
||||
<p>Error: {err.message}</p>
|
||||
<button onClick={reset}>Retry</button>
|
||||
</div>
|
||||
)}>
|
||||
<Content />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Suspense
|
||||
|
||||
```tsx
|
||||
<Suspense fallback={<Loading />}>
|
||||
<AsyncContent />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
### SuspenseList
|
||||
|
||||
```tsx
|
||||
<SuspenseList revealOrder="forwards" tail="collapsed">
|
||||
<Suspense fallback={<Loading />}><Item1 /></Suspense>
|
||||
<Suspense fallback={<Loading />}><Item2 /></Suspense>
|
||||
<Suspense fallback={<Loading />}><Item3 /></Suspense>
|
||||
</SuspenseList>
|
||||
```
|
||||
|
||||
## Rendering
|
||||
|
||||
### render
|
||||
|
||||
```tsx
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
const dispose = render(() => <App />, document.getElementById("root")!);
|
||||
|
||||
// Cleanup
|
||||
dispose();
|
||||
```
|
||||
|
||||
### hydrate
|
||||
|
||||
```tsx
|
||||
import { hydrate } from "solid-js/web";
|
||||
|
||||
hydrate(() => <App />, document.getElementById("root")!);
|
||||
```
|
||||
|
||||
### renderToString
|
||||
|
||||
```tsx
|
||||
import { renderToString } from "solid-js/web";
|
||||
|
||||
const html = renderToString(() => <App />);
|
||||
```
|
||||
|
||||
### renderToStringAsync
|
||||
|
||||
```tsx
|
||||
import { renderToStringAsync } from "solid-js/web";
|
||||
|
||||
const html = await renderToStringAsync(() => <App />);
|
||||
```
|
||||
|
||||
### renderToStream
|
||||
|
||||
```tsx
|
||||
import { renderToStream } from "solid-js/web";
|
||||
|
||||
const stream = renderToStream(() => <App />);
|
||||
stream.pipe(res);
|
||||
```
|
||||
|
||||
### isServer
|
||||
|
||||
```tsx
|
||||
import { isServer } from "solid-js/web";
|
||||
|
||||
if (isServer) {
|
||||
// Server-only code
|
||||
}
|
||||
```
|
||||
|
||||
## JSX Attributes
|
||||
|
||||
### ref
|
||||
|
||||
```tsx
|
||||
let el: HTMLDivElement;
|
||||
<div ref={el} />
|
||||
<div ref={(e) => console.log(e)} />
|
||||
```
|
||||
|
||||
### classList
|
||||
|
||||
```tsx
|
||||
<div classList={{ active: isActive(), disabled: isDisabled() }} />
|
||||
```
|
||||
|
||||
### style
|
||||
|
||||
```tsx
|
||||
<div style={{ color: "red", "font-size": "14px" }} />
|
||||
<div style={`color: ${color()}`} />
|
||||
```
|
||||
|
||||
### on:event (native)
|
||||
|
||||
```tsx
|
||||
<div on:click={handleClick} />
|
||||
<div on:scroll={handleScroll} />
|
||||
```
|
||||
|
||||
### use:directive
|
||||
|
||||
```tsx
|
||||
function clickOutside(el: HTMLElement, accessor: () => () => void) {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (!el.contains(e.target as Node)) accessor()();
|
||||
};
|
||||
document.addEventListener("click", handler);
|
||||
onCleanup(() => document.removeEventListener("click", handler));
|
||||
}
|
||||
|
||||
<div use:clickOutside={() => setOpen(false)} />
|
||||
```
|
||||
|
||||
### prop:property
|
||||
|
||||
```tsx
|
||||
<input prop:value={value()} /> // Set as property, not attribute
|
||||
```
|
||||
|
||||
### attr:attribute
|
||||
|
||||
```tsx
|
||||
<div attr:data-custom={value()} /> // Force attribute
|
||||
```
|
||||
|
||||
### bool:attribute
|
||||
|
||||
```tsx
|
||||
<input bool:disabled={isDisabled()} />
|
||||
```
|
||||
|
||||
### @once
|
||||
|
||||
```tsx
|
||||
<div title={/*@once*/ staticValue} /> // Never updates
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
Component,
|
||||
ParentComponent,
|
||||
FlowComponent,
|
||||
VoidComponent,
|
||||
JSX,
|
||||
Accessor,
|
||||
Setter,
|
||||
Signal,
|
||||
Resource,
|
||||
Owner
|
||||
} from "solid-js";
|
||||
|
||||
// Component types
|
||||
const MyComponent: Component<Props> = (props) => <div />;
|
||||
const Parent: ParentComponent<Props> = (props) => <div>{props.children}</div>;
|
||||
const Flow: FlowComponent<Props, Item> = (props) => props.children(item);
|
||||
const Void: VoidComponent<Props> = (props) => <input />;
|
||||
|
||||
// Event types
|
||||
type Handler = JSX.EventHandler<HTMLButtonElement, MouseEvent>;
|
||||
type ChangeHandler = JSX.ChangeEventHandler<HTMLInputElement>;
|
||||
```
|
||||
720
profiles/opencode/skill/solidjs/references/patterns.md
Normal file
720
profiles/opencode/skill/solidjs/references/patterns.md
Normal file
@@ -0,0 +1,720 @@
|
||||
# SolidJS Patterns & Best Practices
|
||||
|
||||
Common patterns, recipes, and best practices for SolidJS development.
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Controlled vs Uncontrolled Inputs
|
||||
|
||||
**Controlled:**
|
||||
```tsx
|
||||
function ControlledInput() {
|
||||
const [value, setValue] = createSignal("");
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value()}
|
||||
onInput={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Uncontrolled with ref:**
|
||||
```tsx
|
||||
function UncontrolledInput() {
|
||||
let inputRef: HTMLInputElement;
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log(inputRef.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input ref={inputRef!} />
|
||||
<button onClick={handleSubmit}>Submit</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Compound Components
|
||||
|
||||
```tsx
|
||||
const Tabs = {
|
||||
Root: (props: ParentProps<{ defaultTab?: string }>) => {
|
||||
const [activeTab, setActiveTab] = createSignal(props.defaultTab ?? "");
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
||||
<div class="tabs">{props.children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
},
|
||||
|
||||
List: (props: ParentProps) => (
|
||||
<div class="tabs-list" role="tablist">{props.children}</div>
|
||||
),
|
||||
|
||||
Tab: (props: ParentProps<{ value: string }>) => {
|
||||
const ctx = useTabsContext();
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={ctx.activeTab() === props.value}
|
||||
onClick={() => ctx.setActiveTab(props.value)}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
|
||||
Panel: (props: ParentProps<{ value: string }>) => {
|
||||
const ctx = useTabsContext();
|
||||
return (
|
||||
<Show when={ctx.activeTab() === props.value}>
|
||||
<div role="tabpanel">{props.children}</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Usage
|
||||
<Tabs.Root defaultTab="first">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="first">First</Tabs.Tab>
|
||||
<Tabs.Tab value="second">Second</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="first">First Content</Tabs.Panel>
|
||||
<Tabs.Panel value="second">Second Content</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
```
|
||||
|
||||
### Render Props
|
||||
|
||||
```tsx
|
||||
function MouseTracker(props: {
|
||||
children: (pos: { x: number; y: number }) => JSX.Element;
|
||||
}) {
|
||||
const [pos, setPos] = createSignal({ x: 0, y: 0 });
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
|
||||
window.addEventListener("mousemove", handler);
|
||||
onCleanup(() => window.removeEventListener("mousemove", handler));
|
||||
});
|
||||
|
||||
return <>{props.children(pos())}</>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MouseTracker>
|
||||
{(pos) => <div>Mouse: {pos.x}, {pos.y}</div>}
|
||||
</MouseTracker>
|
||||
```
|
||||
|
||||
### Higher-Order Components
|
||||
|
||||
```tsx
|
||||
function withAuth<P extends object>(Component: Component<P>) {
|
||||
return (props: P) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<Show when={user()} fallback={<Redirect to="/login" />}>
|
||||
<Component {...props} />
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const ProtectedDashboard = withAuth(Dashboard);
|
||||
```
|
||||
|
||||
### Polymorphic Components
|
||||
|
||||
```tsx
|
||||
type PolymorphicProps<E extends keyof JSX.IntrinsicElements> = {
|
||||
as?: E;
|
||||
} & JSX.IntrinsicElements[E];
|
||||
|
||||
function Box<E extends keyof JSX.IntrinsicElements = "div">(
|
||||
props: PolymorphicProps<E>
|
||||
) {
|
||||
const [local, others] = splitProps(props as PolymorphicProps<"div">, ["as"]);
|
||||
|
||||
return <Dynamic component={local.as || "div"} {...others} />;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<Box>Default div</Box>
|
||||
<Box as="section">Section element</Box>
|
||||
<Box as="button" onClick={handleClick}>Button</Box>
|
||||
```
|
||||
|
||||
## State Patterns
|
||||
|
||||
### Derived State with Multiple Sources
|
||||
|
||||
```tsx
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = createSignal("");
|
||||
const [filters, setFilters] = createSignal({ category: "all" });
|
||||
|
||||
const results = createMemo(() => {
|
||||
const q = query().toLowerCase();
|
||||
const f = filters();
|
||||
|
||||
return allItems()
|
||||
.filter(item => item.name.toLowerCase().includes(q))
|
||||
.filter(item => f.category === "all" || item.category === f.category);
|
||||
});
|
||||
|
||||
return <For each={results()}>{item => <Item item={item} />}</For>;
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine Pattern
|
||||
|
||||
```tsx
|
||||
type State = "idle" | "loading" | "success" | "error";
|
||||
type Event = { type: "FETCH" } | { type: "SUCCESS"; data: any } | { type: "ERROR"; error: Error };
|
||||
|
||||
function createMachine(initial: State) {
|
||||
const [state, setState] = createSignal<State>(initial);
|
||||
const [data, setData] = createSignal<any>(null);
|
||||
const [error, setError] = createSignal<Error | null>(null);
|
||||
|
||||
const send = (event: Event) => {
|
||||
const current = state();
|
||||
|
||||
switch (current) {
|
||||
case "idle":
|
||||
if (event.type === "FETCH") setState("loading");
|
||||
break;
|
||||
case "loading":
|
||||
if (event.type === "SUCCESS") {
|
||||
setData(event.data);
|
||||
setState("success");
|
||||
} else if (event.type === "ERROR") {
|
||||
setError(event.error);
|
||||
setState("error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return { state, data, error, send };
|
||||
}
|
||||
```
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
```tsx
|
||||
const [todos, setTodos] = createStore<Todo[]>([]);
|
||||
|
||||
async function deleteTodo(id: string) {
|
||||
const original = [...unwrap(todos)];
|
||||
|
||||
// Optimistic remove
|
||||
setTodos(todos => todos.filter(t => t.id !== id));
|
||||
|
||||
try {
|
||||
await api.deleteTodo(id);
|
||||
} catch {
|
||||
// Rollback on error
|
||||
setTodos(reconcile(original));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Undo/Redo
|
||||
|
||||
```tsx
|
||||
function createHistory<T>(initial: T) {
|
||||
const [past, setPast] = createSignal<T[]>([]);
|
||||
const [present, setPresent] = createSignal<T>(initial);
|
||||
const [future, setFuture] = createSignal<T[]>([]);
|
||||
|
||||
const canUndo = () => past().length > 0;
|
||||
const canRedo = () => future().length > 0;
|
||||
|
||||
const set = (value: T | ((prev: T) => T)) => {
|
||||
const newValue = typeof value === "function"
|
||||
? (value as (prev: T) => T)(present())
|
||||
: value;
|
||||
|
||||
setPast(p => [...p, present()]);
|
||||
setPresent(newValue);
|
||||
setFuture([]);
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
if (!canUndo()) return;
|
||||
|
||||
const previous = past()[past().length - 1];
|
||||
setPast(p => p.slice(0, -1));
|
||||
setFuture(f => [present(), ...f]);
|
||||
setPresent(previous);
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
if (!canRedo()) return;
|
||||
|
||||
const next = future()[0];
|
||||
setPast(p => [...p, present()]);
|
||||
setFuture(f => f.slice(1));
|
||||
setPresent(next);
|
||||
};
|
||||
|
||||
return { value: present, set, undo, redo, canUndo, canRedo };
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks/Primitives
|
||||
|
||||
### useLocalStorage
|
||||
|
||||
```tsx
|
||||
function createLocalStorage<T>(key: string, initialValue: T) {
|
||||
const stored = localStorage.getItem(key);
|
||||
const initial = stored ? JSON.parse(stored) : initialValue;
|
||||
|
||||
const [value, setValue] = createSignal<T>(initial);
|
||||
|
||||
createEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(value()));
|
||||
});
|
||||
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
```
|
||||
|
||||
### useDebounce
|
||||
|
||||
```tsx
|
||||
function createDebounce<T>(source: () => T, delay: number) {
|
||||
const [debounced, setDebounced] = createSignal<T>(source());
|
||||
|
||||
createEffect(() => {
|
||||
const value = source();
|
||||
const timer = setTimeout(() => setDebounced(() => value), delay);
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
});
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const debouncedQuery = createDebounce(query, 300);
|
||||
```
|
||||
|
||||
### useThrottle
|
||||
|
||||
```tsx
|
||||
function createThrottle<T>(source: () => T, delay: number) {
|
||||
const [throttled, setThrottled] = createSignal<T>(source());
|
||||
let lastRun = 0;
|
||||
|
||||
createEffect(() => {
|
||||
const value = source();
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastRun >= delay) {
|
||||
lastRun = now;
|
||||
setThrottled(() => value);
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
lastRun = Date.now();
|
||||
setThrottled(() => value);
|
||||
}, delay - (now - lastRun));
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
}
|
||||
});
|
||||
|
||||
return throttled;
|
||||
}
|
||||
```
|
||||
|
||||
### useMediaQuery
|
||||
|
||||
```tsx
|
||||
function createMediaQuery(query: string) {
|
||||
const mql = window.matchMedia(query);
|
||||
const [matches, setMatches] = createSignal(mql.matches);
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
onCleanup(() => mql.removeEventListener("change", handler));
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const isMobile = createMediaQuery("(max-width: 768px)");
|
||||
```
|
||||
|
||||
### useClickOutside
|
||||
|
||||
```tsx
|
||||
function createClickOutside(
|
||||
ref: () => HTMLElement | undefined,
|
||||
callback: () => void
|
||||
) {
|
||||
onMount(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
const el = ref();
|
||||
if (el && !el.contains(e.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handler);
|
||||
onCleanup(() => document.removeEventListener("click", handler));
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
let dropdownRef: HTMLDivElement;
|
||||
createClickOutside(() => dropdownRef, () => setOpen(false));
|
||||
```
|
||||
|
||||
### useIntersectionObserver
|
||||
|
||||
```tsx
|
||||
function createIntersectionObserver(
|
||||
ref: () => HTMLElement | undefined,
|
||||
options?: IntersectionObserverInit
|
||||
) {
|
||||
const [isIntersecting, setIsIntersecting] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const el = ref();
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
}, options);
|
||||
|
||||
observer.observe(el);
|
||||
onCleanup(() => observer.disconnect());
|
||||
});
|
||||
|
||||
return isIntersecting;
|
||||
}
|
||||
```
|
||||
|
||||
## Form Patterns
|
||||
|
||||
### Form Validation
|
||||
|
||||
```tsx
|
||||
function createForm<T extends Record<string, any>>(initial: T) {
|
||||
const [values, setValues] = createStore<T>(initial);
|
||||
const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({});
|
||||
const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({});
|
||||
|
||||
const handleChange = (field: keyof T) => (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
setValues(field as any, target.value as any);
|
||||
};
|
||||
|
||||
const handleBlur = (field: keyof T) => () => {
|
||||
setTouched(field as any, true);
|
||||
};
|
||||
|
||||
const validate = (validators: Partial<Record<keyof T, (v: any) => string | undefined>>) => {
|
||||
let isValid = true;
|
||||
|
||||
for (const [field, validator] of Object.entries(validators)) {
|
||||
if (validator) {
|
||||
const error = validator(values[field as keyof T]);
|
||||
setErrors(field as any, error as any);
|
||||
if (error) isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
return { values, errors, touched, handleChange, handleBlur, validate, setValues };
|
||||
}
|
||||
|
||||
// Usage
|
||||
const form = createForm({ email: "", password: "" });
|
||||
|
||||
<input
|
||||
value={form.values.email}
|
||||
onInput={form.handleChange("email")}
|
||||
onBlur={form.handleBlur("email")}
|
||||
/>
|
||||
<Show when={form.touched.email && form.errors.email}>
|
||||
<span class="error">{form.errors.email}</span>
|
||||
</Show>
|
||||
```
|
||||
|
||||
### Field Array
|
||||
|
||||
```tsx
|
||||
function createFieldArray<T>(initial: T[] = []) {
|
||||
const [fields, setFields] = createStore<T[]>(initial);
|
||||
|
||||
const append = (value: T) => setFields(f => [...f, value]);
|
||||
const remove = (index: number) => setFields(f => f.filter((_, i) => i !== index));
|
||||
const update = (index: number, value: Partial<T>) => setFields(index, v => ({ ...v, ...value }));
|
||||
const move = (from: number, to: number) => {
|
||||
setFields(produce(f => {
|
||||
const [item] = f.splice(from, 1);
|
||||
f.splice(to, 0, item);
|
||||
}));
|
||||
};
|
||||
|
||||
return { fields, append, remove, update, move };
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Virtualized List
|
||||
|
||||
```tsx
|
||||
function VirtualList<T>(props: {
|
||||
items: T[];
|
||||
itemHeight: number;
|
||||
height: number;
|
||||
renderItem: (item: T, index: number) => JSX.Element;
|
||||
}) {
|
||||
const [scrollTop, setScrollTop] = createSignal(0);
|
||||
|
||||
const startIndex = createMemo(() =>
|
||||
Math.floor(scrollTop() / props.itemHeight)
|
||||
);
|
||||
|
||||
const visibleCount = createMemo(() =>
|
||||
Math.ceil(props.height / props.itemHeight) + 1
|
||||
);
|
||||
|
||||
const visibleItems = createMemo(() =>
|
||||
props.items.slice(startIndex(), startIndex() + visibleCount())
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: `${props.height}px`, overflow: "auto" }}
|
||||
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
||||
>
|
||||
<div style={{ height: `${props.items.length * props.itemHeight}px`, position: "relative" }}>
|
||||
<For each={visibleItems()}>
|
||||
{(item, i) => (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: `${(startIndex() + i()) * props.itemHeight}px`,
|
||||
height: `${props.itemHeight}px`
|
||||
}}>
|
||||
{props.renderItem(item, startIndex() + i())}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Loading with Intersection Observer
|
||||
|
||||
```tsx
|
||||
function LazyLoad(props: ParentProps<{ placeholder?: JSX.Element }>) {
|
||||
let ref: HTMLDivElement;
|
||||
const [isVisible, setIsVisible] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "100px" }
|
||||
);
|
||||
observer.observe(ref);
|
||||
onCleanup(() => observer.disconnect());
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref!}>
|
||||
<Show when={isVisible()} fallback={props.placeholder}>
|
||||
{props.children}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Memoized Component
|
||||
|
||||
```tsx
|
||||
// For expensive components that shouldn't re-render on parent updates
|
||||
function MemoizedExpensiveList(props: { items: Item[] }) {
|
||||
// Component only re-renders when items actually change
|
||||
return (
|
||||
<For each={props.items}>
|
||||
{(item) => <ExpensiveItem item={item} />}
|
||||
</For>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Component Testing
|
||||
|
||||
```tsx
|
||||
import { render, fireEvent, screen } from "@solidjs/testing-library";
|
||||
|
||||
test("Counter increments", async () => {
|
||||
render(() => <Counter />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /increment/i });
|
||||
expect(screen.getByText("Count: 0")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByText("Count: 1")).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
### Testing with Context
|
||||
|
||||
```tsx
|
||||
function renderWithContext(component: () => JSX.Element) {
|
||||
return render(() => (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
{component()}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
));
|
||||
}
|
||||
|
||||
test("Dashboard shows user", () => {
|
||||
renderWithContext(() => <Dashboard />);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Async Components
|
||||
|
||||
```tsx
|
||||
import { render, waitFor, screen } from "@solidjs/testing-library";
|
||||
|
||||
test("Loads user data", async () => {
|
||||
render(() => <UserProfile userId="123" />);
|
||||
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Global Error Handler
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(err, reset) => (
|
||||
<ErrorPage error={err} onRetry={reset} />
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<AppLoader />}>
|
||||
<Router>
|
||||
{/* Routes */}
|
||||
</Router>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Async Error Handling
|
||||
|
||||
```tsx
|
||||
function DataComponent() {
|
||||
const [data] = createResource(fetchData);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={data.loading}>
|
||||
<Loading />
|
||||
</Match>
|
||||
<Match when={data.error}>
|
||||
<Error error={data.error} onRetry={() => refetch()} />
|
||||
</Match>
|
||||
<Match when={data()}>
|
||||
{(data) => <Content data={data()} />}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Patterns
|
||||
|
||||
### Focus Management
|
||||
|
||||
```tsx
|
||||
function Modal(props: ParentProps<{ isOpen: boolean; onClose: () => void }>) {
|
||||
let dialogRef: HTMLDivElement;
|
||||
let previousFocus: HTMLElement | null;
|
||||
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
previousFocus = document.activeElement as HTMLElement;
|
||||
dialogRef.focus();
|
||||
} else if (previousFocus) {
|
||||
previousFocus.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={props.isOpen}>
|
||||
<Portal>
|
||||
<div
|
||||
ref={dialogRef!}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => e.key === "Escape" && props.onClose()}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Live Regions
|
||||
|
||||
```tsx
|
||||
function Notifications() {
|
||||
const [message, setMessage] = createSignal("");
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="sr-only"
|
||||
>
|
||||
{message()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -38,6 +38,23 @@ Phase: CLARIFY | Waiting for: answers to questions 1-4
|
||||
3. Ask 3-5 pointed questions that would change the approach. USE YOUR QUESTION TOOL.
|
||||
4. **Wait for responses**
|
||||
|
||||
**IMPORTANT: Always use the `question` tool to ask clarifying questions.** Do NOT output questions as freeform text. The question tool provides structured options and better UX. Example:
|
||||
|
||||
```
|
||||
question({
|
||||
questions: [{
|
||||
header: "Scope",
|
||||
question: "Which subsystems need detailed specs?",
|
||||
options: [
|
||||
{ label: "VCS layer", description: "jj-lib + gix unified interface" },
|
||||
{ label: "Review workflow", description: "GitHub PR-style local review" },
|
||||
{ label: "Event system", description: "pub/sub + persistence" }
|
||||
],
|
||||
multiple: true
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
| Category | Example |
|
||||
|----------|---------|
|
||||
| Scope | "Share where? Social media? Direct link? Embed?" |
|
||||
|
||||
@@ -45,7 +45,6 @@ with pkgs;
|
||||
tree-sitter
|
||||
unzip
|
||||
vivid
|
||||
watchman
|
||||
zip
|
||||
]
|
||||
++ lib.optionals stdenv.isDarwin [
|
||||
@@ -53,6 +52,7 @@ with pkgs;
|
||||
alcove
|
||||
dockutil
|
||||
mas
|
||||
openusage
|
||||
raycast
|
||||
tailscale
|
||||
xcodes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
programs.starship = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
enableNushellIntegration = true;
|
||||
settings = {
|
||||
add_newline = true;
|
||||
command_timeout = 2000;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
programs.zoxide = {
|
||||
enable = true;
|
||||
enableFishIntegration = true;
|
||||
enableNushellIntegration = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:l7jYCSQE0BwYOoIMHgGOmMrWz5s=,iv:4TugFnfmzoeroq6SfRLD36gSSBHGVT6CxQE4Pyp1Ibc=,tag:HOTLlXGab05u+qREtyxAeg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1njjegjjdqzfnrr54f536yl4lduqgna3wuv7ef6vtl9jw5cju0grsgy62tm",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFTmVlZmxIMGFBbE1RYkNN\nbTh1NG5BK1JCTlZ1MzBtSS9NRXRVM3phN3lrCm9qSlpEQjFGZTBsME9ndXhYdHZv\ncW1oek5hU1pjQXhzT0cvTks3VEozdHcKLS0tIG1TUWJKSE1KRVlXMVdydjBhY1ZS\nWXRhNFA2aHZTT2pud0ZXOFpjVldKYjgKYyH7k0BW/sf3vDQLPaZoB2VHoyKmCkWy\nwTolEp4vkuzZld4KPdDW4jYL8kt8Fwa8TNlPKMUOvY0gt82pbJ1MAA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age187jl7e4k9n4guygkmpuqzeh0wenefwrfkpvuyhvwjrjwxqpzassqq3x67j",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5Y2ZpWlhXUll4WUxheENj\nRjlwK2dJUjBOU1lhV1dKWFhiMlJiQ0Q2Q0VrCnJac0ppdm9URW14TUwrVUdwN2x4\ncUgxSGRTM09ORUk0VXhUcVBxS1Z2a0EKLS0tIGhOSmc2L3FKOXdqdG5Da210aXF4\nZ2h0dW45K2dlZGJQMmxneE1IbXMvWmsKkBfh09E6o2uvNegq5pZgUBWOYjREDDyg\nHEgV7G4cWJBPpBFwS1gLGyRZ0TWrmoXCqF8I22BMG6tq94AGrKqFzg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1dqt3znmzcgghsjjzzax0pf0eyu95h0p7kaf5v988ysjv7fl7lumsatl048",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCc1B0Ylo5K1poUXpkcFZk\neGJSQmNBVU00RTdLcTNBRUN2N3cySFE0cUdVCjNmZlpGM0VUOWdvbVQ3U1BoRWJ1\na3kwK3JxQ0IrZ0NUaW5sTmZlWmd4MncKLS0tIHozZ29UbTNPZDBjTUl3WlB2YmFs\nS0IrN3lLK2YyM0ROTERtQlppbjVOdWMKtpLveHYL4RfEpCBLt4c4R/NVg1QF+if1\nz26bWNQseIsPtlIk4GImJZhUhbr3sDY81gcl7sd8XGpze7EAVM262g==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1ez6j3r5wdp0tjy7n5qzv5vfakdc2nh2zeu388zu7a80l0thv052syxq5e2",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBySmdCaVRYV2RjOFR4dFVx\nYmd1VlRzQmUwR1RwcTNJdTlxQnFFZXVscnlFCnRVOWQwa3VVUUlHQTF0WHBXb0FL\nMEViVXVDeWJLZnBLdWlUZTZFQVFqYlUKLS0tIHBGdXdPM2tOdzR0Qm5UT2F6dnNF\nTytWcXNYMEJSTU8xOExMK1MwQUNhdk0KVBbrhhwh+Yup+CW3Y+q9RoQ3XFjBfTLb\nzDbCZBhNx9HP7Q8mlndYPkJc3aGfpKxHpaniBLqzDKNKJ5OE4kzY3Q==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1tlymdmaukhwupzrhszspp26lgd8s64rw4vu9lwc7gsgrjm78095s9fe9l3",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtVnl0ckE1REtQbW8ybEtF\nWG1VNkFtMUVlNk04SnIySVFvM2pkV1dTMTNNCkV6R2NzQzM4WXRIdzJrVEo4L2FX\nQ1dmV2YwNHdMMVJUNmZWRktvK2s1dUUKLS0tIE5nUXNack5meUFzcWxhOTNGLzdG\naGlVYURRK2hLK0lNNm4wYTUrdXptLzAKZgN1tY1G3Jso1+CT0LQQ4I49CgdCECpe\n1wRdgaWI8P4ep2S7QO3Vu+MuoyOgVgGJdG/HzsEAAqJ0XMSBWpeFXg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1h537hhl5qgew5sswjp7xf7d4j4aq0gg9s5flnr8twm2smnqyudhqmum8uy",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3YnVBeGl5akg2b3BidW5m\nMjBnWWxReTFoNTBYVm83ZkhtSHUvTmtNSGxzCk5XcmVJZ0hpRURIT09teHFzOURt\nK25JSFJCNU5mb2k4ZHBTU0Q0emFtYWMKLS0tIEtqMlFNckZmQk15Z0xoT1BrUWgv\nN0VJT1RjOG1ZOVU1UklKTm5TUEhLQmsKP03juyke7ZmLGx+BHE596d18TWVLFuUV\nP1pK0QlwtRL0l/6r7l4hXN9gJ8JU+zO5NTcLtvvHdSL188q3TX//UQ==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-01-07T19:19:41Z",
|
||||
"mac": "ENC[AES256_GCM,data:SnMD9+jpySE35ylbHInXfsIQ/Nq6FBpunlhgJSOnYCQLE9vGc5Rtkg8cYlqFBz82Ukjk2EJafKcjDgBgTx6+JcYC8idM7yCpqyJaTx9p0nr6+p46ozqrL8lm4qF+yJRK997RjfRStLE2JsLN0SRSBFTDL0yPB6mFc/BncywVVZ0=,iv:fJ0kpgysw8eHbIIrtdyUXwWYvHyOa6kJ1wW+6NvBTxY=,tag:cqyY6qmEa0HF6u61v9VZJw==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user