Skip to content

Commit cfb2bfb

Browse files
authored
Merge pull request #119 from tarosky/feature/chatgpt-style-send-button
AI Overview の送信ボタンを ChatGPT 風の丸い「↑」アイコンボタンに変更
2 parents c9031fa + 0f8e092 commit cfb2bfb

5 files changed

Lines changed: 188 additions & 19 deletions

File tree

hamelp.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ function hamelp_render_search_box( $args = [] ) {
297297
*
298298
* @type string $placeholder Input placeholder text. Default 'Enter your question...'.
299299
* @type string $button_text Submit button text. Default 'Ask AI'.
300+
* @type string $hint_text Helper text shown below the input (empty = hidden). Default 'Shift + Enter で改行'.
300301
* @type bool $show_sources Whether to show source FAQ links. Default true.
301302
* @type string $wrapper_attrs Pre-built wrapper attributes string (used internally by block render).
302303
* }
@@ -313,6 +314,7 @@ function hamelp_render_ai_overview( $args = [] ) {
313314
[
314315
'placeholder' => __( 'Enter your question...', 'hamelp' ),
315316
'button_text' => __( 'Ask AI', 'hamelp' ),
317+
'hint_text' => __( 'Shift + Enter で改行', 'hamelp' ),
316318
'show_sources' => true,
317319
'wrapper_attrs' => '',
318320
]
@@ -352,20 +354,41 @@ function hamelp_render_ai_overview( $args = [] ) {
352354
);
353355
}
354356

357+
// Optional helper text below the field (e.g. "Shift + Enter で改行"). When set,
358+
// it is associated with the textarea via aria-describedby; empty = not rendered.
359+
$hint_text = trim( (string) $args['hint_text'] );
360+
$hint_html = '';
361+
$hint_attr = '';
362+
if ( '' !== $hint_text ) {
363+
$hint_id = wp_unique_id( 'hamelp-ai-overview-' ) . '-hint';
364+
$hint_html = sprintf(
365+
'<p class="hamelp-ai-overview__hint" id="%1$s">%2$s</p>',
366+
esc_attr( $hint_id ),
367+
esc_html( $hint_text )
368+
);
369+
$hint_attr = sprintf( ' aria-describedby="%s"', esc_attr( $hint_id ) );
370+
}
371+
355372
return sprintf(
356373
'<div %1$s>
357374
<div class="hamelp-ai-overview__thread" aria-live="polite"></div>
358375
<form class="hamelp-ai-overview__form">
359376
%2$s
360377
<div class="hamelp-ai-overview__input-row">
361-
<textarea class="hamelp-ai-overview__input" placeholder="%3$s" required></textarea>
362-
<button type="submit" class="hamelp-ai-overview__button">%4$s</button>
378+
<textarea class="hamelp-ai-overview__input" placeholder="%3$s"%5$s required></textarea>
379+
<button type="submit" class="hamelp-ai-overview__button">
380+
<svg class="hamelp-ai-overview__button-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M6 11l6-6 6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
381+
<span class="screen-reader-text">%4$s</span>
382+
</button>
363383
</div>
384+
%6$s
364385
</form>
365386
</div>',
366387
$args['wrapper_attrs'],
367388
$continue_toggle, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_html__ above.
368389
esc_attr( $args['placeholder'] ),
369-
esc_html( $args['button_text'] )
390+
esc_html( $args['button_text'] ),
391+
$hint_attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_attr above.
392+
$hint_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_html/esc_attr above.
370393
);
371394
}

