@schalkneethling/toolkit 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/dist/index.mjs +90 -6
- package/dist/index.mjs.map +1 -1
- package/hooks/auto-approve-safe-commands/hook.mjs +134 -0
- package/hooks/auto-approve-safe-commands/hook.mts +188 -0
- package/hooks/auto-approve-safe-commands/settings-fragment.json +17 -0
- package/hooks/block-dangerous-commands/hook.mjs +3 -3
- package/hooks/block-dangerous-commands/hook.mts +23 -10
- package/package.json +8 -10
- package/skills/css-coder/SKILL.md +95 -0
- package/skills/css-coder/references/patterns.md +224 -0
- package/skills/css-tokens/README.md +152 -0
- package/skills/css-tokens/SKILL.md +125 -0
- package/skills/css-tokens/references/tokens.css +162 -0
- package/skills/frontend-security/SKILL.md +134 -0
- package/skills/frontend-security/references/csp-configuration.md +191 -0
- package/skills/frontend-security/references/csrf-protection.md +327 -0
- package/skills/frontend-security/references/dom-security.md +229 -0
- package/skills/frontend-security/references/file-upload-security.md +310 -0
- package/skills/frontend-security/references/framework-patterns.md +307 -0
- package/skills/frontend-security/references/input-validation.md +232 -0
- package/skills/frontend-security/references/jwt-security.md +300 -0
- package/skills/frontend-security/references/nodejs-npm-security.md +261 -0
- package/skills/frontend-security/references/xss-prevention.md +163 -0
- package/skills/frontend-testing/SKILL.md +357 -0
- package/skills/frontend-testing/references/accessibility-testing.md +368 -0
- package/skills/frontend-testing/references/aria-snapshots.md +517 -0
- package/skills/frontend-testing/references/locator-strategies.md +295 -0
- package/skills/frontend-testing/references/visual-regression.md +466 -0
- package/skills/refined-plan-mode/SKILL.md +84 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env -S node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auto-approve-safe-commands: PermissionRequest hook
|
|
5
|
+
*
|
|
6
|
+
* Reads a Claude Code hook payload from stdin, inspects tool_input.command,
|
|
7
|
+
* and allows safe commands that match known-safe patterns.
|
|
8
|
+
*
|
|
9
|
+
* Exit 0 = defers to permission prompt
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
|
|
14
|
+
// --- Types ---
|
|
15
|
+
|
|
16
|
+
interface BashToolInput {
|
|
17
|
+
command: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PermissionRequestInput {
|
|
21
|
+
hook_event_name: "PermissionRequest";
|
|
22
|
+
session_id: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
transcript_path: string;
|
|
25
|
+
tool_name: string;
|
|
26
|
+
tool_input: BashToolInput | Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ApproveOutput {
|
|
30
|
+
hookSpecificOutput: {
|
|
31
|
+
hookEventName: "PermissionRequest";
|
|
32
|
+
decision: {
|
|
33
|
+
behavior: "allow";
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Safe command patterns ---
|
|
39
|
+
//
|
|
40
|
+
// Each entry is a pattern and a label for logging purposes.
|
|
41
|
+
// Order matters — more specific patterns should come before broader ones.
|
|
42
|
+
|
|
43
|
+
const SAFE_PATTERNS: { pattern: RegExp; label: string }[] = [
|
|
44
|
+
// Test runners
|
|
45
|
+
{ pattern: /^npm\s+test\b/, label: "npm test" },
|
|
46
|
+
{ pattern: /^npx\s+vitest\b/, label: "vitest" },
|
|
47
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
48
|
+
{ pattern: /^pnpm\s+test\b/, label: "pnpm test" },
|
|
49
|
+
{ pattern: /^yarn\s+test\b/, label: "yarn test" },
|
|
50
|
+
{ pattern: /^bun\s+test\b/, label: "bun test" },
|
|
51
|
+
{ pattern: /^jest\b/, label: "jest" },
|
|
52
|
+
{ pattern: /^vitest\b/, label: "vitest (direct)" },
|
|
53
|
+
|
|
54
|
+
// Linting and formatting (analysis only, never destructive)
|
|
55
|
+
{ pattern: /^npm\s+run\s+lint\b/, label: "npm run lint" },
|
|
56
|
+
{ pattern: /^pnpm\s+run\s+lint\b/, label: "pnpm run lint" },
|
|
57
|
+
{ pattern: /^yarn\s+lint\b/, label: "yarn lint" },
|
|
58
|
+
{ pattern: /^bun\s+run\s+lint\b/, label: "bun run lint" },
|
|
59
|
+
{ pattern: /^eslint\b/, label: "eslint" },
|
|
60
|
+
{ pattern: /^prettier\s+--check\b/, label: "prettier --check" },
|
|
61
|
+
{ pattern: /^stylelint\b/, label: "stylelint" },
|
|
62
|
+
|
|
63
|
+
// Type checking
|
|
64
|
+
{ pattern: /^tsc\s+--noEmit\b/, label: "tsc --noEmit" },
|
|
65
|
+
{ pattern: /^npx\s+tsc\s+--noEmit\b/, label: "npx tsc --noEmit" },
|
|
66
|
+
{ pattern: /^npm\s+run\s+typecheck\b/, label: "npm run typecheck" },
|
|
67
|
+
{ pattern: /^pnpm\s+run\s+typecheck\b/, label: "pnpm run typecheck" },
|
|
68
|
+
{ pattern: /^bun\s+run\s+typecheck\b/, label: "bun run typecheck" },
|
|
69
|
+
|
|
70
|
+
// Build commands
|
|
71
|
+
{ pattern: /^npm\s+run\s+build\b/, label: "npm run build" },
|
|
72
|
+
{ pattern: /^pnpm\s+run\s+build\b/, label: "pnpm run build" },
|
|
73
|
+
{ pattern: /^yarn\s+build\b/, label: "yarn build" },
|
|
74
|
+
{ pattern: /^bun\s+run\s+build\b/, label: "bun run build" },
|
|
75
|
+
{ pattern: /^vite\s+build\b/, label: "vite build" },
|
|
76
|
+
{ pattern: /^tsc\b/, label: "tsc" },
|
|
77
|
+
|
|
78
|
+
// Vite+ (vp) commands - https://viteplus.dev/guide/#core-commands
|
|
79
|
+
{ pattern: /^vp\s+test\b/, label: "vp test" },
|
|
80
|
+
{ pattern: /^vp\s+check\b/, label: "vp check" },
|
|
81
|
+
{ pattern: /^vp\s+lint\b/, label: "vp lint" },
|
|
82
|
+
{ pattern: /^vp\s+fmt\b/, label: "vp fmt" },
|
|
83
|
+
{ pattern: /^vp\s+build\b/, label: "vp build" },
|
|
84
|
+
{ pattern: /^vp\s+dev\b/, label: "vp dev" },
|
|
85
|
+
{ pattern: /^vp\s+preview\b/, label: "vp preview" },
|
|
86
|
+
{ pattern: /^vp\s+run\b/, label: "vp run" },
|
|
87
|
+
{ pattern: /^vp\s+outdated\b/, label: "vp outdated" },
|
|
88
|
+
{ pattern: /^vp\s+why\b/, label: "vp why" },
|
|
89
|
+
{ pattern: /^vp\s+info\b/, label: "vp info" },
|
|
90
|
+
{ pattern: /^vpx\b/, label: "vpx" },
|
|
91
|
+
|
|
92
|
+
// Dev server
|
|
93
|
+
{ pattern: /^npm\s+run\s+dev\b/, label: "npm run dev" },
|
|
94
|
+
{ pattern: /^pnpm\s+run\s+dev\b/, label: "pnpm run dev" },
|
|
95
|
+
{ pattern: /^yarn\s+dev\b/, label: "yarn dev" },
|
|
96
|
+
{ pattern: /^bun\s+run\s+dev\b/, label: "bun run dev" },
|
|
97
|
+
{ pattern: /^vite\b/, label: "vite" },
|
|
98
|
+
|
|
99
|
+
// Git read operations (no side effects)
|
|
100
|
+
{ pattern: /^git\s+status\b/, label: "git status" },
|
|
101
|
+
{ pattern: /^git\s+log\b/, label: "git log" },
|
|
102
|
+
{ pattern: /^git\s+diff\b/, label: "git diff" },
|
|
103
|
+
{ pattern: /^git\s+branch\b/, label: "git branch" },
|
|
104
|
+
{ pattern: /^git\s+show\b/, label: "git show" },
|
|
105
|
+
|
|
106
|
+
// Filesystem reads
|
|
107
|
+
{ pattern: /^cat\b/, label: "cat" },
|
|
108
|
+
{ pattern: /^ls\b/, label: "ls" },
|
|
109
|
+
{ pattern: /^find\b/, label: "find" },
|
|
110
|
+
{ pattern: /^grep\b/, label: "grep" },
|
|
111
|
+
{ pattern: /^rg\b/, label: "ripgrep" },
|
|
112
|
+
|
|
113
|
+
// Environment checks
|
|
114
|
+
{ pattern: /^node\s+--version\b/, label: "node --version" },
|
|
115
|
+
{ pattern: /^node\s+-v\b/, label: "node -v" },
|
|
116
|
+
{ pattern: /^bun\s+--version\b/, label: "bun --version" },
|
|
117
|
+
{ pattern: /^npm\s+--version\b/, label: "npm --version" },
|
|
118
|
+
{ pattern: /^git\s+--version\b/, label: "git --version" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// --- Helpers ---
|
|
122
|
+
|
|
123
|
+
function approve(): void {
|
|
124
|
+
const output: ApproveOutput = {
|
|
125
|
+
hookSpecificOutput: {
|
|
126
|
+
hookEventName: "PermissionRequest",
|
|
127
|
+
decision: {
|
|
128
|
+
behavior: "allow",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defer(): void {
|
|
138
|
+
// Exit 0 with no output — falls through to the normal permission prompt
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Main ---
|
|
143
|
+
|
|
144
|
+
function main(): void {
|
|
145
|
+
const raw = readFileSync("/dev/stdin", "utf-8").trim();
|
|
146
|
+
|
|
147
|
+
let input: PermissionRequestInput;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
input = JSON.parse(raw) as PermissionRequestInput;
|
|
151
|
+
} catch {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`[auto-approve-safe-commands] Failed to parse stdin JSON: ${raw}\n`,
|
|
154
|
+
);
|
|
155
|
+
defer();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Only handle Bash tool permission requests
|
|
160
|
+
if (input.tool_name !== "Bash") {
|
|
161
|
+
defer();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { command } = input.tool_input as BashToolInput;
|
|
166
|
+
|
|
167
|
+
if (typeof command !== "string") {
|
|
168
|
+
defer();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const trimmed = command.trim();
|
|
173
|
+
|
|
174
|
+
for (const { pattern, label } of SAFE_PATTERNS) {
|
|
175
|
+
if (pattern.test(trimmed)) {
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`[auto-approve-safe-commands] Auto-approved: ${label}\n`,
|
|
178
|
+
);
|
|
179
|
+
approve();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Not on the allowlist — fall through to normal permission prompt
|
|
185
|
+
defer();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PermissionRequest": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node .claude/hooks/auto-approve-safe-commands.mjs",
|
|
10
|
+
"timeout": 5,
|
|
11
|
+
"statusMessage": "Checking if command can be auto-approved..."
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -31,8 +31,7 @@ const rules = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
id: "chmod-777",
|
|
34
|
-
test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) &&
|
|
35
|
-
/-R|--recursive|777/.test(c),
|
|
34
|
+
test: (c) => /\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(c) && /-R|--recursive|777/.test(c),
|
|
36
35
|
message: "`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
|
|
37
36
|
},
|
|
38
37
|
{
|
|
@@ -47,7 +46,8 @@ const rules = [
|
|
|
47
46
|
},
|
|
48
47
|
{
|
|
49
48
|
id: "fork-bomb",
|
|
50
|
-
test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
49
|
+
test: (c) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
50
|
+
/\.\s*\|\s*\.\s*&/.test(c),
|
|
51
51
|
message: "Fork bomb pattern detected and blocked.",
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env -S node
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* block-dangerous-commands: PreToolUse hook for Bash.
|
|
4
5
|
*
|
|
@@ -18,13 +19,15 @@ type Rule = {
|
|
|
18
19
|
const rules: Rule[] = [
|
|
19
20
|
{
|
|
20
21
|
id: "rm-rf",
|
|
21
|
-
test: (c) =>
|
|
22
|
+
test: (c) =>
|
|
23
|
+
/\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*|--recursive|--force)(?:\s|$)/.test(c),
|
|
22
24
|
message:
|
|
23
25
|
"`rm -rf` (and flag variants) is blocked. Delete specific paths with a non-recursive `rm`, or move them to a backup location.",
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
id: "git-push-force",
|
|
27
|
-
test: (c) =>
|
|
29
|
+
test: (c) =>
|
|
30
|
+
/\bgit\s+push\b.*\s(?:--force\b|--force-with-lease\b|-f\b)/.test(c),
|
|
28
31
|
message:
|
|
29
32
|
"`git push --force` is blocked. Use `--force-with-lease` only after coordinating with collaborators, or create a new branch.",
|
|
30
33
|
},
|
|
@@ -46,8 +49,9 @@ const rules: Rule[] = [
|
|
|
46
49
|
{
|
|
47
50
|
id: "chmod-777",
|
|
48
51
|
test: (c) =>
|
|
49
|
-
/\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(
|
|
50
|
-
|
|
52
|
+
/\bchmod\s+(?:-[a-zA-Z]*\s+)*(?:777|[ugoa]*[+=][rwx]*w[rwx]*(?:\s|$))/.test(
|
|
53
|
+
c,
|
|
54
|
+
) && /-R|--recursive|777/.test(c),
|
|
51
55
|
message:
|
|
52
56
|
"`chmod 777` or recursive world-writable chmod is blocked. Grant the minimum permissions required.",
|
|
53
57
|
},
|
|
@@ -58,19 +62,24 @@ const rules: Rule[] = [
|
|
|
58
62
|
},
|
|
59
63
|
{
|
|
60
64
|
id: "system-redirect",
|
|
61
|
-
test: (c) =>
|
|
65
|
+
test: (c) =>
|
|
66
|
+
/(?:>|>>|tee(?:\s+-[a-zA-Z]*)?)\s+\/(?:etc|boot|usr|bin|sbin)\//.test(c),
|
|
62
67
|
message:
|
|
63
68
|
"Writing into /etc, /boot, /usr, /bin, or /sbin is blocked. These are system directories; use a user-writable path.",
|
|
64
69
|
},
|
|
65
70
|
{
|
|
66
71
|
id: "fork-bomb",
|
|
67
|
-
test: (c) =>
|
|
72
|
+
test: (c) =>
|
|
73
|
+
/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(c) ||
|
|
74
|
+
/\.\s*\|\s*\.\s*&/.test(c),
|
|
68
75
|
message: "Fork bomb pattern detected and blocked.",
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
id: "curl-pipe-shell",
|
|
72
79
|
test: (c) =>
|
|
73
|
-
/\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(
|
|
80
|
+
/\b(?:curl|wget|fetch)\b[^|;]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh|fish|ksh|dash)\b/.test(
|
|
81
|
+
c,
|
|
82
|
+
),
|
|
74
83
|
message:
|
|
75
84
|
"Piping remote content directly into a shell is blocked. Download the script, inspect it, then run it.",
|
|
76
85
|
},
|
|
@@ -83,8 +92,11 @@ const rules: Rule[] = [
|
|
|
83
92
|
{
|
|
84
93
|
id: "kill-9",
|
|
85
94
|
test: (c) =>
|
|
86
|
-
/\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(
|
|
87
|
-
|
|
95
|
+
/\bkill\s+(?:-[a-zA-Z]*\s+)*-9\b|\bkill\s+-s\s+(?:9|SIGKILL)\b|\bkill\s+-SIGKILL\b/.test(
|
|
96
|
+
c,
|
|
97
|
+
),
|
|
98
|
+
message:
|
|
99
|
+
"`kill -9` is blocked — it prevents cleanup. Try SIGTERM (default) first.",
|
|
88
100
|
},
|
|
89
101
|
{
|
|
90
102
|
id: "npm-publish",
|
|
@@ -95,7 +107,8 @@ const rules: Rule[] = [
|
|
|
95
107
|
{
|
|
96
108
|
id: "history-clear",
|
|
97
109
|
test: (c) => /\bhistory\s+-c\b/.test(c),
|
|
98
|
-
message:
|
|
110
|
+
message:
|
|
111
|
+
"`history -c` is blocked — erasing shell history hides what happened.",
|
|
99
112
|
},
|
|
100
113
|
];
|
|
101
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schalkneethling/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI for managing Claude Code hooks and skills across projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -18,18 +18,16 @@
|
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"toolkit": "tsx src/index.ts",
|
|
23
|
-
"prepare": "vp pack && vp run build:hooks",
|
|
24
|
-
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
25
|
-
},
|
|
26
21
|
"dependencies": {
|
|
27
22
|
"vite-plus": "^0.1.18"
|
|
28
23
|
},
|
|
29
24
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^
|
|
25
|
+
"@types/node": "^25.6.0",
|
|
31
26
|
"tsx": "^4.21.0",
|
|
32
|
-
"typescript": "^
|
|
27
|
+
"typescript": "^6.0.2"
|
|
33
28
|
},
|
|
34
|
-
"
|
|
35
|
-
|
|
29
|
+
"scripts": {
|
|
30
|
+
"toolkit": "tsx src/index.ts",
|
|
31
|
+
"build:hooks": "tsc --project tsconfig.hooks.json"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: css-coder
|
|
3
|
+
description: CSS authoring guidance emphasizing web standards, accessibility, and performance. Use when writing, reviewing, or refactoring CSS. Provides patterns, snippets, and conventions that prioritize native CSS over frameworks, semantic structure, and maintainable code. Refer to references/patterns.md for specific patterns and snippets.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CSS Coder
|
|
7
|
+
|
|
8
|
+
Guidance for writing CSS that prioritizes web standards, accessibility, performance, and maintainability.
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
|
|
12
|
+
1. **Web standards first** — Use native CSS features before reaching for libraries or frameworks. No Tailwind, no CSS-in-JS unless explicitly requested.
|
|
13
|
+
2. **Accessibility as a requirement** — Ensure styles support, never hinder, assistive technologies. Respect user preferences (motion, color scheme, contrast).
|
|
14
|
+
3. **Performance matters** — Minimize repaints, avoid layout thrashing, use efficient selectors.
|
|
15
|
+
4. **Readable over clever** — Future maintainers (including the author) should understand the code at a glance.
|
|
16
|
+
5. **Explicit over implicit** — Avoid magic numbers and unexplained values. Use custom properties for shared values.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. **Check references first** — Before writing CSS, consult `references/patterns.md` for established patterns and snippets.
|
|
21
|
+
2. **Validate against specs** — When uncertain, reference MDN Web Docs or CSS specifications.
|
|
22
|
+
3. **Suggest alternatives** — Offer ideas beyond the skill's patterns when appropriate, but always aligned with the core principles above.
|
|
23
|
+
|
|
24
|
+
## Writing Guidelines
|
|
25
|
+
|
|
26
|
+
### Selectors
|
|
27
|
+
|
|
28
|
+
- Prefer class selectors over element or ID selectors for styling.
|
|
29
|
+
- Keep specificity low and predictable.
|
|
30
|
+
- Avoid deep nesting (aim for 2-3 levels maximum).
|
|
31
|
+
|
|
32
|
+
### Custom Properties
|
|
33
|
+
|
|
34
|
+
- Use `--` prefixed custom properties for colors, spacing, typography, and other repeated values.
|
|
35
|
+
- Define at `:root` or appropriate scope.
|
|
36
|
+
- Name descriptively: `--color-primary`, `--spacing-md`, `--font-size-body`.
|
|
37
|
+
|
|
38
|
+
### Logical Properties
|
|
39
|
+
|
|
40
|
+
- Always use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`, `block-size`, etc.) instead of physical properties (`margin-left`, `padding-top`, `left`, `height`, etc.).
|
|
41
|
+
- Logical properties support internationalization and different writing modes automatically.
|
|
42
|
+
- Only fall back to physical properties where logical equivalents do not yet exist.
|
|
43
|
+
|
|
44
|
+
### Units
|
|
45
|
+
|
|
46
|
+
- Use `rem` for typography and spacing (respects user font-size preferences).
|
|
47
|
+
- Use `em` for component-relative sizing when appropriate.
|
|
48
|
+
- Use viewport units (`vw`, `vh`, `dvh`) thoughtfully, with fallbacks where needed.
|
|
49
|
+
- Avoid `px` for font sizes; acceptable for borders, shadows, and fine details.
|
|
50
|
+
|
|
51
|
+
### Colors
|
|
52
|
+
|
|
53
|
+
- Use modern space-separated syntax for all color functions (`rgb()`, `hsl()`, `oklch()`).
|
|
54
|
+
- Recommend `oklch()` for vibrant or wide-gamut colors.
|
|
55
|
+
- Use relative color syntax to derive hover states or transparent variants from existing variables.
|
|
56
|
+
- See `references/patterns.md` for syntax examples.
|
|
57
|
+
|
|
58
|
+
### Layout
|
|
59
|
+
|
|
60
|
+
- Use CSS Grid for two-dimensional layouts, or when vertical flow is needed without extra declarations.
|
|
61
|
+
- Use Flexbox for one-dimensional alignment, noting it defaults to `row` direction.
|
|
62
|
+
- Avoid floats for layout (legacy use only).
|
|
63
|
+
|
|
64
|
+
### Media Queries
|
|
65
|
+
|
|
66
|
+
- Use modern range syntax: `@media (width < 48rem)`, `@media (width >= 48rem)`.
|
|
67
|
+
- Prefer **Shared First** over mobile-first: define shared styles outside queries, scope viewport-specific styles with bounded queries.
|
|
68
|
+
- Keep breakpoints to a minimum — add more only when there's a clear need.
|
|
69
|
+
- See `references/patterns.md` for detailed examples.
|
|
70
|
+
|
|
71
|
+
### Accessibility
|
|
72
|
+
|
|
73
|
+
- Never use `display: none` or `visibility: hidden` to hide content that should remain accessible to screen readers. Use appropriate techniques from the references.
|
|
74
|
+
- Respect `prefers-reduced-motion`, `prefers-color-scheme`, and `prefers-contrast`.
|
|
75
|
+
- Ensure sufficient color contrast (WCAG AA minimum, AAA preferred).
|
|
76
|
+
- Maintain visible focus indicators — never remove `:focus` styles without replacement.
|
|
77
|
+
|
|
78
|
+
### Performance
|
|
79
|
+
|
|
80
|
+
- Avoid expensive properties in animations (prefer `transform` and `opacity`).
|
|
81
|
+
- Use `will-change` sparingly and only when needed.
|
|
82
|
+
- Minimize use of `*` selectors.
|
|
83
|
+
- Prefer `@layer` for managing cascade when working with larger codebases.
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
Consult `references/patterns.md` for:
|
|
88
|
+
|
|
89
|
+
- Visually-hidden utility
|
|
90
|
+
- User preference queries (motion, color scheme, contrast)
|
|
91
|
+
- Modern color syntax and relative colors
|
|
92
|
+
- Shared First responsive patterns
|
|
93
|
+
- Any project-specific conventions
|
|
94
|
+
|
|
95
|
+
This file will grow as patterns are added. If a needed pattern doesn't exist, suggest one aligned with the core principles.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# CSS Patterns and Snippets
|
|
2
|
+
|
|
3
|
+
This file contains reusable CSS patterns, snippets, and conventions. Consult before writing CSS.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Utility Patterns
|
|
8
|
+
|
|
9
|
+
### Visually Hidden (Screen Reader Only)
|
|
10
|
+
|
|
11
|
+
Use when content should be accessible to assistive technologies but not visible on screen.
|
|
12
|
+
|
|
13
|
+
```css
|
|
14
|
+
.visually-hidden {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 1px;
|
|
17
|
+
height: 1px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
margin: -1px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
clip: rect(0, 0, 0, 0);
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
border: 0;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## User Preference Queries
|
|
30
|
+
|
|
31
|
+
### Reduced Motion
|
|
32
|
+
|
|
33
|
+
Always wrap animations and transitions for users who prefer reduced motion.
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
@media (prefers-reduced-motion: reduce) {
|
|
37
|
+
*,
|
|
38
|
+
*::before,
|
|
39
|
+
*::after {
|
|
40
|
+
animation-duration: 0.01ms !important;
|
|
41
|
+
animation-iteration-count: 1 !important;
|
|
42
|
+
transition-duration: 0.01ms !important;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Color Scheme
|
|
48
|
+
|
|
49
|
+
```css
|
|
50
|
+
@media (prefers-color-scheme: dark) {
|
|
51
|
+
:root {
|
|
52
|
+
/* dark mode custom properties */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### High Contrast
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
@media (prefers-contrast: more) {
|
|
61
|
+
:root {
|
|
62
|
+
/* increased contrast custom properties */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Color Patterns
|
|
70
|
+
|
|
71
|
+
### Modern Color Syntax
|
|
72
|
+
|
|
73
|
+
Always use space-separated syntax for color functions. This is the current standard and required for modern color features.
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
/* Legacy — avoid */
|
|
77
|
+
.legacy {
|
|
78
|
+
background-color: hsla(10, 100%, 50%, 0.75);
|
|
79
|
+
color: rgba(0, 0, 0, 0.9);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Modern — preferred */
|
|
83
|
+
.modern {
|
|
84
|
+
background-color: hsl(10 100% 50% / 0.75);
|
|
85
|
+
color: rgb(0 0 0 / 0.9);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Key points:
|
|
90
|
+
|
|
91
|
+
- Use `rgb()` and `hsl()` — the `a` suffix (`rgba`, `hsla`) is no longer needed.
|
|
92
|
+
- Separate components with spaces, not commas.
|
|
93
|
+
- Use `/` before the alpha value for transparency.
|
|
94
|
+
|
|
95
|
+
### Wide Gamut Colors (oklch, oklab)
|
|
96
|
+
|
|
97
|
+
Use `oklch()` when users request "vibrant", "bright", or "punchy" colors. It supports a wider gamut than sRGB and is perceptually uniform, making adjustments more predictable.
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
:root {
|
|
101
|
+
--color-accent: oklch(65% 0.25 340);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Note: `oklch()`, `oklab()`, and `color()` only support space-separated syntax.
|
|
106
|
+
|
|
107
|
+
### Relative Color Syntax
|
|
108
|
+
|
|
109
|
+
Use relative colors to derive variations (hover states, overlays, tints) from existing brand variables without defining new custom properties.
|
|
110
|
+
|
|
111
|
+
Syntax: `function(from [base-color] [components] / [alpha])`
|
|
112
|
+
|
|
113
|
+
```css
|
|
114
|
+
:root {
|
|
115
|
+
--color-brand: oklch(55% 0.2 250);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.button {
|
|
119
|
+
background-color: var(--color-brand);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.button:hover {
|
|
123
|
+
/* Lighten by adjusting the L channel */
|
|
124
|
+
background-color: oklch(from var(--color-brand) calc(l + 0.1) c h);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.overlay {
|
|
128
|
+
/* Apply 50% opacity to brand color */
|
|
129
|
+
background-color: oklch(from var(--color-brand) l c h / 0.5);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Responsive Patterns
|
|
136
|
+
|
|
137
|
+
### Shared First (Preferred over Mobile First)
|
|
138
|
+
|
|
139
|
+
Avoid traditional mobile-first CSS where styles "bleed up" through open-ended `min-width` queries, requiring constant overrides. Instead, use a **Shared First** approach:
|
|
140
|
+
|
|
141
|
+
1. Define only truly shared styles outside media queries.
|
|
142
|
+
2. Use bounded media queries to scope styles to specific viewport ranges.
|
|
143
|
+
3. Keep breakpoints to a minimum — add more only when there's a clear need.
|
|
144
|
+
|
|
145
|
+
Use modern range syntax for all media queries:
|
|
146
|
+
|
|
147
|
+
```css
|
|
148
|
+
@media (width < 48rem) {
|
|
149
|
+
/* below breakpoint */
|
|
150
|
+
}
|
|
151
|
+
@media (width >= 48rem) {
|
|
152
|
+
/* at or above breakpoint */
|
|
153
|
+
}
|
|
154
|
+
@media (48rem <= width < 64rem) {
|
|
155
|
+
/* between breakpoints */
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Why Shared First?**
|
|
160
|
+
|
|
161
|
+
- **Fewer side effects** — Changes to one viewport don't accidentally affect others.
|
|
162
|
+
- **Easier debugging** — DevTools shows fewer overridden (struck-through) properties.
|
|
163
|
+
- **Clearer intent** — Each viewport's styles are self-contained.
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
/* Mobile First — avoid */
|
|
167
|
+
body {
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
gap: 1rem;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (min-width: 48rem) {
|
|
174
|
+
body {
|
|
175
|
+
flex-direction: row;
|
|
176
|
+
gap: 2rem;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```css
|
|
182
|
+
/* Shared First — preferred */
|
|
183
|
+
body {
|
|
184
|
+
display: flex;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@media (width < 48rem) {
|
|
188
|
+
body {
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 1rem;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@media (width >= 48rem) {
|
|
195
|
+
body {
|
|
196
|
+
flex-direction: row;
|
|
197
|
+
gap: 2rem;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The Shared First approach may use more lines, but results in more maintainable, predictable CSS with fewer debugging headaches.
|
|
203
|
+
|
|
204
|
+
#### Using Custom Properties with Shared First
|
|
205
|
+
|
|
206
|
+
Custom properties can reduce repetition by keeping the property declaration shared while scoping only the value to each viewport. Use sparingly — overuse can make code harder to follow.
|
|
207
|
+
|
|
208
|
+
```css
|
|
209
|
+
.Dialog-container {
|
|
210
|
+
padding-block: var(--Dialog-block-padding);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@media (width < 48rem) {
|
|
214
|
+
.Dialog-container {
|
|
215
|
+
--Dialog-block-padding: var(--size-16);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (width >= 48rem) {
|
|
220
|
+
.Dialog-container {
|
|
221
|
+
--Dialog-block-padding: var(--size-32);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|