@duro-app/cli 4.2.0 → 4.4.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 CHANGED
@@ -130,12 +130,14 @@ hand-roll widgets the DS ships.
130
130
  ```
131
131
 
132
132
  Wire `--check` into the repo's lint or CI job to catch a stale hook after a
133
- CLI upgrade. The generated script pins `@duro-app/cli@^3.4.0` — a floating
133
+ CLI upgrade. The generated script pins `@duro-app/cli@^4.4.0` — a floating
134
134
  pin, so patch releases never read as drift, but a **floor**: a repo whose
135
- script still pins `^1.2.0` never resolves a 2.x or 3.x payload and never sees
136
- what the session-start injection gained since. After a major, re-run
137
- `hook install` in every consumer and delete `.claude/.duro-session.cache*`
138
- (the payload is cached seven days).
135
+ script still pins `^3.4.0` never resolves a 4.x payload and never sees what
136
+ the session-start injection gained since. The release refuses a tag whose
137
+ major differs from that floor (`scripts/check-pin-major.mjs`). After a major,
138
+ re-run `hook install` (and `skill install`) in every consumer: the cache
139
+ records the floor it was fetched with, so the next session refetches it on its
140
+ own, and a failed refetch says so on stderr.
139
141
 
140
142
  ### Plan-mode nudge
141
143
 
@@ -187,7 +189,7 @@ wires both:
187
189
 
188
190
  ```
189
191
  # stage name scope severity command
