From e9c335fa196063a2fc637d145626d5228f10e306 Mon Sep 17 00:00:00 2001 From: Zach Hawtof Date: Sun, 17 May 2026 11:14:30 -0400 Subject: [PATCH 1/2] fix(input): avoid duplicating placeholder text in editor and preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every input variant factory in `default-blocks.ts` shipped with a canned `placeholder.text` (e.g. "Enter some text", "name@example.com"). When the user opened the Edit Input panel, that same string appeared twice on screen at once — once as the value of the editor's Placeholder field, and again as the placeholder overlay slack-blocks-to-jsx renders inside the preview textarea — which read as a bug. Drop the placeholder defaults from every input variant in both `defaultPalette` and `legacyInputVariants`, and instead surface a per-element-type example as the editor Input's own `placeholder=` attribute via a new `example` prop on the shared `PlaceholderField`. This matches how every other editor field already gives a hint (`placeholder="e.g. Email address"`, etc.) and how Slack's own Block Kit Builder behaves out of the box. Section/Actions accessory placeholders are left as-is because those editors don't expose a Placeholder field, so they can't manifest the duplication. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/editors/input-editor.tsx | 50 +++++--- src/lib/default-blocks.ts | 164 +++--------------------- 2 files changed, 53 insertions(+), 161 deletions(-) diff --git a/src/components/editors/input-editor.tsx b/src/components/editors/input-editor.tsx index 4aee407..774dfe6 100644 --- a/src/components/editors/input-editor.tsx +++ b/src/components/editors/input-editor.tsx @@ -199,7 +199,11 @@ function PlainTextInputEditor({ return ( <> - + - + - + - + - + - + - + onChange({ ...element, options: next })} @@ -502,7 +506,7 @@ function MultiStaticSelectEditor({ return ( <> - + onChange({ ...element, options: next })} @@ -523,7 +527,7 @@ function UsersSelectEditor({ element, onChange }: { element: UsersSelect; onChan return ( <> - + - + ); @@ -576,7 +580,7 @@ function ChannelsSelectEditor({ return ( <> - + - + ); @@ -634,7 +638,7 @@ function ConversationsSelectEditor({ return ( <> - + - + - + - + - +

Edit the initial rich-text value via the View JSON drawer.