src/blocks/ai-overview/block.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
"type": "string",
1717
"default": "Ask AI"
1818
},
19+
"hintText": {
20+
"type": "string",
21+
"default": "Shift + Enter で改行"
22+
},
1923
"showSources": {
2024
"type": "boolean",
2125
"default": true

src/blocks/ai-overview/edit.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
1717
* @return {JSX.Element} Block editor component.
1818
*/
1919
export default function Edit( { attributes, setAttributes } ) {
20-
const { placeholder, buttonText, showSources } = attributes;
20+
const { placeholder, buttonText, hintText, showSources } = attributes;
2121

2222
return (
2323
<>
@@ -31,12 +31,30 @@ export default function Edit( { attributes, setAttributes } ) {
3131
}
3232
/>
3333
<TextControl
34-
label={ __( 'Button Text', 'hamelp' ) }
34+
label={ __(
35+
'送信ボタンのラベル(スクリーンリーダー用)',
36+
'hamelp'
37+
) }
38+
help={ __(
39+
'フロントエンドには↑アイコンのみ表示され、この文言はスクリーンリーダーの読み上げ用ラベルとして使われます。',
40+
'hamelp'
41+
) }
3542
value={ buttonText }
3643
onChange={ ( value ) =>
3744
setAttributes( { buttonText: value } )
3845
}
3946
/>
47+
<TextControl
48+
label={ __( '入力欄下の補助テキスト', 'hamelp' ) }
49+
help={ __(
50+
'Shift+Enterで改行できることの案内などに。空にすると非表示。スマートフォン等のタッチ端末では自動的に非表示になります。',
51+
'hamelp'
52+
) }
53+
value={ hintText }
54+
onChange={ ( value ) =>
55+
setAttributes( { hintText: value } )
56+
}
57+
/>
4058
<ToggleControl
4159
label={ __( 'Show Sources', 'hamelp' ) }
4260
checked={ showSources }
@@ -49,8 +67,33 @@ export default function Edit( { attributes, setAttributes } ) {
4967
<div { ...useBlockProps( { className: 'hamelp-ai-overview' } ) }>
5068
<div className="hamelp-ai-overview__preview">
5169
<textarea placeholder={ placeholder } disabled />
52-
<button disabled>{ buttonText }</button>
70+
<button className="hamelp-ai-overview__button" disabled>
71+
<svg
72+
className="hamelp-ai-overview__button-icon"
73+
width="20"
74+
height="20"
75+
viewBox="0 0 24 24"
76+
fill="none"
77+
aria-hidden="true"
78+
focusable="false"
79+
xmlns="http://www.w3.org/2000/svg"
80+
>
81+
<path
82+
d="M12 19V5M6 11l6-6 6 6"
83+
stroke="currentColor"
84+
strokeWidth="2"
85+
strokeLinecap="round"
86+
strokeLinejoin="round"
87+
/>
88+
</svg>
89+
<span className="screen-reader-text">
90+
{ buttonText }
91+
</span>
92+
</button>
5393
</div>
94+
{ hintText && (
95+
<p className="hamelp-ai-overview__hint">{ hintText }</p>
96+
) }
5497
<p className="hamelp-ai-overview__note">
5598
{ __( 'AI Overview - Preview', 'hamelp' ) }
5699
</p>

src/blocks/ai-overview/render.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
[
2424
'placeholder' => $attributes['placeholder'] ?? __( 'Enter your question...', 'hamelp' ),
2525
'button_text' => $attributes['buttonText'] ?? __( 'Ask AI', 'hamelp' ),
26+
'hint_text' => $attributes['hintText'] ?? __( 'Shift + Enter で改行', 'hamelp' ),
2627
'show_sources' => ! empty( $attributes['showSources'] ),
2728
'wrapper_attrs' => $wrapper_attributes,
2829
]

src/blocks/ai-overview/style.scss

Lines changed: 111 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
*/
1616

1717
:where(.hamelp-ai-overview) {
18+
// Fill the available width in every layout context. WordPress flex Group
19+
// blocks default their children to size-to-content (align-items is not
20+
// `stretch`), which otherwise collapses the block to its intrinsic width when
21+
// placed alongside e.g. a disclaimer paragraph. width:100% keeps it full-width
22+
// in flex/grid parents while remaining a no-op in normal block flow.
23+
width: 100%;
24+
box-sizing: border-box;
1825
// Theme-aware tokens (override these from your theme to re-skin the block).
1926
--hamelp-accent: var(--wp--preset--color--primary, #1571da);
2027
--hamelp-on-accent: var(--wp--preset--color--base, #fff);
@@ -25,6 +32,14 @@
2532
// input + button look identical across both blocks).
2633
--hamelp-radius: 3px;
2734
--hamelp-font-weight: 600;
35+
// Send button size, fully fluid with NO @media breakpoint (avoids colliding
36+
// with the theme's breakpoints). Inverse clamp (negative vw coefficient) so
37+
// the touch target GROWS as the viewport shrinks: 44px on phones (≤360px)
38+
// easing down to 40px on wide screens (≥640px). Everything else (textarea
39+
// min-height, right padding, bottom padding) derives from this token so the
40+
// ChatGPT-style inset and single-line text position stay consistent at every
41+
// width without special-casing.
42+
--hamelp-btn-size: clamp(40px, calc(49.14px - 1.43vw), 44px);
2843
--hamelp-button-bg: var(--hamelp-accent);
2944
// Hover shade derived from the button color; the plain background
3045
// declaration below wins on engines without color-mix() support.
@@ -41,8 +56,13 @@
4156
}
4257

4358
:where(.hamelp-ai-overview__input-row) {
59+
// Positioning context for the send button, which is taken out of flow and
60+
// floated inside the field's bottom-right corner (ChatGPT-style inset). The
61+
// field's own border/background/radius stay driven by the textarea's theme
62+
// variables — this container adds no matte of its own.
63+
position: relative;
64+
// Still a flex row so the textarea fills the full width.
4465
display: flex;
45-
gap: 0.5rem;
4666
}
4767

4868
// "Continue the previous conversation" toggle (conversation mode only).
@@ -57,7 +77,13 @@
5777

5878
:where(.hamelp-ai-overview__input) {
5979
flex: 1;
60-
padding: 0.75rem;
80+
box-sizing: border-box;
81+
// Reserve room on the right so typed text never runs under the inset send
82+
// button (right = btn size + 1rem to clear the button plus a small gap). The
83+
// vertical padding is SYMMETRIC (top/bottom both 0.75rem) so that, combined
84+
// with align-content: center below, the single line centers cleanly in the
85+
// content box regardless of the button size or the theme's line-height.
86+
padding: 0.75rem calc(var(--hamelp-btn-size) + 1rem) 0.75rem 0.75rem;
6187
border: 1px solid var(--hamelp-border);
6288
border-radius: var(--hamelp-radius, 3px);
6389
background: var(--hamelp-surface);
@@ -68,9 +94,20 @@
6894
font-family: inherit;
6995
line-height: 1.5;
7096
field-sizing: content;
71-
min-height: 1lh;
97+
// Minimum height sized so the inset send button clears equal ~8px gaps on
98+
// every side in the single-line state: button height + 2 × the button's
99+
// corner inset (0.5rem each). field-sizing still grows the field past this
100+
// as text wraps, at which point the top gap widens.
101+
min-height: calc(var(--hamelp-btn-size) + 1rem);
72102
max-height: 8lh;
73103
resize: none;
104+
// Vertically CENTER the single line of text within the inflated min-height.
105+
// With symmetric vertical padding above, centering is metric-robust: the line
106+
// stays visually centered independent of the theme's font/line-height (a
107+
// bottom-aligned approach drifts too low under some themes). Once the content
108+
// exceeds min-height (multi-line), align-content has no effect, so wrapped
109+
// text still grows top-anchored as expected (no text under the inset button).
110+
align-content: center;
74111
}
75112

76113
// Focus ring matches the button and the search box input (outline, 2px offset).
@@ -79,20 +116,54 @@
79116
outline-offset: 2px;
80117
}
81118

119+
// Small muted helper text directly under the input (e.g. "Shift + Enter で改行").
120+
// Muted via opacity so it inherits and adapts to any theme's text color.
121+
:where(.hamelp-ai-overview__hint) {
122+
margin: 0;
123+
font-size: 12px;
124+
line-height: 1.4;
125+
text-align: right;
126+
opacity: 0.6;
127+
}
128+
129+
// Hide the hint on touch devices, where Shift+Enter is meaningless without a
130+
// physical keyboard. Device-CAPABILITY query (coarse pointer + no hover),
131+
// deliberately NOT a width breakpoint, to avoid colliding with theme breakpoints.
132+
@media (hover: none) and (pointer: coarse) {
133+
:where(.hamelp-ai-overview__hint) {
134+
display: none;
135+
}
136+
}
137+
82138
:where(.hamelp-ai-overview__button) {
83-
padding: 0.75rem 1.5rem;
139+
// Fixed-size circular icon button (ChatGPT-style), floated inside the field's
140+
// bottom-right corner. The constant bottom offset keeps it pinned to the
141+
// bottom as the textarea grows, instead of riding up with the content.
142+
position: absolute;
143+
right: 0.5rem;
144+
bottom: 0.5rem;
145+
display: flex;
146+
align-items: center;
147+
justify-content: center;
148+
width: var(--hamelp-btn-size);
149+
height: var(--hamelp-btn-size);
150+
padding: 0;
84151
border: 1px solid transparent;
85-
border-radius: var(--hamelp-radius, 3px);
152+
border-radius: 50%;
86153
background: var(--hamelp-button-bg, #1571da);
154+
background: linear-gradient(135deg, var(--hamelp-button-bg, #1571da), var(--hamelp-accent-hover));
87155
color: var(--hamelp-on-accent);
88-
font-size: 1rem;
89-
font-weight: var(--hamelp-font-weight, 600);
90156
cursor: pointer;
91-
transition: background 0.2s ease;
157+
transition: opacity 0.2s ease;
158+
}
159+
160+
:where(.hamelp-ai-overview__button-icon) {
161+
width: 20px;
162+
height: 20px;
92163
}
93164

94165
:where(.hamelp-ai-overview__button:hover) {
95-
background: var(--hamelp-accent-hover);
166+
opacity: 0.9;
96167
}
97168

98169
:where(.hamelp-ai-overview__button:focus-visible) {
@@ -105,6 +176,21 @@
105176
cursor: wait;
106177
}
107178

179+
// WordPress-standard visually-hidden label: the button shows only the ↑ icon,
180+
// while the configurable button text stays available to screen readers.
181+
:where(.hamelp-ai-overview .screen-reader-text) {
182+
position: absolute;
183+
width: 1px;
184+
height: 1px;
185+
padding: 0;
186+
margin: -1px;
187+
overflow: hidden;
188+
clip: rect(1px, 1px, 1px, 1px);
189+
clip-path: inset(50%);
190+
border: 0;
191+
word-wrap: normal !important;
192+
}
193+
108194
// Conversation thread: question/answer turns stack from top to bottom,
109195
// with the form (added in the markup after the thread) staying below.
110196
:where(.hamelp-ai-overview__thread) {
@@ -204,22 +290,34 @@
204290
text-decoration: underline;
205291
}
206292

207-
// Editor preview styles.
293+
// Editor preview styles. Mirrors the front-end inset so the block preview shows
294+
// the send button floated inside the field's bottom-right corner.
208295
:where(.hamelp-ai-overview__preview) {
296+
position: relative;
209297
display: flex;
210-
gap: 0.5rem;
211298
}
212299

213300
:where(.hamelp-ai-overview__preview textarea),
214301
:where(.hamelp-ai-overview__preview button) {
215302
opacity: 0.7;
216303
}
217304

218-
// Keep the editor preview textarea a single line (non-interactive preview).
305+
// Keep the editor preview textarea a single line (non-interactive preview) and
306+
// reserve the same right padding so the inset button never overlaps text.
219307
:where(.hamelp-ai-overview__preview textarea) {
308+
flex: 1;
309+
box-sizing: border-box;
310+
// Mirror the front-end textarea padding (symmetric vertical padding) so the
311+
// inset button and the centered single line stay in the same place (see that
312+
// rule).
313+
padding: 0.75rem calc(var(--hamelp-btn-size) + 1rem) 0.75rem 0.75rem;
220314
resize: none;
221315
field-sizing: content;
222-
min-height: 1lh;
316+
// Match the front-end min-height so the inset send button clears equal gaps
317+
// on every side in the single-line state (see the front-end textarea rule).
318+
min-height: calc(var(--hamelp-btn-size) + 1rem);
319+
// Match the front-end vertically-centered single line of text.
320+
align-content: center;
223321
}
224322

225323
:where(.hamelp-ai-overview__note) {

0 commit comments

Comments
 (0)