190
- pre-commit duro-doctor *.css,package.json,root.tsx,main.tsx,vite.config.ts,vitest.config.ts,postcss.config.js,postcss.config.mjs,babel.config.js block npx -y @duro-app/cli@^3.4.0 doctor
192
+ pre-commit duro-doctor *.css,package.json,root.tsx,main.tsx,vite.config.ts,vitest.config.ts,postcss.config.js,postcss.config.mjs,babel.config.js block npx -y @duro-app/cli@^4.4.0 doctor
191
193
  ```
192
194
 
193
195
  ## Mockups that speak the design system
@@ -32,9 +32,18 @@ export declare const HOOK_COMMAND = "sh .claude/hooks/duro-catalog.sh";
32
32
  * sees what the payload gained (1.2.0 pinned every repo out of 2.x).
33
33
  * 3.0.0: the SCREENS section and the /duro-mockup pointer.
34
34
  * 3.4.0: duro doctor --session.
35
+ * 4.4.0: the first CLI whose payload matches @duro-app/ui 4 — a ^3 floor
36
+ * never reaches 4.x, so 4.x consumers were served 3.x docs. The
37
+ * release refuses a tag whose major differs from this floor's
38
+ * (scripts/check-pin-major.mjs).
35
39
  */
36
- export declare const HOOK_MIN_CLI = "3.4.0";
37
- export declare const HOOK_SCRIPT = "#!/bin/sh\n# Duro design-system session bootstrap \u2014 GENERATED by `duro hook install`.\n# Regenerate: npx -y @duro-app/cli hook install (--check verifies it in CI)\n#\n# Injects the consult-first preamble plus the component catalog into agent\n# context at SessionStart, so \"check the design system before building UI\" is\n# ambient context instead of a step an agent has to remember. Then runs\n# `duro doctor`, which says nothing unless this repo's build wiring flattens\n# the components' styles.\n#\n# Do not hand-edit. Repo-specific caveats go in\n# .claude/duro-hook.local.md \u2014 appended below, and left alone by regeneration.\ncache=\".claude/.duro-session.cache\"\nnotes=\".claude/duro-hook.local.md\"\ndoctor=\".claude/.duro-session.cache.doctor\"\n\n# npx resolution dominates session start; the catalog only changes on upgrade.\nif [ ! -s \"$cache\" ] || [ -n \"$(find \"$cache\" -mtime +7 2>/dev/null)\" ]; then\n if npx -y @duro-app/cli@^3.4.0 hook session-start >\"$cache.tmp\" 2>/dev/null; then\n mv \"$cache.tmp\" \"$cache\"\n else\n rm -f \"$cache.tmp\"\n fi\nfi\n\n# Offline with no cache yet: stay silent rather than fail the session.\nif [ -s \"$cache\" ]; then\n cat \"$cache\"\n if [ -s \"$notes\" ]; then\n echo\n cat \"$notes\"\n fi\nfi\n\n# duro doctor reads build config, entries and stylesheets. Re-run it when one\n# of those changes rather than on a timer, so an edit shows up next session\n# and a healthy repo only pays for npx after touching its config.\nkey=$(find . -maxdepth 4 \\( -name node_modules -o -name .git -o -name dist -o -name build \\) -prune -o \\\n -type f \\( -name package.json -o -name pnpm-workspace.yaml -o -name '*.css' -o -name 'vite*' \\\n -o -name 'babel*' -o -name '.babelrc*' -o -name '*.babel.*' -o -name 'postcss*' \\\n -o -name 'root.tsx' -o -name 'main.tsx' -o -name 'index.tsx' \\) -exec cksum {} + 2>/dev/null |\n sort | cksum)\nif [ \"$key\" != \"$(cat \"$doctor.key\" 2>/dev/null)\" ]; then\n if npx -y @duro-app/cli@^3.4.0 doctor --session >\"$doctor.tmp\" 2>/dev/null; then\n mv \"$doctor.tmp\" \"$doctor\"\n echo \"$key\" >\"$doctor.key\"\n else\n rm -f \"$doctor.tmp\"\n fi\nfi\nif [ -s \"$doctor\" ]; then\n echo\n cat \"$doctor\"\nfi\n";
40
+ export declare const HOOK_MIN_CLI = "4.4.0";
41
+ /**
42
+ * First line of the catalog cache: the floor it was fetched with. A cache
43
+ * from another floor is refetched, and this line is never printed.
44
+ */
45
+ export declare const HOOK_PIN_LINE = "#duro-hook-pin 4.4.0";
46
+ export declare const HOOK_SCRIPT = "#!/bin/sh\n# Duro design-system session bootstrap \u2014 GENERATED by `duro hook install`.\n# Regenerate: npx -y @duro-app/cli hook install (--check verifies it in CI)\n#\n# Injects the consult-first preamble plus the component catalog into agent\n# context at SessionStart, so \"check the design system before building UI\" is\n# ambient context instead of a step an agent has to remember. Then runs\n# `duro doctor`, which says nothing unless this repo's build wiring flattens\n# the components' styles.\n#\n# Do not hand-edit. Repo-specific caveats go in\n# .claude/duro-hook.local.md \u2014 appended below, and left alone by regeneration.\ncache=\".claude/.duro-session.cache\"\nnotes=\".claude/duro-hook.local.md\"\ndoctor=\".claude/.duro-session.cache.doctor\"\npin=\"#duro-hook-pin 4.4.0\"\n\n# npx resolution dominates session start; the catalog only changes on upgrade.\n# Refetch when it is missing, a week old, fetched with another floor (its\n# first line), or older than this script (a regenerate).\nif [ ! -s \"$cache\" ] || [ \"$(head -n 1 \"$cache\" 2>/dev/null)\" != \"$pin\" ] ||\n [ -n \"$(find \"$cache\" -mtime +7 2>/dev/null)\" ] || [ \".claude/hooks/duro-catalog.sh\" -nt \"$cache\" ]; then\n # The pin is written with the payload, so a failed fetch (or one that\n # printed nothing) leaves the old cache as it was and never a pin-only file.\n if { echo \"$pin\"; npx -y @duro-app/cli@^4.4.0 hook session-start; } >\"$cache.tmp\" 2>/dev/null &&\n [ -n \"$(tail -n +2 \"$cache.tmp\")\" ]; then\n mv \"$cache.tmp\" \"$cache\"\n else\n rm -f \"$cache.tmp\"\n if [ -s \"$cache\" ] && [ \"$(head -n 1 \"$cache\")\" != \"$pin\" ]; then\n echo \"duro: the catalog below is from an older CLI and could not be refreshed; run: npx -y @duro-app/cli@^4.4.0 hook install\" >&2\n fi\n fi\nfi\n\n# Offline with no cache yet: stay silent rather than fail the session.\nif [ -s \"$cache\" ]; then\n # The pin line is bookkeeping, not catalog: skip it, and only it.\n case \"$(head -n 1 \"$cache\")\" in\n '#duro-hook-pin '*) tail -n +2 \"$cache\" ;;\n *) cat \"$cache\" ;;\n esac\n if [ -s \"$notes\" ]; then\n echo\n cat \"$notes\"\n fi\nfi\n\n# duro doctor reads build config, entries and stylesheets. Re-run it when one\n# of those changes rather than on a timer, so an edit shows up next session\n# and a healthy repo only pays for npx after touching its config.\nkey=$(find . -maxdepth 4 \\( -name node_modules -o -name .git -o -name dist -o -name build \\) -prune -o \\\n -type f \\( -name package.json -o -name pnpm-workspace.yaml -o -name '*.css' -o -name 'vite*' \\\n -o -name 'babel*' -o -name '.babelrc*' -o -name '*.babel.*' -o -name 'postcss*' \\\n -o -name 'root.tsx' -o -name 'main.tsx' -o -name 'index.tsx' \\) -exec cksum {} + 2>/dev/null |\n sort | cksum)\n# The floor is part of the key: a new floor reruns the doctor.\nkey=\"4.4.0 $key\"\nif [ \"$key\" != \"$(cat \"$doctor.key\" 2>/dev/null)\" ]; then\n if npx -y @duro-app/cli@^4.4.0 doctor --session >\"$doctor.tmp\" 2>/dev/null; then\n mv \"$doctor.tmp\" \"$doctor\"\n echo \"$key\" >\"$doctor.key\"\n else\n rm -f \"$doctor.tmp\"\n fi\nfi\nif [ -s \"$doctor\" ]; then\n echo\n cat \"$doctor\"\nfi\n";
38
47
  /**
39
48
  * The UserPromptSubmit hook: a plan-mode nudge toward fresh mockup
40
49
  * directions (fleet decision `handoff.fresh-directions-trigger`), so a plan
@@ -1 +1 @@
1
- {"version":3,"file":"hook-script.d.ts","sourceRoot":"","sources":["../src/hook-script.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,kCAAkC,CAAA;AAC/D,eAAO,MAAM,kBAAkB,0BAA0B,CAAA;AACzD,eAAO,MAAM,eAAe,gCAAgC,CAAA;AAC5D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,uCAA8B,CAAA;AACjE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,iCAAwB,CAAA;AACtD,gFAAgF;AAChF,eAAO,MAAM,eAAe,+BAA+B,CAAA;AAE3D,mEAAmE;AACnE,eAAO,MAAM,YAAY,qCAA2B,CAAA;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC,eAAO,MAAM,WAAW,qtEAsDvB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,+BAA+B,CAAA;AAEjE,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,kCAAgC,CAAA;AAE9D,uEAAuE;AACvE,eAAO,MAAM,UAAU,ieAKtB,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,2oCAiB5B,CAAA;AAED,eAAO,MAAM,eAAe,kFAA0E,CAAA"}
1
+ {"version":3,"file":"hook-script.d.ts","sourceRoot":"","sources":["../src/hook-script.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,kCAAkC,CAAA;AAC/D,eAAO,MAAM,kBAAkB,0BAA0B,CAAA;AACzD,eAAO,MAAM,eAAe,gCAAgC,CAAA;AAC5D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,uCAA8B,CAAA;AACjE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,iCAAwB,CAAA;AACtD,gFAAgF;AAChF,eAAO,MAAM,eAAe,+BAA+B,CAAA;AAE3D,mEAAmE;AACnE,eAAO,MAAM,YAAY,qCAA2B,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC;;;GAGG;AACH,eAAO,MAAM,aAAa,yBAAmC,CAAA;AAE7D,eAAO,MAAM,WAAW,+qGAsEvB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,+BAA+B,CAAA;AAEjE,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,kCAAgC,CAAA;AAE9D,uEAAuE;AACvE,eAAO,MAAM,UAAU,ieAKtB,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,2oCAiB5B,CAAA;AAED,eAAO,MAAM,eAAe,kFAA0E,CAAA"}
@@ -32,8 +32,17 @@ export const HOOK_COMMAND = `sh ${HOOK_SCRIPT_PATH}`;
32
32
  * sees what the payload gained (1.2.0 pinned every repo out of 2.x).
33
33
  * 3.0.0: the SCREENS section and the /duro-mockup pointer.
34
34
  * 3.4.0: duro doctor --session.
35
+ * 4.4.0: the first CLI whose payload matches @duro-app/ui 4 — a ^3 floor
36
+ * never reaches 4.x, so 4.x consumers were served 3.x docs. The
37
+ * release refuses a tag whose major differs from this floor's
38
+ * (scripts/check-pin-major.mjs).
35
39
  */
36
- export const HOOK_MIN_CLI = '3.4.0';
40
+ export const HOOK_MIN_CLI = '4.4.0';
41
+ /**
42
+ * First line of the catalog cache: the floor it was fetched with. A cache
43
+ * from another floor is refetched, and this line is never printed.
44
+ */
45
+ export const HOOK_PIN_LINE = `#duro-hook-pin ${HOOK_MIN_CLI}`;
37
46
  export const HOOK_SCRIPT = `#!/bin/sh
38
47
  # Duro design-system session bootstrap — GENERATED by \`duro hook install\`.
39
48
  # Regenerate: npx -y @duro-app/cli hook install (--check verifies it in CI)
@@ -49,19 +58,33 @@ export const HOOK_SCRIPT = `#!/bin/sh
49
58
  cache="${HOOK_CACHE_PATH}"
