feat: support user defined component labels by istance - #1763
Conversation
|
@fasenderos is attempting to deploy a commit to the Puck Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR adds per-instance component labels. Users can edit labels in the outline, action bar, and sidebar. Labels are stored in ChangesComponent label editing
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant InlineLabelEdit
participant PuckStore
participant setComponentLabelAction
participant ComponentData
User->>InlineLabelEdit: Double-click label
InlineLabelEdit->>PuckStore: Read node and current label
User->>InlineLabelEdit: Enter trimmed label
InlineLabelEdit->>PuckStore: Dispatch setComponentLabel
PuckStore->>setComponentLabelAction: Apply label action
setComponentLabelAction->>ComponentData: Store or remove __puck.label
ComponentData-->>PuckStore: Updated component data
PuckStore-->>InlineLabelEdit: Render updated label
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
packages/core/components/SidebarSection/index.tsxOops! Something went wrong! :( ESLint: 9.39.4 YAMLException: Cannot read config file: /packages/eslint-config-custom/index.mjs 7 | ... Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/components/InlineLabelEdit/index.tsx`:
- Around line 105-112: Update the label trigger in InlineLabelEdit’s rendered
span to be keyboard accessible: make it focusable, handle Enter and Space key
presses by calling startEditing, and retain the existing double-click behavior
and label rendering.
In `@packages/core/components/LayerTree/components/layer/index.tsx`:
- Around line 197-199: Update the layer row component around InlineLabelEdit to
provide a keyboard-focusable rename control that triggers editing without
relying solely on double-click, and restructure the JSX so the editing input
renders outside the row selection button. Preserve existing layer selection
behavior while avoiding nested interactive elements.
In `@packages/core/components/SidebarSection/index.tsx`:
- Around line 27-40: Update the heading content in SidebarSection around
selectedItem and InlineLabelEdit so it falls back to the title prop when
selectedItem is absent, represents the root, or lacks an indexed component node.
Preserve InlineLabelEdit for valid editable component selections, rendering
title whenever it returns no editable heading.
In `@packages/core/reducer/actions/set-component-label.ts`:
- Around line 17-20: Normalize action.label in the SetComponentLabel reducer
before comparison and storage: trim surrounding whitespace, convert an empty
result to undefined, and use that normalized value for the default-label check
and custom override. Keep non-empty labels’ normalized text as the stored value.
- Around line 15-17: Update setComponentLabelAction around the newLabel
calculation to normalize incoming labels consistently with getComponentLabel and
InlineLabelEdit: trim surrounding whitespace and treat empty or whitespace-only
values as undefined before storing __puck.label. Preserve the existing
configured-label fallback behavior.
In `@packages/core/types/Data.tsx`:
- Around line 10-14: Update both Render implementations to remove the __puck
metadata from rootProps before spreading those props into config.root.render;
preserve all other root props and keep component, slot, and drop-zone rendering
behavior unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 99f5390a-8277-45f9-b29a-2ced0ecac93e
📒 Files selected for processing (16)
packages/core/components/DraggableComponent/index.tsxpackages/core/components/DropZone/index.tsxpackages/core/components/InlineLabelEdit/index.tsxpackages/core/components/InlineLabelEdit/styles.module.csspackages/core/components/LayerTree/components/layer/index.tsxpackages/core/components/LayerTree/lib/build-layer-tree.tspackages/core/components/Puck/components/Layout/index.tsxpackages/core/components/SidebarSection/index.tsxpackages/core/lib/data/get-component-label.tspackages/core/lib/dictionary.tspackages/core/lib/use-breadcrumbs.tspackages/core/plugins/fields/index.tsxpackages/core/reducer/actions.tsxpackages/core/reducer/actions/set-component-label.tspackages/core/reducer/index.tspackages/core/types/Data.tsx
| return ( | ||
| <span | ||
| onDoubleClick={startEditing} | ||
| title={renameMsg} | ||
| className={getClassName("label")} | ||
| > | ||
| {typeof children === "function" ? children({ label }) : label} | ||
| </span> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add keyboard access to label editing.
Line 107 starts editing only from onDoubleClick. The span cannot receive keyboard focus and has no keyboard handler. Keyboard users cannot rename a component from this control. Use a keyboard-accessible trigger and support Enter and Space.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/components/InlineLabelEdit/index.tsx` around lines 105 - 112,
Update the label trigger in InlineLabelEdit’s rendered span to be keyboard
accessible: make it focusable, handle Enter and Space key presses by calling
startEditing, and retain the existing double-click behavior and label rendering.
| <div className={getClassName("name")}> | ||
| <InlineLabelEdit componentId={node.itemId} /> | ||
| </div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 10 '<button|InlineLabelEdit|<input|onDoubleClick' \
packages/core/components/LayerTree/components/layer/index.tsx \
packages/core/components/InlineLabelEdit/index.tsxRepository: puckeditor/puck
Length of output: 12880
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Layer component ---'
sed -n '130,215p' packages/core/components/LayerTree/components/layer/index.tsx
printf '%s\n' '--- InlineLabelEdit component ---'
sed -n '29,125p' packages/core/components/InlineLabelEdit/index.tsx
printf '%s\n' '--- Related usages and tests ---'
rg -n -C 5 'InlineLabelEdit|LayerTree|double.?click|renameMsg' \
packages/core --glob '*.{ts,tsx,js,jsx}' | head -n 500
printf '%s\n' '--- InlineLabelEdit files ---'
git ls-files packages/core/components/InlineLabelEditRepository: puckeditor/puck
Length of output: 39164
Make outline renaming keyboard-accessible and keep the editor outside the selection button.
InlineLabelEdit starts editing only on onDoubleClick, so keyboard-only users cannot rename layers. Its editing state renders an <input> inside the row <button>, which creates invalid interactive nesting. Add a keyboard-focusable rename control and render the input outside the selection button.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/components/LayerTree/components/layer/index.tsx` around lines
197 - 199, Update the layer row component around InlineLabelEdit to provide a
keyboard-focusable rename control that triggers editing without relying solely
on double-click, and restructure the JSX so the editing input renders outside
the row selection button. Preserve existing layer selection behavior while
avoiding nested interactive elements.
| /** Per-instance metadata. The __puck property is removed from all components in the tree when passing data down to the Render component, before the components are actually rendered. This avoids polluting data sent to user pages when doing SSR. */ | ||
| __puck?: { | ||
| /** User-set custom label for this component instance. */ | ||
| label?: string; | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Expect: every render path removes __puck before invoking Render.
rg -n -C 8 '__puck|<Render\b|Render\(' packages/core --glob '*.{ts,tsx}'Repository: puckeditor/puck
Length of output: 22894
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Render-related files ---'
git ls-files packages/core | rg '(^|/)(Render|ServerRender|DropZone|data|Internal|types)(/|\\.)' | head -200
printf '%s\n' '--- Render definitions and call sites ---'
rg -n -C 12 'export (function|const) Render|function Render|<Render\b|renderComponent|Component\.render|propsWithSlots|strip.*puck|__puck' packages/core \
--glob '*.{ts,tsx}' \
--glob '!**/__tests__/**'Repository: puckeditor/puck
Length of output: 40932
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in $(git ls-files packages/core | rg 'ServerRender/index\.tsx$|components/Render|lib/data/.*(render|strip)|components/DropZone/index\.tsx$|types/Internal\.ts'); do
echo "--- $f ---"
wc -l "$f"
ast-grep outline "$f" 2>/dev/null | head -120 || true
doneRepository: puckeditor/puck
Length of output: 3369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- ServerRender implementation ---'
sed -n '1,240p' packages/core/components/ServerRender/index.tsx
echo '--- Render exports and references ---'
rg -n -C 15 'export .*Render|function Render|const Render|<Render\b|Render\(' packages/core \
--glob '*.{ts,tsx}' \
--glob '!**/__tests__/**'Repository: puckeditor/puck
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- Data transformation helpers ---'
rg -n -C 20 'strip|remove|omit|expandNode|flattenNode|propsWithSlots|slots|content' packages/core/lib packages/core/components \
--glob '*.{ts,tsx}' \
--glob '!**/__tests__/**' | head -1200
echo '--- Relevant type definitions ---'
sed -n '1,220p' packages/core/types/Data.tsx
sed -n '1,220p' packages/core/types/Internal.tsRepository: puckeditor/puck
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- package scripts and available test configuration ---'
if [ -f package.json ]; then
node - <<'JS'
const p = require('./package.json');
console.log(JSON.stringify({scripts:p.scripts, workspaces:p.workspaces}, null, 2));
JS
fi
find packages/core -maxdepth 3 -type f \( -name 'package.json' -o -name 'jest.config.*' -o -name 'vitest.config.*' \) -printRepository: puckeditor/puck
Length of output: 1315
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- client Render ---'
sed -n '1,115p' packages/core/components/Render/index.tsx
echo '--- client DropZone render path ---'
sed -n '540,635p' packages/core/components/DropZone/index.tsx
echo '--- server SlotRender ---'
sed -n '1,100p' packages/core/components/SlotRender/server.tsx
echo '--- useSlots ---'
fd -i 'use-slots' packages/core --type f --exec sh -c 'echo "--- $1 ---"; sed -n "1,240p" "$1"' sh {}
echo '--- data types ---'
sed -n '1,180p' packages/core/types/Data.tsxRepository: puckeditor/puck
Length of output: 10632
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- all direct spreads of root and item data ---'
rg -n -C 8 '\.\.\.(rootProps|data\.root|item|props|nodeProps|defaultedNode|renderItem)' \
packages/core/components packages/core/lib \
--glob '*.{ts,tsx}' \
--glob '!**/__tests__/**'
echo '--- exact __puck references outside editor metadata consumers ---'
rg -n -C 6 '__puck' packages/core --glob '*.{ts,tsx}' --glob '!**/__tests__/**'Repository: puckeditor/puck
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
files = [
Path("packages/core/components/Render/index.tsx"),
Path("packages/core/components/ServerRender/index.tsx"),
Path("packages/core/components/DropZone/index.tsx"),
Path("packages/core/components/SlotRender/server.tsx"),
Path("packages/core/lib/use-slots.ts"),
Path("packages/core/types/Data.tsx"),
]
needles = ("__puck", "rootProps", "item.props", "propsWithSlots", "Component.render", "config.root.render")
for path in files:
print(f"--- {path} ---")
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
if any(n in line for n in needles):
lo, hi = max(1, i - 3), min(len(lines), i + 5)
for j in range(lo, hi + 1):
print(f"{j:4}: {lines[j-1]}")
print()
PYRepository: puckeditor/puck
Length of output: 7638
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read-only structural verifier for the documented invariant:
# report every object that is assembled immediately before a user render
# and whether that assembly explicitly omits the top-level __puck key.
python3 - <<'PY'
from pathlib import Path
import re
targets = [
Path("packages/core/components/Render/index.tsx"),
Path("packages/core/components/ServerRender/index.tsx"),
Path("packages/core/components/DropZone/index.tsx"),
Path("packages/core/components/SlotRender/server.tsx"),
]
patterns = [
r"<(?:config\.)?root\.render",
r"<Component\.render",
r"<Component\.render",
]
for path in targets:
lines = path.read_text().splitlines()
print(f"--- {path} ---")
for i, line in enumerate(lines):
if any(re.search(p, line) for p in patterns):
start = max(0, i - 18)
block = "\n".join(lines[start:i+3])
print(f"render at line {i+1}")
print(block)
print("explicit __puck omission:", bool(re.search(r"\b__puck\b", block)))
PYRepository: puckeditor/puck
Length of output: 3378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- root migration and call sites ---'
sed -n '1,110p' packages/core/lib/migrate.ts
rg -n -C 10 'migrate\(|RootDataWithProps|RootDataWithoutProps|root:\s*\{\s*__puck|__puck.*root|root.*__puck' packages/core packages --glob '*.{ts,tsx}' --glob '!**/node_modules/**' --glob '!**/dist/**' | head -1200
echo '--- root data fixtures containing metadata ---'
rg -n -C 8 '__puck|root:\s*\{' packages/core/components/ServerRender packages/core/components/Render packages/core/types packages/core/lib/data --glob '*.{ts,tsx}' | head -1200Repository: puckeditor/puck
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
# Read-only verifier for the two RootData representations used by Render.
# It models only the object spreads in the checked source paths.
def client_or_server_root_props(root):
return root["props"] if "props" in root else root
def rendered_props(root):
root_props = client_or_server_root_props(root)
return {**root_props, "puck": {}, "title": "", "editMode": False, "id": "puck-root"}
cases = {
"new root metadata sibling": {
"props": {"title": "x"},
"__puck": {"label": "root"},
},
"legacy root metadata sibling": {
"title": "x",
"__puck": {"label": "root"},
},
"component metadata outside props": {
"type": "A",
"props": {"id": "a", "title": "x"},
"__puck": {"label": "A"},
},
}
for name, value in cases.items():
if "type" in value:
actual = {**value["props"], "puck": {}}
else:
actual = rendered_props(value)
print(name, "=> __puck passed:", "__puck" in actual, actual)
PYRepository: puckeditor/puck
Length of output: 507
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- migrate implementation ---'
sed -n '1,130p' packages/core/lib/migrate.ts
echo '--- migrate references ---'
rg -n -C 8 '\bmigrate\b' packages/core --glob '*.{ts,tsx}' --glob '!**/__tests__/**'
echo '--- RootData representation references ---'
rg -n -C 6 'RootDataWithProps|RootDataWithoutProps|RootData<' packages/core --glob '*.{ts,tsx}' --glob '!**/__tests__/**'Repository: puckeditor/puck
Length of output: 16426
Strip __puck from root props before rendering.
migrate copies legacy root fields, including __puck, into root.props. Both Render implementations then spread rootProps into config.root.render, so root metadata reaches user components and SSR. Component, slot, and drop-zone metadata does not cross their render boundaries.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/types/Data.tsx` around lines 10 - 14, Update both Render
implementations to remove the __puck metadata from rootProps before spreading
those props into config.root.render; preserve all other root props and keep
component, slot, and drop-zone rendering behavior unchanged.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/components/SidebarSection/index.tsx`:
- Around line 35-40: Reorder the JSX around InlineLabelEdit so Heading remains
the outer element and renders the editable label inside it. Preserve the
existing rank="2" and size="xs" props, and keep InlineLabelEdit responsible only
for the label content and editing behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8e9fc99e-a8c2-4c2e-a587-cd9dc46d1d6c
📒 Files selected for processing (2)
packages/core/components/SidebarSection/index.tsxpackages/core/reducer/actions/set-component-label.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/reducer/actions/set-component-label.ts
|
@FedericoBonel @chrisvxd can you review this PR? Thanks |
|
Hey @fasenderos! Thanks for the contribution, I actually had some thoughts on this PR and how it should be implemented but you got ahead of me! I have some ideas on the UI, but I think that would be easier picked up on my side since it would take some iterations with the rest of the team to get it done. I'll give it a review when I get a moment, thanks again. |
Closes #1752
Description
This PR adds per-instance custom labels for Puck components. Users can now rename individual component instances directly from the UI — the sidebar, the canvas action bar, and the outline panel — with the label persisting in the component data via a
__puck.labelfield.A new
InlineLabelEditcomponent centralizes the label editing logic (read store, resolve label, dispatch updates), and agetComponentLabelutility provides a consistent resolution priority:__puck.label → config.components[type].label → type.toString().Changes made
Core: label storage & resolution
__puck?: { label?: string }toBaseDatainData.tsx— per-instance metadata stored outside props so it never leaks to rendered outputlib/data/get-component-label.tswithgetComponentLabel(item, config, fallback)— resolves label with priority__puck.label → component config label → type string— andgetNodeLabel(node, config, fallback)for the store index node formatsetComponentLabelreducer action — stores the custom label in__puck.label, and auto-strips it when the label matches the type default or is emptyNew shared component:
InlineLabelEditcomponentIdprop and resolves the label internally withgetComponentLabelsetComponentLabelautomatically on savechildren({ label })for custom display; defaults to plain text when omittedUI integration
SidebarSection— when a component is selected, the breadcrumb heading title becomes editable viaInlineLabelEditDraggableComponent(DefaultActionBar) — the action bar label usesInlineLabelEditwith a render-prop for ActionBar.LabelLayerTree/layer— the outline panel shows the editable label viaInlineLabelEditCurrentTitlenow respects__puck.labelHow to test
Note
Tests and documentation will be added when you think the implementation is ready to be approved.
Summary by CodeRabbit