@@ -934,22 +938,32 @@ function ActionIdField({ /** * Shared placeholder field used by every element that supports * `placeholder` (i.e. anything implementing `Placeholdable`). + * + * `example` is shown as the input's own `placeholder` attribute so the + * user gets a per-element-type hint about what to type. The element's + * factory in `default-blocks.ts` intentionally ships without a default + * `placeholder.text` so this hint and the live preview don't render the + * same string twice when the editor first opens. + * * @param props - props * @param props.element - the element being edited * @param props.onChange - called with the updated element + * @param props.example - greyed-out hint shown in the editor input (not + * stored on the block); defaults to a generic suggestion * @returns the rendered field */ function PlaceholderField< T extends { placeholder?: { type: 'plain_text'; text: string; emoji?: boolean }; } ->({ element, onChange }: { element: T; onChange: (next: T) => void }) { +>({ element, onChange, example = 'e.g. Type a hint' }: { element: T; onChange: (next: T) => void; example?: string }) { return ( onChange({ ...element, diff --git a/src/lib/default-blocks.ts b/src/lib/default-blocks.ts index ffad40f..89d0377 100644 --- a/src/lib/default-blocks.ts +++ b/src/lib/default-blocks.ts @@ -453,12 +453,7 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Label', emoji: true }, element: { type: 'plain_text_input', - action_id: 'plain_text_input', - placeholder: { - type: 'plain_text', - text: 'Enter some text', - emoji: true - } + action_id: 'plain_text_input' } }) }, @@ -471,12 +466,7 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'plain_text_input', action_id: 'plain_text_input_multiline', - multiline: true, - placeholder: { - type: 'plain_text', - text: 'Enter a longer description', - emoji: true - } + multiline: true } }) }, @@ -488,12 +478,7 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Email address', emoji: true }, element: { type: 'email_text_input', - action_id: 'email_text_input', - placeholder: { - type: 'plain_text', - text: 'name@example.com', - emoji: true - } + action_id: 'email_text_input' } }) }, @@ -505,12 +490,7 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Website', emoji: true }, element: { type: 'url_text_input', - action_id: 'url_text_input', - placeholder: { - type: 'plain_text', - text: 'https://example.com', - emoji: true - } + action_id: 'url_text_input' } }) }, @@ -523,8 +503,7 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'number_input', action_id: 'number_input', - is_decimal_allowed: false, - placeholder: { type: 'plain_text', text: '0', emoji: true } + is_decimal_allowed: false } }) }, @@ -536,12 +515,7 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Pick a date', emoji: true }, element: { type: 'datepicker', - action_id: 'datepicker', - placeholder: { - type: 'plain_text', - text: 'Select a date', - emoji: true - } + action_id: 'datepicker' } }) }, @@ -554,11 +528,6 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'static_select', action_id: 'static_select', - placeholder: { - type: 'plain_text', - text: 'Choose one', - emoji: true - }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -584,12 +553,7 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Description', emoji: true }, element: { type: 'rich_text_input', - action_id: 'rich_text_input', - placeholder: { - type: 'plain_text', - text: 'Type something', - emoji: true - } + action_id: 'rich_text_input' } }) }, @@ -855,12 +819,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Label', emoji: true }, element: { type: 'plain_text_input', - action_id: 'plain_text_input', - placeholder: { - type: 'plain_text', - text: 'Enter some text', - emoji: true - } + action_id: 'plain_text_input' } }) }, @@ -873,12 +832,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'plain_text_input', action_id: 'plain_text_input_multiline', - multiline: true, - placeholder: { - type: 'plain_text', - text: 'Enter a longer description', - emoji: true - } + multiline: true } }) }, @@ -890,12 +844,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Email address', emoji: true }, element: { type: 'email_text_input', - action_id: 'email_text_input', - placeholder: { - type: 'plain_text', - text: 'name@example.com', - emoji: true - } + action_id: 'email_text_input' } }) }, @@ -907,12 +856,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Website', emoji: true }, element: { type: 'url_text_input', - action_id: 'url_text_input', - placeholder: { - type: 'plain_text', - text: 'https://example.com', - emoji: true - } + action_id: 'url_text_input' } }) }, @@ -925,8 +869,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'number_input', action_id: 'number_input', - is_decimal_allowed: false, - placeholder: { type: 'plain_text', text: '0', emoji: true } + is_decimal_allowed: false } }) }, @@ -938,12 +881,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a date', emoji: true }, element: { type: 'datepicker', - action_id: 'datepicker', - placeholder: { - type: 'plain_text', - text: 'Select a date', - emoji: true - } + action_id: 'datepicker' } }) }, @@ -955,12 +893,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a time', emoji: true }, element: { type: 'timepicker', - action_id: 'timepicker', - placeholder: { - type: 'plain_text', - text: 'Select a time', - emoji: true - } + action_id: 'timepicker' } }) }, @@ -989,11 +922,6 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'static_select', action_id: 'static_select', - placeholder: { - type: 'plain_text', - text: 'Choose one', - emoji: true - }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -1020,11 +948,6 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'multi_static_select', action_id: 'multi_static_select', - placeholder: { - type: 'plain_text', - text: 'Choose one or more', - emoji: true - }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -1050,12 +973,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a user', emoji: true }, element: { type: 'users_select', - action_id: 'users_select', - placeholder: { - type: 'plain_text', - text: 'Select a user', - emoji: true - } + action_id: 'users_select' } }) }, @@ -1067,12 +985,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick users', emoji: true }, element: { type: 'multi_users_select', - action_id: 'multi_users_select', - placeholder: { - type: 'plain_text', - text: 'Select users', - emoji: true - } + action_id: 'multi_users_select' } }) }, @@ -1084,12 +997,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a channel', emoji: true }, element: { type: 'channels_select', - action_id: 'channels_select', - placeholder: { - type: 'plain_text', - text: 'Select a channel', - emoji: true - } + action_id: 'channels_select' } }) }, @@ -1101,12 +1009,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick channels', emoji: true }, element: { type: 'multi_channels_select', - action_id: 'multi_channels_select', - placeholder: { - type: 'plain_text', - text: 'Select channels', - emoji: true - } + action_id: 'multi_channels_select' } }) }, @@ -1122,12 +1025,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ }, element: { type: 'conversations_select', - action_id: 'conversations_select', - placeholder: { - type: 'plain_text', - text: 'Select a conversation', - emoji: true - } + action_id: 'conversations_select' } }) }, @@ -1143,12 +1041,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ }, element: { type: 'multi_conversations_select', - action_id: 'multi_conversations_select', - placeholder: { - type: 'plain_text', - text: 'Select conversations', - emoji: true - } + action_id: 'multi_conversations_select' } }) }, @@ -1161,11 +1054,6 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'external_select', action_id: 'external_select', - placeholder: { - type: 'plain_text', - text: 'Choose one', - emoji: true - }, min_query_length: 0 } }) @@ -1179,11 +1067,6 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'multi_external_select', action_id: 'multi_external_select', - placeholder: { - type: 'plain_text', - text: 'Choose one or more', - emoji: true - }, min_query_length: 0 } }) @@ -1248,12 +1131,7 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Description', emoji: true }, element: { type: 'rich_text_input', - action_id: 'rich_text_input', - placeholder: { - type: 'plain_text', - text: 'Type something', - emoji: true - } + action_id: 'rich_text_input' } }) }, From 20dd043e2b9ab474d8481571b884dacc88ec667c Mon Sep 17 00:00:00 2001 From: Zach Hawtof Date: Sun, 17 May 2026 11:24:53 -0400 Subject: [PATCH 2/2] revert(input): restore factory placeholder defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the `default-blocks.ts` half of the previous commit. The factory-supplied `placeholder.text` strings are kept after all: the primary use of block-kitchen is producing JSON the user copies into their Slack app, and shipping a fully-populated placeholder gives customers a working starting point they can edit rather than an empty field they must remember to fill in. What the previous commit perceived as a duplication bug ("same string in the editor field and the preview overlay at once") is just the editor honestly reflecting the model: the form shows the current value, the preview renders it. They're supposed to match. Keeps the `PlaceholderField` `example` prop and per-call-site hints from the previous commit — those now serve as the greyed `e.g. ...` hint that appears only when the user has cleared the placeholder value, so the editor still has a useful affordance when empty without hiding any model state. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/default-blocks.ts | 164 +++++++++++++++++++++++++++++++++----- 1 file changed, 143 insertions(+), 21 deletions(-) diff --git a/src/lib/default-blocks.ts b/src/lib/default-blocks.ts index 89d0377..ffad40f 100644 --- a/src/lib/default-blocks.ts +++ b/src/lib/default-blocks.ts @@ -453,7 +453,12 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Label', emoji: true }, element: { type: 'plain_text_input', - action_id: 'plain_text_input' + action_id: 'plain_text_input', + placeholder: { + type: 'plain_text', + text: 'Enter some text', + emoji: true + } } }) }, @@ -466,7 +471,12 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'plain_text_input', action_id: 'plain_text_input_multiline', - multiline: true + multiline: true, + placeholder: { + type: 'plain_text', + text: 'Enter a longer description', + emoji: true + } } }) }, @@ -478,7 +488,12 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Email address', emoji: true }, element: { type: 'email_text_input', - action_id: 'email_text_input' + action_id: 'email_text_input', + placeholder: { + type: 'plain_text', + text: 'name@example.com', + emoji: true + } } }) }, @@ -490,7 +505,12 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Website', emoji: true }, element: { type: 'url_text_input', - action_id: 'url_text_input' + action_id: 'url_text_input', + placeholder: { + type: 'plain_text', + text: 'https://example.com', + emoji: true + } } }) }, @@ -503,7 +523,8 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'number_input', action_id: 'number_input', - is_decimal_allowed: false + is_decimal_allowed: false, + placeholder: { type: 'plain_text', text: '0', emoji: true } } }) }, @@ -515,7 +536,12 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Pick a date', emoji: true }, element: { type: 'datepicker', - action_id: 'datepicker' + action_id: 'datepicker', + placeholder: { + type: 'plain_text', + text: 'Select a date', + emoji: true + } } }) }, @@ -528,6 +554,11 @@ export const defaultPalette: readonly PaletteSection[] = [ element: { type: 'static_select', action_id: 'static_select', + placeholder: { + type: 'plain_text', + text: 'Choose one', + emoji: true + }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -553,7 +584,12 @@ export const defaultPalette: readonly PaletteSection[] = [ label: { type: 'plain_text', text: 'Description', emoji: true }, element: { type: 'rich_text_input', - action_id: 'rich_text_input' + action_id: 'rich_text_input', + placeholder: { + type: 'plain_text', + text: 'Type something', + emoji: true + } } }) }, @@ -819,7 +855,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Label', emoji: true }, element: { type: 'plain_text_input', - action_id: 'plain_text_input' + action_id: 'plain_text_input', + placeholder: { + type: 'plain_text', + text: 'Enter some text', + emoji: true + } } }) }, @@ -832,7 +873,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'plain_text_input', action_id: 'plain_text_input_multiline', - multiline: true + multiline: true, + placeholder: { + type: 'plain_text', + text: 'Enter a longer description', + emoji: true + } } }) }, @@ -844,7 +890,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Email address', emoji: true }, element: { type: 'email_text_input', - action_id: 'email_text_input' + action_id: 'email_text_input', + placeholder: { + type: 'plain_text', + text: 'name@example.com', + emoji: true + } } }) }, @@ -856,7 +907,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Website', emoji: true }, element: { type: 'url_text_input', - action_id: 'url_text_input' + action_id: 'url_text_input', + placeholder: { + type: 'plain_text', + text: 'https://example.com', + emoji: true + } } }) }, @@ -869,7 +925,8 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'number_input', action_id: 'number_input', - is_decimal_allowed: false + is_decimal_allowed: false, + placeholder: { type: 'plain_text', text: '0', emoji: true } } }) }, @@ -881,7 +938,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a date', emoji: true }, element: { type: 'datepicker', - action_id: 'datepicker' + action_id: 'datepicker', + placeholder: { + type: 'plain_text', + text: 'Select a date', + emoji: true + } } }) }, @@ -893,7 +955,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a time', emoji: true }, element: { type: 'timepicker', - action_id: 'timepicker' + action_id: 'timepicker', + placeholder: { + type: 'plain_text', + text: 'Select a time', + emoji: true + } } }) }, @@ -922,6 +989,11 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'static_select', action_id: 'static_select', + placeholder: { + type: 'plain_text', + text: 'Choose one', + emoji: true + }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -948,6 +1020,11 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'multi_static_select', action_id: 'multi_static_select', + placeholder: { + type: 'plain_text', + text: 'Choose one or more', + emoji: true + }, options: [ { text: { type: 'plain_text', text: 'Option 1', emoji: true }, @@ -973,7 +1050,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a user', emoji: true }, element: { type: 'users_select', - action_id: 'users_select' + action_id: 'users_select', + placeholder: { + type: 'plain_text', + text: 'Select a user', + emoji: true + } } }) }, @@ -985,7 +1067,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick users', emoji: true }, element: { type: 'multi_users_select', - action_id: 'multi_users_select' + action_id: 'multi_users_select', + placeholder: { + type: 'plain_text', + text: 'Select users', + emoji: true + } } }) }, @@ -997,7 +1084,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick a channel', emoji: true }, element: { type: 'channels_select', - action_id: 'channels_select' + action_id: 'channels_select', + placeholder: { + type: 'plain_text', + text: 'Select a channel', + emoji: true + } } }) }, @@ -1009,7 +1101,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Pick channels', emoji: true }, element: { type: 'multi_channels_select', - action_id: 'multi_channels_select' + action_id: 'multi_channels_select', + placeholder: { + type: 'plain_text', + text: 'Select channels', + emoji: true + } } }) }, @@ -1025,7 +1122,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ }, element: { type: 'conversations_select', - action_id: 'conversations_select' + action_id: 'conversations_select', + placeholder: { + type: 'plain_text', + text: 'Select a conversation', + emoji: true + } } }) }, @@ -1041,7 +1143,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ }, element: { type: 'multi_conversations_select', - action_id: 'multi_conversations_select' + action_id: 'multi_conversations_select', + placeholder: { + type: 'plain_text', + text: 'Select conversations', + emoji: true + } } }) }, @@ -1054,6 +1161,11 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'external_select', action_id: 'external_select', + placeholder: { + type: 'plain_text', + text: 'Choose one', + emoji: true + }, min_query_length: 0 } }) @@ -1067,6 +1179,11 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ element: { type: 'multi_external_select', action_id: 'multi_external_select', + placeholder: { + type: 'plain_text', + text: 'Choose one or more', + emoji: true + }, min_query_length: 0 } }) @@ -1131,7 +1248,12 @@ export const legacyInputVariants: readonly PaletteVariant[] = [ label: { type: 'plain_text', text: 'Description', emoji: true }, element: { type: 'rich_text_input', - action_id: 'rich_text_input' + action_id: 'rich_text_input', + placeholder: { + type: 'plain_text', + text: 'Type something', + emoji: true + } } }) },