50
59
  notes="${HOOK_NOTES_PATH}"
51
60
  doctor="${HOOK_DOCTOR_CACHE_PATH}"
61
+ pin="${HOOK_PIN_LINE}"
52
62
 
53
63
  # npx resolution dominates session start; the catalog only changes on upgrade.
54
- if [ ! -s "$cache" ] || [ -n "$(find "$cache" -mtime +7 2>/dev/null)" ]; then
55
- if npx -y @duro-app/cli@^${HOOK_MIN_CLI} hook session-start >"$cache.tmp" 2>/dev/null; then
64
+ # Refetch when it is missing, a week old, fetched with another floor (its
65
+ # first line), or older than this script (a regenerate).
66
+ if [ ! -s "$cache" ] || [ "$(head -n 1 "$cache" 2>/dev/null)" != "$pin" ] ||
67
+ [ -n "$(find "$cache" -mtime +7 2>/dev/null)" ] || [ "${HOOK_SCRIPT_PATH}" -nt "$cache" ]; then
68
+ # The pin is written with the payload, so a failed fetch (or one that
69
+ # printed nothing) leaves the old cache as it was and never a pin-only file.
70
+ if { echo "$pin"; npx -y @duro-app/cli@^${HOOK_MIN_CLI} hook session-start; } >"$cache.tmp" 2>/dev/null &&
71
+ [ -n "$(tail -n +2 "$cache.tmp")" ]; then
56
72
  mv "$cache.tmp" "$cache"
57
73
  else
58
74
  rm -f "$cache.tmp"
75
+ if [ -s "$cache" ] && [ "$(head -n 1 "$cache")" != "$pin" ]; then
76
+ echo "duro: the catalog below is from an older CLI and could not be refreshed; run: npx -y @duro-app/cli@^${HOOK_MIN_CLI} hook install" >&2
77
+ fi
59
78
  fi
60
79
  fi
61
80
 
62
81
  # Offline with no cache yet: stay silent rather than fail the session.
63
82
  if [ -s "$cache" ]; then
64
- cat "$cache"
83
+ # The pin line is bookkeeping, not catalog: skip it, and only it.
84
+ case "$(head -n 1 "$cache")" in
85
+ '#duro-hook-pin '*) tail -n +2 "$cache" ;;
86
+ *) cat "$cache" ;;
87
+ esac
65
88
  if [ -s "$notes" ]; then
66
89
  echo
67
90
  cat "$notes"
@@ -76,6 +99,8 @@ key=$(find . -maxdepth 4 \\( -name node_modules -o -name .git -o -name dist -o -
76
99
  -o -name 'babel*' -o -name '.babelrc*' -o -name '*.babel.*' -o -name 'postcss*' \\
77
100
  -o -name 'root.tsx' -o -name 'main.tsx' -o -name 'index.tsx' \\) -exec cksum {} + 2>/dev/null |
78
101
  sort | cksum)
102
+ # The floor is part of the key: a new floor reruns the doctor.
103
+ key="${HOOK_MIN_CLI} $key"
79
104
  if [ "$key" != "$(cat "$doctor.key" 2>/dev/null)" ]; then
80
105
  if npx -y @duro-app/cli@^${HOOK_MIN_CLI} doctor --session >"$doctor.tmp" 2>/dev/null; then
81
106
  mv "$doctor.tmp" "$doctor"
