@ketecode/loom 0.1.0 → 0.2.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 +111 -11
- package/agents/code-reviewer/evals/behavior.json +60 -0
- package/agents/inbox-triager/AGENT.md +98 -0
- package/agents/inbox-triager/evals/triggers.json +16 -0
- package/agents/spec-reviewer/AGENT.md +95 -0
- package/agents/spec-reviewer/evals/behavior.json +57 -0
- package/agents/spec-reviewer/evals/triggers.json +16 -0
- package/agents/task-implementer/AGENT.md +89 -0
- package/agents/task-implementer/evals/behavior.json +47 -0
- package/agents/task-implementer/evals/triggers.json +16 -0
- package/bin/kete-loom.js +46 -6
- package/commands/brainstorm.md +25 -0
- package/commands/debug.md +27 -0
- package/commands/dual-review.md +34 -0
- package/commands/feature-dev.md +8 -7
- package/commands/finish-branch.md +26 -0
- package/commands/fix-bug.md +38 -0
- package/commands/production-readiness.md +45 -0
- package/commands/usage-report.md +43 -0
- package/hooks/block-foreground-servers/HOOK.md +34 -0
- package/hooks/block-foreground-servers/cases.json +31 -0
- package/hooks/block-foreground-servers/hook.js +41 -0
- package/hooks/capture-lessons/HOOK.md +24 -0
- package/hooks/capture-lessons/hook.js +48 -0
- package/hooks/notify-on-stop/HOOK.md +25 -0
- package/hooks/notify-on-stop/hook.js +27 -0
- package/hooks/protect-lint-config/HOOK.md +29 -0
- package/hooks/protect-lint-config/cases.json +18 -0
- package/hooks/protect-lint-config/fixtures/.eslintrc.json +1 -0
- package/hooks/protect-lint-config/fixtures/.golangci.yml +1 -0
- package/hooks/protect-lint-config/fixtures/.prettierrc +1 -0
- package/hooks/protect-lint-config/fixtures/.rubocop.yml +1 -0
- package/hooks/protect-lint-config/fixtures/biome.json +1 -0
- package/hooks/protect-lint-config/fixtures/eslint.config.mjs +1 -0
- package/hooks/protect-lint-config/fixtures/ruff.toml +1 -0
- package/hooks/protect-lint-config/hook.js +25 -0
- package/hooks/read-before-edit/HOOK.md +26 -0
- package/hooks/read-before-edit/cases.json +15 -0
- package/hooks/read-before-edit/hook.js +50 -0
- package/hooks/run.js +22 -6
- package/hooks/verify-before-stop/HOOK.md +32 -0
- package/hooks/verify-before-stop/hook.js +70 -0
- package/mcp/clickhouse.json +11 -0
- package/mcp/exa.json +11 -0
- package/mcp/firecrawl.json +11 -0
- package/mcp/vercel.json +11 -0
- package/package.json +6 -4
- package/profiles.json +54 -5
- package/rules/use-skills.md +23 -0
- package/scripts/lib/behavior.js +52 -0
- package/scripts/lib/hooks-install.js +9 -7
- package/scripts/lib/lint.js +24 -2
- package/scripts/lib/mcp-install.js +5 -1
- package/scripts/lib/render.js +6 -0
- package/scripts/lib/skills.js +1 -0
- package/scripts/lib/targets.js +10 -0
- package/skills/agent-failure-debugging/SKILL.md +92 -0
- package/skills/agent-failure-debugging/evals/triggers.json +16 -0
- package/skills/agent-outbound-messaging/SKILL.md +92 -0
- package/skills/agent-outbound-messaging/evals/triggers.json +16 -0
- package/skills/agent-security/SKILL.md +117 -0
- package/skills/agent-security/evals/triggers.json +16 -0
- package/skills/agent-security/references/host-configs.md +37 -0
- package/skills/api-client-integration/SKILL.md +86 -0
- package/skills/api-client-integration/evals/triggers.json +16 -0
- package/skills/api-client-integration/references/resilience.md +57 -0
- package/skills/blender-automation/SKILL.md +100 -0
- package/skills/blender-automation/evals/triggers.json +16 -0
- package/skills/blender-automation/references/batch-and-render.md +77 -0
- package/skills/blender-automation/references/bpy-inspection.md +91 -0
- package/skills/blender-automation/references/rig-and-retarget-checks.md +23 -0
- package/skills/brand-strategy/SKILL.md +83 -0
- package/skills/brand-strategy/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/SKILL.md +89 -0
- package/skills/change-impact-analysis/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/references/graph-commands.md +28 -0
- package/skills/contract-drafting/SKILL.md +96 -0
- package/skills/contract-drafting/evals/triggers.json +16 -0
- package/skills/contract-drafting/references/term-sheets.md +68 -0
- package/skills/contract-first-development/SKILL.md +86 -0
- package/skills/contract-first-development/evals/triggers.json +16 -0
- package/skills/contract-first-development/references/tooling.md +46 -0
- package/skills/decision-council/SKILL.md +98 -0
- package/skills/decision-council/evals/triggers.json +16 -0
- package/skills/design-brainstorming/SKILL.md +132 -0
- package/skills/design-brainstorming/evals/behavior.json +55 -0
- package/skills/design-brainstorming/evals/triggers.json +16 -0
- package/skills/design-brainstorming/references/visual-mockups.md +34 -0
- package/skills/desktop-app-testing/SKILL.md +104 -0
- package/skills/desktop-app-testing/evals/triggers.json +16 -0
- package/skills/desktop-app-testing/references/drivers.md +81 -0
- package/skills/document-processing/SKILL.md +100 -0
- package/skills/document-processing/evals/triggers.json +17 -0
- package/skills/document-processing/references/extraction-and-ocr.md +71 -0
- package/skills/document-processing/references/forms-and-conversion.md +69 -0
- package/skills/document-processing/references/redaction.md +61 -0
- package/skills/energy-procurement/SKILL.md +106 -0
- package/skills/energy-procurement/evals/triggers.json +16 -0
- package/skills/finishing-a-branch/SKILL.md +127 -0
- package/skills/finishing-a-branch/evals/behavior.json +49 -0
- package/skills/finishing-a-branch/evals/triggers.json +16 -0
- package/skills/freight-carrier-management/SKILL.md +107 -0
- package/skills/freight-carrier-management/evals/triggers.json +16 -0
- package/skills/git-worktrees/SKILL.md +130 -0
- package/skills/git-worktrees/evals/behavior.json +56 -0
- package/skills/git-worktrees/evals/triggers.json +16 -0
- package/skills/google-workspace-integration/SKILL.md +65 -0
- package/skills/google-workspace-integration/evals/triggers.json +17 -0
- package/skills/google-workspace-integration/references/api.md +121 -0
- package/skills/harmonyos-arkts/SKILL.md +117 -0
- package/skills/harmonyos-arkts/evals/triggers.json +16 -0
- package/skills/harmonyos-arkts/references/build-errors.md +42 -0
- package/skills/implementation-plans/SKILL.md +117 -0
- package/skills/implementation-plans/evals/behavior.json +54 -0
- package/skills/implementation-plans/evals/triggers.json +16 -0
- package/skills/ios-platform-features/SKILL.md +99 -0
- package/skills/ios-platform-features/evals/triggers.json +16 -0
- package/skills/ios-platform-features/references/foundation-models.md +90 -0
- package/skills/jvm-sql-mappers/SKILL.md +98 -0
- package/skills/jvm-sql-mappers/evals/triggers.json +16 -0
- package/skills/jvm-sql-mappers/references/examples.md +129 -0
- package/skills/mobile-accessibility/SKILL.md +94 -0
- package/skills/mobile-accessibility/evals/triggers.json +16 -0
- package/skills/network-design/SKILL.md +98 -0
- package/skills/network-design/evals/triggers.json +16 -0
- package/skills/observability/SKILL.md +125 -0
- package/skills/observability/evals/triggers.json +17 -0
- package/skills/observability/references/otel-setup.md +87 -0
- package/skills/observability/references/slo-alerts.md +70 -0
- package/skills/open-source-release/SKILL.md +107 -0
- package/skills/open-source-release/evals/triggers.json +15 -0
- package/skills/open-source-release/references/history-scrub.md +65 -0
- package/skills/patent-research/SKILL.md +94 -0
- package/skills/patent-research/evals/triggers.json +16 -0
- package/skills/patent-research/references/databases-and-queries.md +52 -0
- package/skills/prospect-research/SKILL.md +87 -0
- package/skills/prospect-research/evals/triggers.json +16 -0
- package/skills/python-app-distribution/SKILL.md +107 -0
- package/skills/python-app-distribution/evals/triggers.json +16 -0
- package/skills/python-app-distribution/references/pypi-trusted-publishing.md +55 -0
- package/skills/python-app-distribution/references/windows-macos-signing.md +70 -0
- package/skills/quality-nonconformance/SKILL.md +107 -0
- package/skills/quality-nonconformance/evals/triggers.json +16 -0
- package/skills/quality-nonconformance/references/8d-and-capa.md +46 -0
- package/skills/react-testing/SKILL.md +121 -0
- package/skills/react-testing/evals/triggers.json +18 -0
- package/skills/react-testing/references/msw.md +64 -0
- package/skills/react-testing/references/queries.md +41 -0
- package/skills/react-testing/references/setup.md +74 -0
- package/skills/recommender-systems/SKILL.md +119 -0
- package/skills/recommender-systems/evals/triggers.json +16 -0
- package/skills/recommender-systems/references/evaluation.md +39 -0
- package/skills/refactoring-safely/evals/behavior.json +57 -0
- package/skills/reproducible-dev-environments/SKILL.md +132 -0
- package/skills/reproducible-dev-environments/evals/triggers.json +16 -0
- package/skills/responding-to-code-review/SKILL.md +124 -0
- package/skills/responding-to-code-review/evals/behavior.json +54 -0
- package/skills/responding-to-code-review/evals/triggers.json +16 -0
- package/skills/security-review/evals/behavior.json +56 -0
- package/skills/session-handoff/evals/behavior.json +55 -0
- package/skills/skill-authoring/SKILL.md +13 -2
- package/skills/skill-authoring/evals/triggers.json +2 -1
- package/skills/skill-authoring/references/pressure-testing.md +87 -0
- package/skills/slide-decks/SKILL.md +107 -0
- package/skills/slide-decks/evals/triggers.json +18 -0
- package/skills/slide-decks/references/html-decks.md +86 -0
- package/skills/slide-decks/references/layouts.md +45 -0
- package/skills/slide-decks/references/pptx.md +87 -0
- package/skills/subagent-driven-development/SKILL.md +109 -0
- package/skills/subagent-driven-development/evals/behavior.json +53 -0
- package/skills/subagent-driven-development/evals/triggers.json +15 -0
- package/skills/subagent-driven-development/references/implementer-brief.md +60 -0
- package/skills/subagent-driven-development/references/reviewer-briefs.md +52 -0
- package/skills/systematic-debugging/SKILL.md +92 -0
- package/skills/systematic-debugging/evals/behavior.json +59 -0
- package/skills/systematic-debugging/evals/triggers.json +16 -0
- package/skills/systematic-debugging/references/condition-waiting.md +52 -0
- package/skills/systematic-debugging/references/root-cause-tracing.md +49 -0
- package/skills/systematic-debugging/references/test-pollution.md +46 -0
- package/skills/tdd-workflow/SKILL.md +3 -1
- package/skills/tdd-workflow/evals/behavior.json +55 -0
- package/skills/tdd-workflow/evals/triggers.json +2 -1
- package/skills/tdd-workflow/references/testing-anti-patterns.md +87 -0
- package/skills/transactional-email/SKILL.md +68 -0
- package/skills/transactional-email/evals/triggers.json +17 -0
- package/skills/transactional-email/references/providers.md +63 -0
- package/skills/ui-demo-recording/SKILL.md +93 -0
- package/skills/ui-demo-recording/evals/triggers.json +16 -0
- package/skills/ui-demo-recording/references/cursor-and-captions.md +48 -0
- package/skills/ui-demo-recording/references/playwright-recording.md +63 -0
- package/skills/ui-demo-recording/references/post-processing.md +57 -0
- package/skills/ui-design-polish/SKILL.md +113 -0
- package/skills/ui-design-polish/evals/triggers.json +18 -0
- package/skills/ui-design-polish/references/detail-checklist.md +45 -0
- package/skills/ui-design-polish/references/tokens.md +68 -0
- package/skills/ui-flow-audit/SKILL.md +112 -0
- package/skills/ui-flow-audit/evals/triggers.json +17 -0
- package/skills/ui-motion/SKILL.md +103 -0
- package/skills/ui-motion/evals/triggers.json +18 -0
- package/skills/ui-motion/references/patterns.md +138 -0
- package/skills/ui-motion/references/tokens.md +52 -0
- package/skills/unicode-text-processing/SKILL.md +103 -0
- package/skills/unicode-text-processing/evals/triggers.json +17 -0
- package/skills/unicode-text-processing/references/apis.md +89 -0
- package/skills/unicode-text-processing/references/script-notes.md +50 -0
- package/skills/verification-loop/evals/behavior.json +52 -0
- package/skills/video-editing/SKILL.md +111 -0
- package/skills/video-editing/evals/triggers.json +16 -0
- package/skills/video-editing/references/edl-and-logging.md +44 -0
- package/skills/video-editing/references/ffmpeg-edit-recipes.md +92 -0
- package/skills/video-editing/references/resolve-scripting.md +53 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-flow-audit
|
|
3
|
+
description: Static trace of every user-facing control in a frontend, following each click, toggle or submit through its event handler, state setters, effects, store actions, network calls and their responses to the final rendered state, to find interactions that each work alone but cancel each other, race, double-fire or leave the screen in the wrong state, reported flow by flow. Use when a feature behaves correctly in isolation yet breaks when actions are combined or repeated, a toggle snaps back, a filter or form resets unexpectedly, or someone wants an audit of how the UI's state changes fit together before a release.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Framework-neutral (React, Vue, Svelte, Angular, vanilla); reading code is enough, running the app and its tests confirms findings.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: coding
|
|
9
|
+
kete-loom/risk: runs-commands
|
|
10
|
+
kete-loom/owner: "Kete-org"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# UI flow audit
|
|
14
|
+
|
|
15
|
+
Bugs in interactive UIs often live between handlers, not inside them: a click sets state, an effect reacts and resets it, a response from an earlier request lands last and overwrites the newer one. Each piece passes its unit test. This audit traces the whole chain for each control and checks what the user finally sees.
|
|
16
|
+
|
|
17
|
+
The audit does not change source files. It may run the app or tests to confirm a finding, with the user's permission. It produces a report; fixing is a separate step the user approves. Neighbours: `browser-qa` clicks through a running app; `react-reviewer` reviews a diff; `react-patterns` says how to structure state. This skill maps cause and effect across a whole screen or feature.
|
|
18
|
+
|
|
19
|
+
## 0. Scope
|
|
20
|
+
|
|
21
|
+
1. Agree the scope with the user: one screen, one feature, or a list of routes. A whole app at once gives a shallow report; **ask** to narrow it if there are more than ~30 controls.
|
|
22
|
+
2. Identify the framework and the state tools in use (component state, context, Redux/Zustand/Pinia/NgRx stores, a query cache such as TanStack Query or SWR, URL/search params, form libraries, local storage).
|
|
23
|
+
3. Note the expected behaviour for each flow. If there is a spec, PRD or test describing it, read it. If not, write your assumption next to each flow and flag it for confirmation.
|
|
24
|
+
|
|
25
|
+
## 1. Inventory controls
|
|
26
|
+
|
|
27
|
+
Search the scoped components for every user-facing trigger and list them:
|
|
28
|
+
|
|
29
|
+
- Event props: `onClick`, `onChange`, `onSubmit`, `onBlur`, `onKeyDown`, `@click`, `v-model`, `(click)`, `bind:value`, `on:submit`.
|
|
30
|
+
- Indirect triggers: route changes, URL param edits, drag/drop, keyboard shortcuts, timers and polling, websocket or SSE messages, window focus refetch, `storage` events from other tabs.
|
|
31
|
+
|
|
32
|
+
Give each an ID (`F1`, `F2`...) with file and line.
|
|
33
|
+
|
|
34
|
+
## 2. Trace each flow
|
|
35
|
+
|
|
36
|
+
For each control, follow the chain until nothing else changes. Write it as a sequence:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
F3 "Apply filter" button (Filters.tsx:42)
|
|
40
|
+
1. onClick -> setFilters({...filters, status}) Filters.tsx:44
|
|
41
|
+
2. filters is a dependency of useEffect -> setPage(1) ResultsList.tsx:18
|
|
42
|
+
3. [filters, page] in query key -> GET /api/items?status&page useItems.ts:9
|
|
43
|
+
4. URL sync effect writes ?status=... but not page useUrlState.ts:31
|
|
44
|
+
5. Response -> cache -> list renders
|
|
45
|
+
Final state: filter applied, page 1, URL missing page
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
At each step record: what changed, who reads it, and what that reader does. Follow:
|
|
49
|
+
|
|
50
|
+
- Every setter call and every place that state is read (search for its name).
|
|
51
|
+
- Effects/watchers/computed values that depend on it, and what they set in turn.
|
|
52
|
+
- Store actions, reducers and middleware/sagas/listeners they trigger.
|
|
53
|
+
- Network calls: request, success path, error path, and what happens if two are in flight.
|
|
54
|
+
- Derived or mirrored state (props copied into state, state copied to URL or storage), the commonest source of fights.
|
|
55
|
+
|
|
56
|
+
## 3. Cross-check pairs and sequences
|
|
57
|
+
|
|
58
|
+
The bugs are in combinations. For each flow, check against the others that touch the same state:
|
|
59
|
+
|
|
60
|
+
| Check | Question | Typical bug |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Shared writes | Do two flows write the same state, URL param or cache key? | Second handler or an effect silently undoes the first |
|
|
63
|
+
| Effect loops | Does an effect write state that triggers another effect that writes back? | Value snaps back, infinite render loop, flicker |
|
|
64
|
+
| Order | Does the final state depend on which handler or effect runs first? | `onBlur` save vs `onClick` cancel; parent and child both handle the event |
|
|
65
|
+
| Stale closure | Does a handler, timer or subscription capture an old value? | Increment uses the value from mount; interval sees initial filters |
|
|
66
|
+
| Races | Can responses arrive out of order? Is there cancellation (`AbortController`, query keys, ignore flag)? | Typing "ab" shows results for "a" |
|
|
67
|
+
| Repeat | What if the user clicks twice, double submits, or toggles fast? | Duplicate POST, toggle ends in wrong state, optimistic update not rolled back |
|
|
68
|
+
| Optimistic updates | Is rollback on error correct, and does a refetch overwrite a newer local change? | Item reappears after delete; like count jumps |
|
|
69
|
+
| Reset paths | What resets this state (route change, logout, form reset, key change)? Does it reset everything it should, and nothing else? | Old filter survives navigation; modal reopens with last user's data |
|
|
70
|
+
| Remount | Does a changing `key`, conditional render or route remount the component and lose state? | Input clears while typing |
|
|
71
|
+
| Disabled/loading | Is the control disabled while its action runs, and re-enabled on error? | Stuck spinner after a failed request |
|
|
72
|
+
| External sync | Back/forward buttons, a second tab, a websocket event: do they agree with local state? | URL says page 3, list shows page 1 |
|
|
73
|
+
|
|
74
|
+
## 4. Confirm
|
|
75
|
+
|
|
76
|
+
For each suspected problem, try to confirm it before reporting:
|
|
77
|
+
|
|
78
|
+
1. Re-read the exact lines; check the framework's semantics (for example, React 18+ batches all setters in the same tick, including in promises and timeouts; `useEffect` normally runs after paint, but after a discrete event such as a click React flushes it before the browser paints; Vue watchers are batched and by default run before the next component update, `flush: 'pre'`).
|
|
79
|
+
2. If tests exist, look for one covering the combination. If the user allows running things, run the app and reproduce, or write a throwaway test outside the source tree (or delete it afterwards), and note the result. Do not commit anything.
|
|
80
|
+
3. Mark each finding **confirmed** (reproduced or provable from the code alone) or **suspected** (depends on timing or data you could not check). Drop anything you cannot point to a line for.
|
|
81
|
+
|
|
82
|
+
## Never
|
|
83
|
+
|
|
84
|
+
- Never edit source files during the audit. Suggested fixes go in the report.
|
|
85
|
+
- Never report a hypothetical without the sequence of steps that produces it.
|
|
86
|
+
- Never assume a library's behaviour; check its docs or source when a finding depends on it (cache invalidation, batching, debounce semantics).
|
|
87
|
+
|
|
88
|
+
## Report
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
## UI flow audit: <scope>
|
|
92
|
+
Assumptions to confirm: <list>
|
|
93
|
+
|
|
94
|
+
### Flow map
|
|
95
|
+
F1 <control> (file:line): <one-line chain> -> final state OK | PROBLEM
|
|
96
|
+
F2 ...
|
|
97
|
+
|
|
98
|
+
### Findings (most severe first)
|
|
99
|
+
1. [confirmed|suspected] <title> Flows: F3 + F7
|
|
100
|
+
Steps: <user steps that trigger it>
|
|
101
|
+
Chain: <file:line -> file:line -> ...>
|
|
102
|
+
Expected: <...> Actual: <...>
|
|
103
|
+
Fix: <smallest change, e.g. derive instead of mirror, cancel stale request, single owner for the URL param>
|
|
104
|
+
Test to add: <the combination test that would catch it>
|
|
105
|
+
2. ...
|
|
106
|
+
|
|
107
|
+
### Clean flows
|
|
108
|
+
<IDs that were traced and are fine>
|
|
109
|
+
|
|
110
|
+
### Not traced
|
|
111
|
+
<controls or triggers skipped and why>
|
|
112
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"When I change the sort order and then the filter, the sort snaps back to default; trace what every control on the search page actually does",
|
|
4
|
+
"Each button on the cart screen works on its own but combinations leave the wrong total, audit how the handlers and effects interact",
|
|
5
|
+
"Map every click handler, effect and store update on the booking form and find where they fight each other",
|
|
6
|
+
"The dark mode toggle flips back after a second, follow the state through the context, the effect and localStorage to see why",
|
|
7
|
+
"Before release, audit the kanban board's interactions for races, double submits and stale state between actions"
|
|
8
|
+
],
|
|
9
|
+
"should_not_trigger": [
|
|
10
|
+
"Click through the staging build and write up any bugs you find before the demo",
|
|
11
|
+
"Review my React pull request for missing useEffect dependencies",
|
|
12
|
+
"Should this modal's open state live in the URL or in component state?",
|
|
13
|
+
"Write React Testing Library tests for the cart total component",
|
|
14
|
+
"Draw the user journey for onboarding as a flowchart for the product team",
|
|
15
|
+
"Our dashboard renders slowly when filters change, profile it"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-motion
|
|
3
|
+
description: Interface animation for the web with the Motion library (motion/react, formerly Framer Motion) and plain CSS transitions, keyframes and the View Transitions API - motion duration and easing tokens, spring physics, hover, tap and drag gestures, AnimatePresence exit animations, layout and shared-element transitions, route changes, prefers-reduced-motion fallbacks, and keeping animation on transform and opacity within the frame budget. Use when adding or fixing a micro-interaction, modal or list animation, page transition or draggable element, an animation stutters or causes layout shift, or motion must respect reduced-motion settings.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Motion examples need React 18+ and the motion npm package; CSS and View Transitions sections apply to any framework.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: coding
|
|
9
|
+
kete-loom/risk: writes-files
|
|
10
|
+
kete-loom/owner: "Kete-org"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# UI motion
|
|
14
|
+
|
|
15
|
+
Animation in a product UI has a job: show where something came from, confirm an action, or keep the user oriented during a change. If it does none of these, leave it out. Good UI motion is short, consistent, interruptible and optional.
|
|
16
|
+
|
|
17
|
+
Neighbours: `remotion-video` and `manim-animations` render video; `ui-design-polish` sets the visual direction; `react-performance` covers render cost. This skill is live interface animation.
|
|
18
|
+
|
|
19
|
+
## 0. Before you start
|
|
20
|
+
|
|
21
|
+
1. Check what the project has: `motion` or `framer-motion` in `package.json`, a CSS animation convention, or existing duration/easing variables. Use what is there. Do not add Motion for something a CSS transition does.
|
|
22
|
+
2. Note the import path in use. Current Motion: `import { motion, AnimatePresence } from "motion/react"`. Older code imports the same names from `"framer-motion"`; both work, do not mix them in one codebase. Migrating is a separate change; **ask first**.
|
|
23
|
+
3. Ask what the motion is for if it is not obvious. "Make it feel alive" needs a concrete target: which element, which change, what it should communicate.
|
|
24
|
+
|
|
25
|
+
## 1. Decide: CSS or Motion
|
|
26
|
+
|
|
27
|
+
| Need | Use |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Hover, focus, colour or simple show/hide state | CSS `transition` |
|
|
30
|
+
| Looping or decorative keyframes (spinner, shimmer) | CSS `@keyframes` |
|
|
31
|
+
| Exit animation when React unmounts an element | Motion `AnimatePresence` |
|
|
32
|
+
| Element changes size/position because layout changed (reorder, expand) | Motion `layout` |
|
|
33
|
+
| Same thing moves between two components (thumbnail to detail) | Motion `layoutId`, or View Transitions |
|
|
34
|
+
| Drag, swipe, pan with momentum | Motion `drag` |
|
|
35
|
+
| Interruptible, velocity-aware motion | Motion springs |
|
|
36
|
+
| Page or route change in a multi-page or SPA app | View Transitions API where supported, else Motion `AnimatePresence` around the route |
|
|
37
|
+
|
|
38
|
+
## 2. Motion tokens
|
|
39
|
+
|
|
40
|
+
Define once and reuse (values to start from in `references/tokens.md`):
|
|
41
|
+
|
|
42
|
+
- Durations: `--duration-fast` 100-150ms (hover, press), `--duration-base` 200-250ms (small enter/exit), `--duration-slow` 300-400ms (modals, page-level). UI animation over 500ms feels slow.
|
|
43
|
+
- Easing: ease-out for entering (`cubic-bezier(0.2, 0, 0, 1)` style curves), ease-in for leaving, ease-in-out for moving on screen. Never linear except for progress and spinners.
|
|
44
|
+
- Springs: two or three named presets (`snappy`, `gentle`, `bouncy`) as Motion transition objects.
|
|
45
|
+
- Exits are faster than entrances (about 75% of the enter duration).
|
|
46
|
+
|
|
47
|
+
## 3. Build it
|
|
48
|
+
|
|
49
|
+
Core patterns with code in `references/patterns.md`:
|
|
50
|
+
|
|
51
|
+
- **Enter/exit**: wrap conditionally rendered elements in `<AnimatePresence>`, give each child a stable `key`, set `initial`, `animate` and `exit`. Use `mode="wait"` when one element replaces another; `initial={false}` on `AnimatePresence` to skip the animation on first page load.
|
|
52
|
+
- **Gestures**: `whileHover`, `whileTap`, `whileFocus` for feedback. Tap feedback is a small scale (0.97-0.98), not a bounce.
|
|
53
|
+
- **Drag**: `drag` (`true`, `"x"` or `"y"`), `dragConstraints` (pixel object or a ref to a container), `dragElastic` (0-1), `dragMomentum={false}` to stop on release, `onDragEnd={(e, info) => ...}` using `info.offset` and `info.velocity` to decide a swipe. Always provide a keyboard or button alternative to a drag action.
|
|
54
|
+
- **Layout**: `layout` on elements whose position or size changes; `layout="position"` when only position should animate (stops text squashing); `layoutId` for shared elements. Wrap siblings that affect each other in `<LayoutGroup>`.
|
|
55
|
+
- **Lists**: stagger with `transition={{ delay: index * 0.03 }}` or variants with `staggerChildren`; cap total stagger at about 300ms so long lists do not take seconds.
|
|
56
|
+
- **Springs**: `transition={{ type: "spring", visualDuration: 0.3, bounce: 0.2 }}` is easiest to reason about; or `stiffness`/`damping`/`mass` (these override `bounce` and `duration`). Bounce 0 for UI chrome, 0.1-0.3 for playful elements.
|
|
57
|
+
- **Bundle size**: in bundle-sensitive apps use `LazyMotion` with `domAnimation` (no drag or layout) or `domMax` and the `m` component from `"motion/react-m"`.
|
|
58
|
+
|
|
59
|
+
## 4. Reduced motion
|
|
60
|
+
|
|
61
|
+
Required, not optional (WCAG 2.2 SC 2.3.3 Animation from Interactions is AAA, but vestibular disorders are common and the OS setting is the user's explicit request).
|
|
62
|
+
|
|
63
|
+
- App-wide in Motion: `<MotionConfig reducedMotion="user">` at the root. It disables transform and layout animations when the OS setting is on and keeps opacity and colour changes.
|
|
64
|
+
- Per component: `const reduce = useReducedMotion();` then swap movement for a fade.
|
|
65
|
+
- CSS: `@media (prefers-reduced-motion: reduce) { ... }` removing translation, parallax, auto-play and large scale changes. Keep short opacity fades so state changes are still visible.
|
|
66
|
+
- Auto-playing or looping motion longer than 5 seconds needs a pause control (WCAG 2.2.2).
|
|
67
|
+
- Nothing should flash more than three times per second (WCAG 2.3.1).
|
|
68
|
+
|
|
69
|
+
## 5. Performance
|
|
70
|
+
|
|
71
|
+
- Animate `transform` (`x`, `y`, `scale`, `rotate`) and `opacity`. These can run on the compositor. Animating `width`, `height`, `top`, `left`, `margin` or `box-shadow` triggers layout or paint every frame; use `layout` animations or `transform: scale` instead.
|
|
72
|
+
- Frame budget: 16.7ms at 60Hz, 8.3ms at 120Hz. Check with the browser's Performance panel (look for long tasks and layout/paint during the animation) and the Rendering panel's paint flashing.
|
|
73
|
+
- `will-change: transform` only on elements about to animate, removed after; not on everything.
|
|
74
|
+
- Do not drive animation from React state set every frame; use Motion values (`useMotionValue`, `useTransform`, `useScroll`) which update without re-rendering.
|
|
75
|
+
- Scroll-linked effects: prefer CSS scroll-driven animations (`animation-timeline: scroll()` / `view()`) where browser support fits, or Motion `useScroll`; never a `scroll` event listener that sets state.
|
|
76
|
+
- Test on a throttled CPU (4x slowdown) and a real low-end phone if the audience uses them.
|
|
77
|
+
|
|
78
|
+
## 6. Verify
|
|
79
|
+
|
|
80
|
+
1. Trigger each animation, then interrupt it (click again mid-animation, navigate away). It must not jump, stack or leave an element half-visible.
|
|
81
|
+
2. Turn on the OS reduced-motion setting (or emulate `prefers-reduced-motion` in browser devtools) and repeat.
|
|
82
|
+
3. Record a performance profile of the heaviest animation; no dropped frames on the target device class.
|
|
83
|
+
4. Check keyboard: focus lands correctly after a modal opens or a route changes, and is not lost to an exiting element.
|
|
84
|
+
5. Check no layout shift is introduced (elements entering should not push content after load).
|
|
85
|
+
|
|
86
|
+
## Never
|
|
87
|
+
|
|
88
|
+
- Never block input until an animation finishes, or make users wait for motion to reach content.
|
|
89
|
+
- Never animate on page load just because you can; entrance animations on every scroll section get tiring.
|
|
90
|
+
- Never use motion as the only signal of a state change.
|
|
91
|
+
- Never mix `framer-motion` and `motion/react` imports, or two animation libraries on one element.
|
|
92
|
+
|
|
93
|
+
## Report back
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
## Motion: <scope>
|
|
97
|
+
Added/changed: <element: trigger -> animation, duration/spring, file>
|
|
98
|
+
Tokens: <defined or reused>
|
|
99
|
+
Reduced motion: <how each animation degrades>
|
|
100
|
+
Performance: <properties animated; profile result or "not profiled">
|
|
101
|
+
Interruptions tested: <yes/no, what>
|
|
102
|
+
Not done: <items and why>
|
|
103
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Make the notification toasts slide in and fade out when dismissed using motion/react",
|
|
4
|
+
"Our accordion animates height and it janks on mobile, make it smooth",
|
|
5
|
+
"Add a shared-element transition so the product thumbnail grows into the detail page image",
|
|
6
|
+
"Let users swipe cards left or right to dismiss them, with a spring back if they don't swipe far enough",
|
|
7
|
+
"How do I make all our Framer Motion animations respect prefers-reduced-motion?",
|
|
8
|
+
"Define duration and easing tokens so every hover and modal transition in the app feels consistent"
|
|
9
|
+
],
|
|
10
|
+
"should_not_trigger": [
|
|
11
|
+
"Render a 30 second promo MP4 from React components with spring animations",
|
|
12
|
+
"Animate a visual proof of the chain rule for a maths video",
|
|
13
|
+
"Pick a colour palette and type scale so the admin panel stops looking generic",
|
|
14
|
+
"The product grid re-renders on every filter change, profile and fix it",
|
|
15
|
+
"Check that our modal traps focus and is announced by screen readers",
|
|
16
|
+
"Write Playwright tests for the drag-and-drop kanban board"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Motion patterns
|
|
2
|
+
|
|
3
|
+
Motion for React (`npm i motion`, imports from `"motion/react"`). Check the project's installed version; the API below matches current Motion docs.
|
|
4
|
+
|
|
5
|
+
## Enter and exit
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
9
|
+
|
|
10
|
+
<AnimatePresence>
|
|
11
|
+
{open && (
|
|
12
|
+
<motion.div
|
|
13
|
+
key="panel"
|
|
14
|
+
initial={{ opacity: 0, y: 8 }}
|
|
15
|
+
animate={{ opacity: 1, y: 0 }}
|
|
16
|
+
exit={{ opacity: 0, y: 4, transition: { duration: 0.12 } }}
|
|
17
|
+
transition={spring.gentle}
|
|
18
|
+
/>
|
|
19
|
+
)}
|
|
20
|
+
</AnimatePresence>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The conditional must be inside `AnimatePresence`; if the whole `AnimatePresence` unmounts, there is no exit animation.
|
|
24
|
+
|
|
25
|
+
## Replace one element with another
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
29
|
+
<motion.span key={status} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
30
|
+
{label}
|
|
31
|
+
</motion.span>
|
|
32
|
+
</AnimatePresence>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Changing `key` is what triggers exit then enter.
|
|
36
|
+
|
|
37
|
+
## Reorderable list
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<ul>
|
|
41
|
+
<AnimatePresence initial={false}>
|
|
42
|
+
{items.map((item) => (
|
|
43
|
+
<motion.li key={item.id} layout initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
44
|
+
{item.name}
|
|
45
|
+
</motion.li>
|
|
46
|
+
))}
|
|
47
|
+
</AnimatePresence>
|
|
48
|
+
</ul>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use `item.id` keys, never the array index, or the wrong items animate. For drag-to-reorder, Motion has `Reorder.Group` / `Reorder.Item`.
|
|
52
|
+
|
|
53
|
+
## Shared element
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// In the grid
|
|
57
|
+
<motion.img layoutId={`photo-${id}`} src={thumb} />
|
|
58
|
+
// In the detail view
|
|
59
|
+
<motion.img layoutId={`photo-${id}`} src={full} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If text inside a `layout` element distorts, add `layout` to the child as well, or use `layout="position"`.
|
|
63
|
+
|
|
64
|
+
## Swipe to dismiss
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<motion.div
|
|
68
|
+
drag="x"
|
|
69
|
+
dragConstraints={{ left: 0, right: 0 }}
|
|
70
|
+
dragElastic={0.6}
|
|
71
|
+
onDragEnd={(_, info) => {
|
|
72
|
+
if (Math.abs(info.offset.x) > 120 || Math.abs(info.velocity.x) > 500) onDismiss();
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Also render a visible dismiss button: drag is not available to keyboard or switch users.
|
|
78
|
+
|
|
79
|
+
## Scroll-linked progress bar
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const { scrollYProgress } = useScroll();
|
|
83
|
+
return <motion.div style={{ scaleX: scrollYProgress, transformOrigin: "0 50%" }} className="progress" />;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
CSS-only alternative where supported:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
.progress { transform-origin: 0 50%; animation: grow linear both; animation-timeline: scroll(); }
|
|
90
|
+
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Reduced motion
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { MotionConfig, useReducedMotion } from "motion/react";
|
|
97
|
+
|
|
98
|
+
<MotionConfig reducedMotion="user">{app}</MotionConfig> // "user" | "always" | "never"
|
|
99
|
+
|
|
100
|
+
function Card() {
|
|
101
|
+
const reduce = useReducedMotion();
|
|
102
|
+
return <motion.div initial={{ opacity: 0, y: reduce ? 0 : 16 }} animate={{ opacity: 1, y: 0 }} />;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## View Transitions API (no library)
|
|
107
|
+
|
|
108
|
+
Same-document (SPA):
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
if (!document.startViewTransition) updateDom();
|
|
112
|
+
else document.startViewTransition(() => updateDom());
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```css
|
|
116
|
+
.hero-image { view-transition-name: hero; } /* unique per page */
|
|
117
|
+
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 200ms; }
|
|
118
|
+
@media (prefers-reduced-motion: reduce) {
|
|
119
|
+
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none; }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Cross-document (multi-page sites): add `@view-transition { navigation: auto; }` to both pages' CSS. Check current browser support on MDN or caniuse before relying on it; it degrades to a normal navigation where unsupported.
|
|
124
|
+
|
|
125
|
+
In frameworks, use their wrapper when one exists (for example Astro's `<ClientRouter />` or the router's view-transition option) rather than calling the API by hand around router internals.
|
|
126
|
+
|
|
127
|
+
## Bundle-size mode
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { LazyMotion, domAnimation } from "motion/react";
|
|
131
|
+
import * as m from "motion/react-m";
|
|
132
|
+
|
|
133
|
+
<LazyMotion features={domAnimation} strict>
|
|
134
|
+
<m.div animate={{ opacity: 1 }} />
|
|
135
|
+
</LazyMotion>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`domAnimation` covers animations, variants, exit and tap/hover/focus gestures; `domMax` adds drag, pan and layout animations. `strict` throws if a full `motion` component sneaks in.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Motion tokens
|
|
2
|
+
|
|
3
|
+
Starting values; tune to the product's tone. Keep CSS and JS in sync by defining them once.
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
:root {
|
|
7
|
+
--duration-instant: 75ms;
|
|
8
|
+
--duration-fast: 150ms;
|
|
9
|
+
--duration-base: 220ms;
|
|
10
|
+
--duration-slow: 350ms;
|
|
11
|
+
|
|
12
|
+
--ease-out: cubic-bezier(0.2, 0, 0, 1); /* enter */
|
|
13
|
+
--ease-in: cubic-bezier(0.4, 0, 1, 1); /* exit */
|
|
14
|
+
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* move on screen */
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (prefers-reduced-motion: reduce) {
|
|
18
|
+
:root {
|
|
19
|
+
--duration-fast: 1ms;
|
|
20
|
+
--duration-base: 1ms;
|
|
21
|
+
--duration-slow: 1ms;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Setting durations to `1ms` rather than `0` keeps `transitionend` / `animationend` events firing for code that waits on them. Only do this blanket reduction if every animation is decorative; otherwise keep opacity fades and remove movement per component.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// motion-tokens.ts
|
|
30
|
+
import type { Transition } from "motion/react";
|
|
31
|
+
|
|
32
|
+
export const spring = {
|
|
33
|
+
snappy: { type: "spring", visualDuration: 0.2, bounce: 0 },
|
|
34
|
+
gentle: { type: "spring", visualDuration: 0.35, bounce: 0.1 },
|
|
35
|
+
bouncy: { type: "spring", visualDuration: 0.45, bounce: 0.3 },
|
|
36
|
+
} satisfies Record<string, Transition>;
|
|
37
|
+
|
|
38
|
+
export const fade = { duration: 0.15, ease: [0.2, 0, 0, 1] } satisfies Transition;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What to use where
|
|
42
|
+
|
|
43
|
+
| Element | Enter | Exit | Notes |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Button press | scale 0.97, `instant` | back, `fast` | `whileTap` |
|
|
46
|
+
| Tooltip | opacity + 4px y, `fast` | opacity, `instant` | delay show 300-500ms, hide immediately |
|
|
47
|
+
| Dropdown / popover | opacity + scale 0.96 from trigger origin, `fast` | opacity, `instant` | `transform-origin` at the trigger |
|
|
48
|
+
| Toast | slide + fade, `gentle` spring | fade, `fast` | |
|
|
49
|
+
| Modal | backdrop fade `base`; panel scale 0.96 + fade | faster than enter | return focus to the trigger |
|
|
50
|
+
| Drawer / sheet | translate from edge, `snappy` spring | translate out | drag to dismiss with velocity threshold |
|
|
51
|
+
| List add/remove | height via `layout`, fade | fade, then siblings close gap | `AnimatePresence` + `layout` |
|
|
52
|
+
| Route change | crossfade `base` or shared element | | keep under 300ms |
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unicode-text-processing
|
|
3
|
+
description: Code-level correctness for text in complex and non-Latin scripts such as Bengali, Devanagari, Tamil, Arabic, Hebrew, Thai, Khmer, Chinese, Japanese and Korean - choosing NFC versus NFKC normalization and case folding, counting and truncating by grapheme cluster instead of code unit, detecting scripts from the Unicode Script property, dictionary-based word segmentation for languages written without spaces, ICU collation, locale-aware sorting and search folding, bidirectional text isolation, display width, and confusable or invisible characters. Use when string length, slicing, sorting, search matching, tokenization or rendering breaks on non-English text, when a database or search index must compare such text correctly, or when user input mixes scripts.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Examples use Python 3 (unicodedata, the regex package, PyICU), JavaScript (Intl.Segmenter, Intl.Collator, String.prototype.normalize), Java (java.text, ICU4J) and PostgreSQL ICU collations; use the equivalent ICU binding in other languages.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: data
|
|
9
|
+
kete-loom/risk: writes-files
|
|
10
|
+
kete-loom/owner: "Kete-org"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Unicode text processing
|
|
14
|
+
|
|
15
|
+
A "character" is not a byte, not a UTF-16 code unit and often not a code point. Most bugs with non-Latin text come from code that assumes one of those. Fix them with the Unicode algorithms and ICU, never with hand-written character tables. This skill is about string handling in code; translating an app's UI and locale formatting is the `i18n-localization` skill.
|
|
16
|
+
|
|
17
|
+
Standards to cite in comments and reviews: UAX #15 (normalization), UAX #29 (text segmentation), UAX #24 (script property), UTS #10 (collation), UAX #9 (bidi), UAX #11 (East Asian width), UTS #39 (security and confusables). Library calls per language are in `references/apis.md`; script-specific pitfalls in `references/script-notes.md`.
|
|
18
|
+
|
|
19
|
+
## 0. Reproduce with real text
|
|
20
|
+
|
|
21
|
+
1. Get a failing sample in the real script from the user or the data, as text and as code points (`[hex(ord(c)) for c in s]` in Python, `[...s].map(c => c.codePointAt(0).toString(16))` in JS). Screenshots hide the difference between look-alike sequences.
|
|
22
|
+
2. Confirm the encoding end to end: files, HTTP `Content-Type; charset=utf-8`, database encoding and connection charset (MySQL `utf8mb4`, not `utf8`). Mojibake like `ব` means UTF-8 bytes were decoded as Latin-1 or Windows-1252 somewhere; fix the boundary, never "repair" the data with replace tables.
|
|
23
|
+
3. Write a test with that sample before changing code.
|
|
24
|
+
|
|
25
|
+
## 1. Normalize at the boundary
|
|
26
|
+
|
|
27
|
+
| Form | Use for | Avoid for |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| NFC | Storing and comparing user text; default for the web and most APIs | |
|
|
30
|
+
| NFD | Stripping combining marks for accent-insensitive search keys (Latin) | Storing text |
|
|
31
|
+
| NFKC | Identifiers, usernames, search keys where full-width and compatibility forms should match (`A` vs `A`, `fi` vs `fi`) | Storing the user's text: it is lossy |
|
|
32
|
+
| NFKC_Casefold / `casefold()` after NFKC | Case-insensitive identifier matching | Display |
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Normalize once when text enters the system (API input, file import), store NFC, and normalize again before comparing with outside data.
|
|
36
|
+
- NFC and NFKC give different results for the same input: compare strings normalized to the same form.
|
|
37
|
+
- Case-insensitive matching uses case folding (`str.casefold()`, ICU `foldCase`), not `lower()`. For Turkish and Azerbaijani, dotted and dotless i need the locale-specific rules.
|
|
38
|
+
- For search keys, keep the original string for display and store a separate folded key.
|
|
39
|
+
|
|
40
|
+
## 2. Count, slice and truncate by grapheme cluster
|
|
41
|
+
|
|
42
|
+
What a user sees as one character can be several code points: Bengali `ক্ষ` (conjunct), Devanagari `कि` (consonant plus vowel sign), emoji with skin tones or ZWJ families, flags, Hangul jamo sequences.
|
|
43
|
+
|
|
44
|
+
- Length limits shown to users ("max 50 characters") count extended grapheme clusters (UAX #29): Python `regex.findall(r"\X", s)`, JS `Intl.Segmenter(undefined, { granularity: "grapheme" })`, ICU `BreakIterator.getCharacterInstance`.
|
|
45
|
+
- Storage limits (database column bytes, SMS segments, protocol fields) count bytes or code units; check them separately and **truncate at a grapheme boundary**, never mid-cluster, then verify the byte length.
|
|
46
|
+
- Never reverse, split or slice user-visible strings by index without segmenting them first.
|
|
47
|
+
- Note: a grapheme cluster is not always one Indic syllable (aksara). If the product needs syllable-level units (for example a cursor or a typing tutor for Bengali), check with the user. Unicode 15.1 added rule GB9c, so conjuncts such as `ক্ষ` in Bengali, Devanagari, Gujarati, Malayalam, Oriya and Telugu are one extended grapheme cluster; libraries on older Unicode data split them after the virama, so check the library's Unicode version.
|
|
48
|
+
|
|
49
|
+
## 3. Detect scripts
|
|
50
|
+
|
|
51
|
+
1. Use the Unicode `Script` and `Script_Extensions` properties (UAX #24): Python `regex` `\p{Script=Bengali}` / `\p{scx=Beng}`, JS `/\p{Script=Bengali}/u`, ICU `UScript`.
|
|
52
|
+
2. Ignore characters whose script is `Common` or `Inherited` (digits, punctuation, combining marks) when deciding the dominant script.
|
|
53
|
+
3. Script is not language: Bengali script covers Bengali and Assamese; Devanagari covers Hindi, Marathi, Nepali and Sanskrit; Arabic script covers Arabic, Persian and Urdu. Use a language-ID model only if the language matters, and report its confidence.
|
|
54
|
+
4. Mixed-script identifiers (Latin plus Cyrillic in one username) are a spoofing signal; see step 7.
|
|
55
|
+
|
|
56
|
+
## 4. Segment words and tokenize
|
|
57
|
+
|
|
58
|
+
- Languages written without spaces between words (Thai, Lao, Khmer, Burmese, Chinese, Japanese) need dictionary-based word segmentation: ICU `BreakIterator.getWordInstance(locale)` or `Intl.Segmenter(locale, { granularity: "word" })` and its `isWordLike` flag. Splitting on whitespace returns whole sentences.
|
|
59
|
+
- For Chinese and Japanese search, character bigrams or a dedicated analyzer (for example a morphological analyzer for Japanese) usually beat ICU word breaks; choose with the user and measure on their queries.
|
|
60
|
+
- Splitting on `\w` or `[a-zA-Z]` breaks Indic and Arabic words, because combining vowel signs and virama are marks (`\p{M}`), not letters. Use `[\p{L}\p{M}\p{N}]+` with a Unicode-aware regex engine, or a word segmenter.
|
|
61
|
+
- For ML tokenizers, normalize (NFC or the model's documented form) before tokenizing, and check the tokenizer does not strip ZWJ or ZWNJ, which change meaning and shape in Indic and Persian text.
|
|
62
|
+
|
|
63
|
+
## 5. Sort and search with collation
|
|
64
|
+
|
|
65
|
+
- Never sort user-visible text by code point. Use ICU collation for the user's locale (UTS #10 plus CLDR tailoring): Python `icu.Collator.createInstance(icu.Locale("bn"))`, JS `Intl.Collator("bn")`, Java `Collator.getInstance(Locale.forLanguageTag("bn"))`.
|
|
66
|
+
- Choose strength deliberately: primary ignores accents and case, secondary ignores case, tertiary compares both.
|
|
67
|
+
- In the database, use ICU collations (PostgreSQL `CREATE COLLATION bn_icu (provider = icu, locale = 'bn-BD')`; nondeterministic collations for case- or accent-insensitive equality) and index with the same collation the queries use.
|
|
68
|
+
- For search, build a folded key: NFKC, case fold, and the script-specific foldings the user agrees to (for example removing Arabic tatweel and optional diacritics, unifying alef forms). Document every folding; each one makes some distinct words match.
|
|
69
|
+
- Collation rules change with ICU and CLDR versions: rebuild indexes that depend on collation after an ICU upgrade (PostgreSQL warns about collation version mismatches).
|
|
70
|
+
|
|
71
|
+
## 6. Bidirectional text
|
|
72
|
+
|
|
73
|
+
- Set `dir="auto"` on user-generated content and wrap embedded values in `<bdi>` or Unicode isolates (FSI U+2068 ... PDI U+2069) so a Hebrew or Arabic name does not reorder the surrounding text.
|
|
74
|
+
- Use logical order in storage; never reverse strings to "fix" display.
|
|
75
|
+
- Use CSS logical properties (`margin-inline-start`) for layout; layout mirroring belongs to the `i18n-localization` skill.
|
|
76
|
+
|
|
77
|
+
## 7. Security and invisible characters
|
|
78
|
+
|
|
79
|
+
- Strip or reject bidi control characters (U+202A to U+202E, U+2066 to U+2069) in source code, filenames and identifiers; they enable "Trojan Source" style attacks (CVE-2021-42574).
|
|
80
|
+
- Check identifiers for confusables and mixed scripts with UTS #39 (ICU `SpoofChecker`).
|
|
81
|
+
- Keep ZWJ (U+200D) and ZWNJ (U+200C) in natural-language text where they are meaningful; remove them only in identifiers where policy says so.
|
|
82
|
+
|
|
83
|
+
## 8. Display width and rendering
|
|
84
|
+
|
|
85
|
+
- Terminal and fixed-width layouts: use East Asian Width and `wcwidth`, not `len`, and measure per grapheme.
|
|
86
|
+
- Rendering needs a font that covers the script and a shaping engine (HarfBuzz in browsers and most toolkits). Boxes (tofu) or detached vowel signs mean a missing font or no shaping, not bad data.
|
|
87
|
+
|
|
88
|
+
## Never
|
|
89
|
+
|
|
90
|
+
- Never compare or deduplicate user text without normalizing both sides to the same form.
|
|
91
|
+
- Never store NFKC-folded text as the only copy of the user's input.
|
|
92
|
+
- Never hand-roll tables of letters, vowels or word lists for a script; use Unicode properties and ICU.
|
|
93
|
+
- Never silently strip characters you do not recognise; log and report them.
|
|
94
|
+
|
|
95
|
+
## Report back
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Symptom: ... (sample as text and code points)
|
|
99
|
+
Cause: which assumption failed (code units, no normalization, whitespace tokenization, code-point sort, ...)
|
|
100
|
+
Fix: files changed, library calls used, form chosen (NFC/NFKC), collation and strength
|
|
101
|
+
Tests: samples per script covered, command run, results
|
|
102
|
+
Follow-ups: indexes to rebuild, data to re-normalize (with a migration the user approves)
|
|
103
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Our 50-character username limit cuts Bengali names in half and leaves a broken vowel sign",
|
|
4
|
+
"Searching for a Hindi word misses results because some rows store the nukta letter differently",
|
|
5
|
+
"Split Thai product descriptions into words for our search index, there are no spaces",
|
|
6
|
+
"Sort a list of Bengali and Arabic customer names correctly instead of by code point",
|
|
7
|
+
"Detect whether each support message is written in Devanagari, Arabic or Latin script",
|
|
8
|
+
"An Arabic name inside an English sentence flips the punctuation around in our notification text"
|
|
9
|
+
],
|
|
10
|
+
"should_not_trigger": [
|
|
11
|
+
"Translate the checkout screen into Hindi and Bengali and add the message catalogs",
|
|
12
|
+
"Format prices and dates for Japanese users in the dashboard",
|
|
13
|
+
"Review this UI diff for hard-coded strings before it ships in other languages",
|
|
14
|
+
"OCR these scanned Bengali land records into searchable PDFs",
|
|
15
|
+
"Write a style guide for our Arabic marketing copy"
|
|
16
|
+
]
|
|
17
|
+
}
|