@@ -1 +1 @@
1
- {"version":3,"file":"hook-script.js","sourceRoot":"","sources":["../src/hook-script.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,+BAA+B,CAAA;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAA;AACzD,MAAM,CAAC,MAAM,eAAe,GAAG,6BAA6B,CAAA;AAC5D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,eAAe,SAAS,CAAA;AACjE;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,eAAe,GAAG,CAAA;AACtD,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,4BAA4B,CAAA;AAE3D,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,gBAAgB,EAAE,CAAA;AAEpD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAA;AAEnC,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;IAWvB,eAAe;SACV,eAAe;SACf,eAAe;UACd,sBAAsB;;;;6BAIH,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;6BAyBZ,YAAY;;;;;;;;;;;CAWxC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAA;AAEjE,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,qBAAqB,EAAE,CAAA;AAE9D,uEAAuE;AACvE,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;CAKzB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;EAc9B,UAAU;;;CAGX,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,kDAAkD,iBAAiB,IAAI,CAAA"}
1
+ {"version":3,"file":"hook-script.js","sourceRoot":"","sources":["../src/hook-script.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,+BAA+B,CAAA;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAA;AACzD,MAAM,CAAC,MAAM,eAAe,GAAG,6BAA6B,CAAA;AAC5D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,eAAe,SAAS,CAAA;AACjE;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,eAAe,GAAG,CAAA;AACtD,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,4BAA4B,CAAA;AAE3D,mEAAmE;AACnE,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,gBAAgB,EAAE,CAAA;AAEpD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAA;AAEnC;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,kBAAkB,YAAY,EAAE,CAAA;AAE7D,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;IAWvB,eAAe;SACV,eAAe;SACf,eAAe;UACd,sBAAsB;OACzB,aAAa;;;;;;0DAMsC,gBAAgB;;;4CAG9B,YAAY;;;;;;kHAM0D,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BvH,YAAY;;6BAEU,YAAY;;;;;;;;;;;CAWxC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,4BAA4B,CAAA;AAEjE,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,qBAAqB,EAAE,CAAA;AAE9D,uEAAuE;AACvE,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;CAKzB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;EAc9B,UAAU;;;CAGX,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,kDAAkD,iBAAiB,IAAI,CAAA"}
@@ -14,9 +14,11 @@ export declare const SKILL_PATH = ".claude/skills/duro-mockup/SKILL.md";
14
14
  /** Repo-specific caveats, read by the skill and never touched by install. */
15
15
  export declare const SKILL_NOTES_PATH = ".claude/duro-mockup.local.md";
16
16
  /**
17
- * Floor for the npx pin inside the skill. `duro mockup` landed in 3.0.0. A
18
- * floating pin keeps patch releases from showing up as --check drift.
17
+ * Floor for the npx pin inside the skill. `duro mockup` landed in 3.0.0; it
18
+ * moves with the hook's floor to the release's own major (4.4.0), so a 4.x
19
+ * consumer runs a 4.x CLI. A floating pin keeps patch releases from showing
20
+ * up as --check drift.
19
21
  */
20
- export declare const SKILL_MIN_CLI = "3.0.0";
21
- export declare const SKILL = "---\nname: duro-mockup\ndescription: Take a new screen from mockup directions to an implemented, verified PR without leaving the Duro design system. Use whenever the user asks for a screen, a page, a UI proposal, mockups, or \"show me options\" in a repo that consumes @duro-app/ui \u2014 and when planning (plan mode) a new screen or a material layout, hierarchy or interaction change there.\n---\n\n# duro-mockup\n\nGENERATED by `duro skill install` \u2014 do not hand-edit. Regenerate:\n`npx -y @duro-app/cli@^3.0.0 skill install` (`--check` verifies it in CI). Repo-specific notes\nlive in `.claude/duro-mockup.local.md`; read it if it exists.\n\nStanding rule (fleet decision `ui.mockup-handoff`): a mockup is written in\nthe design system's own vocabulary, and the direction the user picks is a\ncontract. What binds: layout, hierarchy, states, the `data-duro` component\nmap and the token names. What does not bind: the artboard's pixel geometry.\n\n**This overrides the `design` skill's step 0.** That skill says to lift\nresolved values from the real stylesheets. Here you never write a resolved\nvalue: every colour, spacing, radius, font, shadow and duration is a\n`var(--duro-*)` from the seeded token block, and every element a person can\nact on carries `data-duro=\"<Component>\"`. The rest of the `design` skill\n(artboard shape, `seed-canvas.mjs`, the canvas Artifact) still applies.\n\n## Args\n\n- `propose <screen>` \u2014 seed 2\u20134 direction artboards, check them, publish the canvas.\n- `implement <screen> <direction>` \u2014 build the picked direction from its component map.\n- `prove <route>` \u2014 screenshot the built route and assemble the PR section.\n- no args \u2014 infer the stage from what exists under `docs/mockups/<screen>/`.\n\n## propose\n\n```bash\nnpx -y @duro-app/cli@^3.0.0 mockup seed --out docs/mockups/<screen> --name Main # direction A\nnpx -y @duro-app/cli@^3.0.0 mockup seed --out docs/mockups/<screen> --name <Direction> # B, C, \u2026\n```\n\n- `<screen>` is kebab-case (`approval-gates`). Artboards are committed with\n the PR \u2014 never leave them in a scratch directory; a session scratchpad is\n garbage-collected and the canvas URL would be the only surviving artefact.\n- Author each artboard inside the seeded `<style>` and root div. Colours,\n spacing, radii, type, shadows and motion come only from the token block;\n widths, heights and positions are yours. Every button, row, chip, tab,\n input, link, table and dialog \u2014 whatever element it is drawn as \u2014 carries\n `data-duro=\"<Component>\"` or `data-duro=\"<Component>.<Part>\"`, using names\n from `npx -y @duro-app/cli@^3.0.0 list components`. Notes for the implementer go after the\n name: `data-duro=\"Button variant=primary\"`.\n- Start from a recipe when one fits the screen shape (`npx -y @duro-app/cli@^3.0.0 list recipes`\n \u2014 admin-detail-page, split-pane, page-with-sidenav, settings-page, \u2026) and\n say so in the artboard's header comment; a hand-rolled layout needs a reason.\n- Gate before publishing:\n\n```bash\nnpx -y @duro-app/cli@^3.0.0 mockup check docs/mockups/<screen>/*.dc.html\n```\n\n Exit 1 lists `file:line rule: message`. Fix every finding; do not publish\n an artboard that fails. Then seed the canvas with the `design` skill's\n helper (it lives under the current Claude Code version's bundled-skills\n directory \u2014 locate it, never hardcode the path) and publish the Artifact.\n- Ask the user to pick a direction. Record the answer as\n `<canvas URL> \u00B7 direction <X>` \u2014 it goes in the PR.\n- If the user edits an artboard in the canvas editor, its property panel\n writes literals back over `var()` by design. Re-extract and run\n `mockup check` again, but treat those findings as advisory: the authored\n file in `docs/mockups/` is the contract, the canvas is the review surface.\n\n## implement\n\n- Read the picked artboard. Its `data-duro` attributes ARE the component\n list: for each one, `npx -y @duro-app/cli@^3.0.0 <Component>` (or the `duro_ds_lookup` MCP\n tool) gives props and usage. Do not substitute a hand-rolled element for a\n named component; if a named component does not fit, say so and propose the\n design-system change rather than working around it.\n- Tokens map one to one: `var(--duro-spacing-md)` is `spacing.md` from\n `@duro-app/tokens/tokens/spacing.css`, `--duro-color-bg-card` is\n `colors.bgCard`, `--duro-radius-sm` is `radii.sm`, `--duro-typography-*`\n and `--duro-type-scale-*` are `typography`/`typeScale`, `--duro-shadow-*`\n is `shadows`, `--duro-duration-*`/`--duro-easing-*` are `duration`/`easing`.\n A media query on `768px` is `breakpoints.md` from\n `@duro-app/tokens/tokens/breakpoints.css`. The lint config refuses the raw\n value, so write the token the first time.\n- Layout comes from a recipe before a grid: `npx -y @duro-app/cli@^3.0.0 <recipe> --source-only`.\n- Geometry in the artboard is a sketch. Match its hierarchy and states, not\n its pixel widths.\n\n## prove\n\n- Run the app, load the route in a real browser (Chrome MCP), and take a\n screenshot at the artboard's width (1120px) in the artboard's theme.\n- The PR body carries, under `## Mockup`: the canvas URL and direction, the\n path `docs/mockups/<screen>/`, and the artboard next to the live screenshot.\n A PR that touches no screen writes `Mockup: none` instead. CI checks for\n one or the other.\n- If the screenshot and the artboard disagree on hierarchy or states, that is\n a finding to fix, not a caption to write.\n";
22
+ export declare const SKILL_MIN_CLI = "4.4.0";
23
+ export declare const SKILL = "---\nname: duro-mockup\ndescription: Take a new screen from mockup directions to an implemented, verified PR without leaving the Duro design system. Use whenever the user asks for a screen, a page, a UI proposal, mockups, or \"show me options\" in a repo that consumes @duro-app/ui \u2014 and when planning (plan mode) a new screen or a material layout, hierarchy or interaction change there.\n---\n\n# duro-mockup\n\nGENERATED by `duro skill install` \u2014 do not hand-edit. Regenerate:\n`npx -y @duro-app/cli@^4.4.0 skill install` (`--check` verifies it in CI). Repo-specific notes\nlive in `.claude/duro-mockup.local.md`; read it if it exists.\n\nStanding rule (fleet decision `ui.mockup-handoff`): a mockup is written in\nthe design system's own vocabulary, and the direction the user picks is a\ncontract. What binds: layout, hierarchy, states, the `data-duro` component\nmap and the token names. What does not bind: the artboard's pixel geometry.\n\n**This overrides the `design` skill's step 0.** That skill says to lift\nresolved values from the real stylesheets. Here you never write a resolved\nvalue: every colour, spacing, radius, font, shadow and duration is a\n`var(--duro-*)` from the seeded token block, and every element a person can\nact on carries `data-duro=\"<Component>\"`. The rest of the `design` skill\n(artboard shape, `seed-canvas.mjs`, the canvas Artifact) still applies.\n\n## Args\n\n- `propose <screen>` \u2014 seed 2\u20134 direction artboards, check them, publish the canvas.\n- `implement <screen> <direction>` \u2014 build the picked direction from its component map.\n- `prove <route>` \u2014 screenshot the built route and assemble the PR section.\n- no args \u2014 infer the stage from what exists under `docs/mockups/<screen>/`.\n\n## propose\n\n```bash\nnpx -y @duro-app/cli@^4.4.0 mockup seed --out docs/mockups/<screen> --name Main # direction A\nnpx -y @duro-app/cli@^4.4.0 mockup seed --out docs/mockups/<screen> --name <Direction> # B, C, \u2026\n```\n\n- `<screen>` is kebab-case (`approval-gates`). Artboards are committed with\n the PR \u2014 never leave them in a scratch directory; a session scratchpad is\n garbage-collected and the canvas URL would be the only surviving artefact.\n- Author each artboard inside the seeded `<style>` and root div. Colours,\n spacing, radii, type, shadows and motion come only from the token block;\n widths, heights and positions are yours. Every button, row, chip, tab,\n input, link, table and dialog \u2014 whatever element it is drawn as \u2014 carries\n `data-duro=\"<Component>\"` or `data-duro=\"<Component>.<Part>\"`, using names\n from `npx -y @duro-app/cli@^4.4.0 list components`. Notes for the implementer go after the\n name: `data-duro=\"Button variant=primary\"`.\n- Start from a recipe when one fits the screen shape (`npx -y @duro-app/cli@^4.4.0 list recipes`\n \u2014 admin-detail-page, split-pane, page-with-sidenav, settings-page, \u2026) and\n say so in the artboard's header comment; a hand-rolled layout needs a reason.\n- Gate before publishing:\n\n```bash\nnpx -y @duro-app/cli@^4.4.0 mockup check docs/mockups/<screen>/*.dc.html\n```\n\n Exit 1 lists `file:line rule: message`. Fix every finding; do not publish\n an artboard that fails. Then seed the canvas with the `design` skill's\n helper (it lives under the current Claude Code version's bundled-skills\n directory \u2014 locate it, never hardcode the path) and publish the Artifact.\n- Ask the user to pick a direction. Record the answer as\n `<canvas URL> \u00B7 direction <X>` \u2014 it goes in the PR.\n- If the user edits an artboard in the canvas editor, its property panel\n writes literals back over `var()` by design. Re-extract and run\n `mockup check` again, but treat those findings as advisory: the authored\n file in `docs/mockups/` is the contract, the canvas is the review surface.\n\n## implement\n\n- Read the picked artboard. Its `data-duro` attributes ARE the component\n list: for each one, `npx -y @duro-app/cli@^4.4.0 <Component>` (or the `duro_ds_lookup` MCP\n tool) gives props and usage. Do not substitute a hand-rolled element for a\n named component; if a named component does not fit, say so and propose the\n design-system change rather than working around it.\n- Tokens map one to one: `var(--duro-spacing-md)` is `spacing.md` from\n `@duro-app/tokens/tokens/spacing.css`, `--duro-color-bg-card` is\n `colors.bgCard`, `--duro-radius-sm` is `radii.sm`, `--duro-typography-*`\n and `--duro-type-scale-*` are `typography`/`typeScale`, `--duro-shadow-*`\n is `shadows`, `--duro-duration-*`/`--duro-easing-*` are `duration`/`easing`.\n A media query on `768px` is `breakpoints.md` from\n `@duro-app/tokens/tokens/breakpoints.css`. The lint config refuses the raw\n value, so write the token the first time.\n- Layout comes from a recipe before a grid: `npx -y @duro-app/cli@^4.4.0 <recipe> --source-only`.\n- Geometry in the artboard is a sketch. Match its hierarchy and states, not\n its pixel widths.\n\n## prove\n\n- Run the app, load the route in a real browser (Chrome MCP), and take a\n screenshot at the artboard's width (1120px) in the artboard's theme.\n- The PR body carries, under `## Mockup`: the canvas URL and direction, the\n path `docs/mockups/<screen>/`, and the artboard next to the live screenshot.\n A PR that touches no screen writes `Mockup: none` instead. CI checks for\n one or the other.\n- If the screenshot and the artboard disagree on hierarchy or states, that is\n a finding to fix, not a caption to write.\n";
22
24
  //# sourceMappingURL=skill-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill-template.d.ts","sourceRoot":"","sources":["../src/skill-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,SAAS,+BAA+B,CAAA;AACrD,eAAO,MAAM,UAAU,wCAA0B,CAAA;AACjD,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,iCAAiC,CAAA;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,UAAU,CAAA;AAIpC,eAAO,MAAM,KAAK,88KAgGjB,CAAA"}
1
+ {"version":3,"file":"skill-template.d.ts","sourceRoot":"","sources":["../src/skill-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,SAAS,+BAA+B,CAAA;AACrD,eAAO,MAAM,UAAU,wCAA0B,CAAA;AACjD,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,iCAAiC,CAAA;AAE9D;;;;;GAKG;AACH,eAAO,MAAM,aAAa,UAAU,CAAA;AAIpC,eAAO,MAAM,KAAK,88KAgGjB,CAAA"}
@@ -14,10 +14,12 @@ export const SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
14
14
  /** Repo-specific caveats, read by the skill and never touched by install. */
15
15
  export const SKILL_NOTES_PATH = '.claude/duro-mockup.local.md';
16
16
  /**
17
- * Floor for the npx pin inside the skill. `duro mockup` landed in 3.0.0. A
18
- * floating pin keeps patch releases from showing up as --check drift.
17
+ * Floor for the npx pin inside the skill. `duro mockup` landed in 3.0.0; it
18
+ * moves with the hook's floor to the release's own major (4.4.0), so a 4.x
19
+ * consumer runs a 4.x CLI. A floating pin keeps patch releases from showing
20
+ * up as --check drift.
19
21
  */
20
- export const SKILL_MIN_CLI = '3.0.0';
22
+ export const SKILL_MIN_CLI = '4.4.0';
21
23
  const DURO = `npx -y @duro-app/cli@^${SKILL_MIN_CLI}`;
22
24
  export const SKILL = `---
23
25
  name: duro-mockup
@@ -1 +1 @@
1
- {"version":3,"file":"skill-template.js","sourceRoot":"","sources":["../src/skill-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4CAA4C;AAC5C,MAAM,CAAC,MAAM,SAAS,GAAG,4BAA4B,CAAA;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,SAAS,WAAW,CAAA;AACjD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,8BAA8B,CAAA;AAE9D;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,MAAM,IAAI,GAAG,yBAAyB,aAAa,EAAE,CAAA;AAErD,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;IAQjB,IAAI;YACI,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAwB1B,IAAI;EACJ,IAAI;;;;;;;;;;;WAWK,IAAI;;0DAE2C,IAAI;;;;;;EAM5D,IAAI;;;;;;;;;;;;;;;;;0BAiBoB,IAAI;;;;;;;;;;;;gDAYkB,IAAI;;;;;;;;;;;;;;CAcnD,CAAA"}
1
+ {"version":3,"file":"skill-template.js","sourceRoot":"","sources":["../src/skill-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4CAA4C;AAC5C,MAAM,CAAC,MAAM,SAAS,GAAG,4BAA4B,CAAA;AACrD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,SAAS,WAAW,CAAA;AACjD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,8BAA8B,CAAA;AAE9D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,MAAM,IAAI,GAAG,yBAAyB,aAAa,EAAE,CAAA;AAErD,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;IAQjB,IAAI;YACI,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;EAwB1B,IAAI;EACJ,IAAI;;;;;;;;;;;WAWK,IAAI;;0DAE2C,IAAI;;;;;;EAM5D,IAAI;;;;;;;;;;;;;;;;;0BAiBoB,IAAI;;;;;;;;;;;;gDAYkB,IAAI;;;;;;;;;;;;;;CAcnD,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/cli",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Machine-queryable docs for the Duro design system: components, props, recipes, tokens and rules as CLI output, JSON, or an MCP server.",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/sdk": ">=1.0.0",
48
- "@duro-app/ui": "^4.2.0"
48
+ "@duro-app/ui": "^4.4.0"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@duro-app/ui": {
@@ -60,9 +60,9 @@
60
60
  "@types/node": "^24.0.0",
61
61
  "ts-morph": "^26.0.0",
62
62
  "typescript": "^5.7.0",
63
- "@duro-app/tokens": "^4.2.0",
64
- "@duro-app/ui": "^4.2.0",
65
- "@duro-app/eslint-plugin": "^4.2.0"
63
+ "@duro-app/eslint-plugin": "^4.4.0",
64
+ "@duro-app/tokens": "^4.4.0",
65
+ "@duro-app/ui": "^4.4.0"
66
66
  },
67
67
  "scripts": {
68
68
  "registry": "node scripts/build-registry.mjs",
package/registry.json CHANGED
@@ -11,6 +11,11 @@
11
11
  "component": "Table",
12
12
  "kind": "composition",
13
13
  "relationship": "Common pattern: Table checkboxes + ActionBar for bulk ops"
14
+ },
15
+ {
16
+ "component": "DetailPanel",
17
+ "kind": "composition",
18
+ "relationship": "With a DetailPanel docked at the end edge, pass the panel's width as insetInlineEnd"
14
19
  }
15
20
  ],
16
21
  "whenNotToUse": [
@@ -20,7 +25,8 @@
20
25
  "whenToUse": [
21
26
  "Bulk actions on table selections (delete, revoke, export)",
22
27
  "Multi-select workflows where actions apply to all selected items",
23
- "Selection-scoped verbs on a canvas/editor surface (set `bottomOffset` to clear any bottom chrome such as zoom controls)"
28
+ "Selection-scoped verbs on a canvas/editor surface (set `bottomOffset` to clear any bottom chrome such as zoom controls)",
29
+ "A docked side panel (DetailPanel) is open: pass its width as `insetInlineEnd` so the bar centres beside it, not under it (follows the writing direction: the left edge in right-to-left)"
24
30
  ]
25
31
  },
26
32
  "name": "ActionBar",
@@ -61,6 +67,12 @@
61
67
  "required": false,
62
68
  "type": "SpacingToken | number"
63
69
  },
70
+ {
71
+ "description": "Width in px of chrome docked at the window's inline-end edge (right in left-to-right, left in right-to-left), such as an open DetailPanel: the bar centres in what is left and keeps 24px clear of it. 0 or omitted centres on the window.",
72
+ "name": "insetInlineEnd",
73
+ "required": false,
74
+ "type": "number"
75
+ },
64
76
  {
65
77
  "description": "Action buttons to display.",
66
78
  "name": "children",
@@ -81,6 +93,11 @@
81
93
  "component": "Table",
82
94
  "kind": "composition",
83
95
  "relationship": "Common pattern: Table checkboxes + ActionBar for bulk ops"
96
+ },
97
+ {
98
+ "component": "DetailPanel",
99
+ "kind": "composition",
100
+ "relationship": "With a DetailPanel docked at the end edge, pass the panel's width as insetInlineEnd"
84
101
  }
85
102
  ],
86
103
  "whenNotToUse": [
@@ -90,7 +107,8 @@
90
107
  "whenToUse": [
91
108
  "Bulk actions on table selections (delete, revoke, export)",
92
109
  "Multi-select workflows where actions apply to all selected items",
93
- "Selection-scoped verbs on a canvas/editor surface (set `bottomOffset` to clear any bottom chrome such as zoom controls)"
110
+ "Selection-scoped verbs on a canvas/editor surface (set `bottomOffset` to clear any bottom chrome such as zoom controls)",
111
+ "A docked side panel (DetailPanel) is open: pass its width as `insetInlineEnd` so the bar centres beside it, not under it (follows the writing direction: the left edge in right-to-left)"
94
112
  ]
95
113
  },
96
114
  "name": "ActionBarProvider",
@@ -1329,11 +1347,21 @@
1329
1347
  "description": "Non-modal side panel for right-side inspection. Click a table row to show details, click another to update in place. Built on Panel internally. In-flow (flex child), not fixed/modal — content behind remains interactive.",
1330
1348
  "example": "<html.div style={layoutStyles.container}>\n <html.div style={layoutStyles.main}>\n <Table.Root columns={3}>...</Table.Root>\n </html.div>\n <DetailPanel.Root open={selectedId != null} onOpenChange={handleOpenChange}>\n <DetailPanel.Content size=\"sm\" label=\"User details\">\n <DetailPanel.Header>\n <DetailPanel.Title>{user.name}</DetailPanel.Title>\n <DetailPanel.Close />\n </DetailPanel.Header>\n <DetailPanel.Body>\n <Stack gap=\"md\">...</Stack>\n </DetailPanel.Body>\n </DetailPanel.Content>\n </DetailPanel.Root>\n</html.div>",
1331
1349
  "relatedTo": [
1350
+ {
1351
+ "component": "ActionBar",
1352
+ "kind": "composition",
1353
+ "relationship": "An ActionBar shown while the panel is open takes the panel's width as insetInlineEnd"
1354
+ },
1332
1355
  {
1333
1356
  "component": "Drawer",
1334
1357
  "kind": "contrast",
1335
1358
  "relationship": "Drawer is modal with backdrop; DetailPanel is non-modal and in-flow"
1336
1359
  },
1360
+ {
1361
+ "component": "Popover",
1362
+ "kind": "contrast",
1363
+ "relationship": "Popover is a small anchored overlay for a few controls; DetailPanel is an in-flow panel for long editing"
1364
+ },
1337
1365
  {
1338
1366
  "component": "Table",
1339
1367
  "kind": "composition",
@@ -1579,6 +1607,11 @@
1579
1607
  "component": "DetailPanel",
1580
1608
  "kind": "contrast",
1581
1609
  "relationship": "DetailPanel is non-modal and in-flow; Dialog is modal with backdrop"
1610
+ },
1611
+ {
1612
+ "component": "Popover",
1613
+ "kind": "contrast",
1614
+ "relationship": "Popover is a small non-modal card anchored to an element; Dialog is a modal decision"
1582
1615
  }
1583
1616
  ],
1584
1617
  "whenNotToUse": [
@@ -1932,6 +1965,11 @@
1932
1965
  "component": "DetailPanel",
1933
1966
  "kind": "contrast",
1934
1967
  "relationship": "DetailPanel is non-modal and in-flow; Drawer is modal with backdrop"
1968
+ },
1969
+ {
1970
+ "component": "Popover",
1971
+ "kind": "contrast",
1972
+ "relationship": "Popover is a small card anchored to an element; Drawer takes its place at phone width"
1935
1973
  }
1936
1974
  ],
1937
1975
  "whenNotToUse": [
@@ -3343,7 +3381,8 @@
3343
3381
  ],
3344
3382
  "whenNotToUse": [
3345
3383
  "Picking a value — use Select",
3346
- "Primary navigation — use SideNav or Tabs"
3384
+ "Primary navigation — use SideNav or Tabs",
3385
+ "A small form tied to the trigger — use Popover"
3347
3386
  ],
3348
3387
  "whenToUse": [
3349
3388
  "Context menu or \"more actions\" dropdown",
@@ -3686,6 +3725,167 @@
3686
3725
  "props": null,
3687
3726
  "sourcePath": "packages/ui/src/components/Panel/Panel.tsx"
3688
3727
  },
3728
+ "Popover": {
3729
+ "importPath": "@duro-app/ui",
3730
+ "kind": "compound",
3731
+ "meta": {
3732
+ "anatomy": {
3733
+ "optional": [
3734
+ "Trigger",
3735
+ "Close"
3736
+ ],
3737
+ "required": [
3738
+ "Root",
3739
+ "Popup"
3740
+ ]
3741
+ },
3742
+ "description": "Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves.",
3743
+ "example": "const ref = useRef<PopoverHandle>(null)\nconst anchor = () => new DOMRect(point.x, point.y, 0, 0)\n\n<Popover.Root ref={ref} open={open} onOpenChange={setOpen} anchor={anchor} side=\"bottom\" align=\"start\">\n <Popover.Popup label=\"Edit label\">\n <Field.Root>\n <Field.Label>Label</Field.Label>\n <Input value={label} onChange={(e) => setLabel(e.target.value)} />\n </Field.Root>\n <Popover.Close>Done</Popover.Close>\n </Popover.Popup>\n</Popover.Root>\n// after the anchor moves: ref.current?.reposition()",
3744
+ "relatedTo": [
3745
+ {
3746
+ "component": "Menu",
3747
+ "kind": "contrast",
3748
+ "relationship": "Menu opens a list of actions; Popover holds small forms and controls"
3749
+ },
3750
+ {
3751
+ "component": "Tooltip",
3752
+ "kind": "contrast",
3753
+ "relationship": "Tooltip shows hover-only text; Popover holds interactive content and stays open"
3754
+ },
3755
+ {
3756
+ "component": "Dialog",
3757
+ "kind": "contrast",
3758
+ "relationship": "Dialog is modal and blocks the page; Popover is non-modal and anchored"
3759
+ },
3760
+ {
3761
+ "component": "DetailPanel",
3762
+ "kind": "contrast",
3763
+ "relationship": "DetailPanel is an in-flow panel for long editing; Popover is a small anchored overlay"
3764
+ },
3765
+ {
3766
+ "component": "Drawer",
3767
+ "kind": "contrast",
3768
+ "relationship": "Drawer is the modal edge panel for phone width; Popover is anchored and non-modal"
3769
+ },
3770
+ {
3771
+ "component": "Field",
3772
+ "kind": "composition",
3773
+ "relationship": "Place Field.Root with its control inside Popover.Popup for a labelled field"
3774
+ }
3775
+ ],
3776
+ "whenNotToUse": [
3777
+ "A list of actions — use Menu",
3778
+ "Hover-only text — use Tooltip",
3779
+ "A blocking decision — use Dialog or ConfirmDialog",
3780
+ "A long editor, or one that follows the selection — use DetailPanel",
3781
+ "Phone width — use Drawer"
3782
+ ],
3783
+ "whenToUse": [
3784
+ "A small form or controls tied to one element that stay open while you work in them",
3785
+ "Anchoring to a spot that is not a button (a canvas point, a text selection) with a virtual anchor"
3786
+ ]
3787
+ },
3788
+ "name": "Popover",
3789
+ "parts": {
3790
+ "Close": {
3791
+ "name": "Close",
3792
+ "props": [
3793
+ {
3794
+ "name": "children",
3795
+ "required": true,
3796
+ "type": "ReactNode"
3797
+ }
3798
+ ],
3799
+ "sourcePath": "packages/ui/src/components/Popover/Popover.tsx"
3800
+ },
3801
+ "Popup": {
3802
+ "name": "Popup",
3803
+ "props": [
3804
+ {
3805
+ "name": "children",
3806
+ "required": true,
3807
+ "type": "ReactNode"
3808
+ },
3809
+ {
3810
+ "description": "Accessible name for the dialog.",
3811
+ "name": "label",
3812
+ "required": true,
3813
+ "type": "string"
3814
+ }
3815
+ ],
3816
+ "sourcePath": "packages/ui/src/components/Popover/Popover.tsx"
3817
+ },
3818
+ "Root": {
3819
+ "name": "Root",
3820
+ "props": [
3821
+ {
3822
+ "name": "children",
3823
+ "required": true,
3824
+ "type": "ReactNode"
3825
+ },
3826
+ {
3827
+ "name": "open",
3828
+ "required": false,
3829
+ "type": "boolean"
3830
+ },
3831
+ {
3832
+ "default": "false",
3833
+ "name": "defaultOpen",
3834
+ "required": false,
3835
+ "type": "boolean"
3836
+ },
3837
+ {
3838
+ "name": "onOpenChange",
3839
+ "required": false,
3840
+ "type": "(open: boolean) => void"
3841
+ },
3842
+ {
3843
+ "default": "'bottom'",
3844
+ "name": "side",
3845
+ "required": false,
3846
+ "type": "PopoverSide"
3847
+ },
3848
+ {
3849
+ "default": "'start'",
3850
+ "name": "align",
3851
+ "required": false,
3852
+ "type": "PopoverAlign"
3853
+ },
3854
+ {
3855
+ "description": "Virtual anchor: a viewport rect. Replaces the Trigger as the anchor.",
3856
+ "name": "anchor",
3857
+ "required": false,
3858
+ "type": "() => DOMRect"
3859
+ },
3860
+ {
3861
+ "description": "Elements (or getters) whose pointerdown does not count as an outside press.",
3862
+ "name": "ignore",
3863
+ "required": false,
3864
+ "type": "ReadonlyArray<IgnoreEntry>"
3865
+ },
3866
+ {
3867
+ "name": "ref",
3868
+ "required": false,
3869
+ "type": "Ref<PopoverHandle>"
3870
+ }
3871
+ ],
3872
+ "sourcePath": "packages/ui/src/components/Popover/Popover.tsx"
3873
+ },
3874
+ "Trigger": {
3875
+ "name": "Trigger",
3876
+ "props": [
3877
+ {
3878
+ "name": "children",
3879
+ "required": true,
3880
+ "type": "ReactNode"
3881
+ }
3882
+ ],
3883
+ "sourcePath": "packages/ui/src/components/Popover/Popover.tsx"
3884
+ }
3885
+ },
3886
+ "props": null,
3887
+ "sourcePath": "packages/ui/src/components/Popover/Popover.tsx"
3888
+ },
3689
3889
  "RadioGroup": {
3690
3890
  "importPath": "@duro-app/ui",
3691
3891
  "kind": "compound",
@@ -5831,7 +6031,7 @@
5831
6031
  "example": "<Tooltip.Root content=\"Delete this item permanently\" placement=\"top\">\n <Tooltip.Trigger>\n <Button variant=\"danger\">Delete</Button>\n </Tooltip.Trigger>\n</Tooltip.Root>",
5832
6032
  "whenNotToUse": [
5833
6033
  "Critical information that must always be visible — use Text or Callout",
5834
- "Interactive content (links, buttons) — tooltips are not interactive"
6034
+ "Interactive content (links, buttons) — tooltips are not interactive — use Popover"
5835
6035
  ],
5836
6036
  "whenToUse": [
5837
6037
  "Providing extra context on hover for icon-only buttons",