Skip to content

Commit bd4e461

Browse files
feat: TriggerButton component (attention/subtle/minimal), editor trigger config
1 parent 82872d3 commit bd4e461

9 files changed

Lines changed: 261 additions & 7 deletions

File tree

,

Whitespace-only changes.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "next-easytour",
3-
"version": "0.3.0-alpha.19",
3+
"version": "0.3.0-alpha.20",
44
"description": "Generalizable interactive tutorial overlay library for React/Next.js. CSS-selector targeting, step actions, waitFor conditions, auto-scroll, highlight effects, animated transitions — all composable and declarative.",
55
"keywords": [
66
"tutorial",

src/editor/Editor.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
SaveHandler,
3030
Step,
3131
TargetPoint,
32+
TriggerConfig,
3233
ViewportAnchor,
3334
} from "../types";
3435

@@ -53,6 +54,8 @@ interface Overrides {
5354
removedIds: string[];
5455
/** alpha.8: global card position for all steps without their own override. */
5556
defaultCardAnchor?: ViewportAnchor;
57+
/** alpha.20: trigger button configuration. */
58+
triggerConfig?: TriggerConfig;
5659
}
5760

5861
const EMPTY_OVERRIDES: Overrides = {
@@ -64,6 +67,7 @@ const EMPTY_OVERRIDES: Overrides = {
6467
addedSteps: [],
6568
removedIds: [],
6669
defaultCardAnchor: undefined,
70+
triggerConfig: undefined,
6771
};
6872

6973
function unsavedCountOf(o: Overrides): number {
@@ -194,6 +198,9 @@ export interface EditorInternalApi {
194198
moveStep: (stepId: string, direction: "up" | "down") => void;
195199
/** alpha.8: set card position for ALL steps (global default). */
196200
setDefaultCardAnchor: (a: ViewportAnchor) => void;
201+
/** alpha.20: trigger button config. */
202+
triggerConfig?: TriggerConfig;
203+
setTriggerConfig: (config: TriggerConfig) => void;
197204
save: () => Promise<void>;
198205
revert: () => void;
199206
saveStatus: EditorState["saveStatus"];
@@ -329,6 +336,10 @@ export function Editor<Meta = never>(props: EditorProps<Meta>) {
329336
setOverrides((p) => ({ ...p, defaultCardAnchor: a }));
330337
}, []);
331338

339+
const setTriggerConfig = useCallback((config: TriggerConfig) => {
340+
setOverrides((p) => ({ ...p, triggerConfig: { ...(p.triggerConfig ?? {}), ...config } }));
341+
}, []);
342+
332343
const revert = useCallback(() => {
333344
setOverrides(EMPTY_OVERRIDES);
334345
setSaveStatus("idle");
@@ -375,11 +386,14 @@ export function Editor<Meta = never>(props: EditorProps<Meta>) {
375386
steps: mergedSteps as Step<unknown>[],
376387
setCardAnchor, clearCardAnchor, setArrowTip, setArrow, setCircles,
377388
updateStep, addStep, removeStep, moveStep,
378-
setDefaultCardAnchor, save, revert, saveStatus,
389+
setDefaultCardAnchor,
390+
triggerConfig: overrides.triggerConfig,
391+
setTriggerConfig,
392+
save, revert, saveStatus,
379393
}),
380394
[active, overrides, mergedSteps, setCardAnchor, clearCardAnchor,
381395
setArrowTip, setArrow, setCircles, updateStep, addStep, removeStep,
382-
moveStep, setDefaultCardAnchor, save, revert, saveStatus],
396+
moveStep, setDefaultCardAnchor, setTriggerConfig, save, revert, saveStatus],
383397
);
384398

385399
return (

src/editor/EditorPanel.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useTutorial } from "../core/Tutorial";
1212
import { useEditor, type EditorInternalApi } from "./Editor";
1313
import { OverlayPortal } from "../core/OverlayPortal";
1414
import { targetPoint } from "../coords";
15-
import type { Step, TextLabel, TextLabelAnimation, TextLabelFrame } from "../types";
15+
import type { Step, TextLabel, TextLabelAnimation, TextLabelFrame, TriggerConfig } from "../types";
1616

1717
// ── Icons ───────────────────────────────────────────────────────────────
1818

@@ -124,6 +124,31 @@ export function EditorPanel(props: EditorPanelProps) {
124124

125125
{!collapsed && (
126126
<>
127+
{/* ── Trigger button config ── */}
128+
<div className="eto-panel-trigger-section">
129+
<div className="eto-panel-section-title">Trigger Button</div>
130+
<div className="eto-panel-section-body">
131+
<Field label="Text">
132+
<input className="eto-panel-input"
133+
value={editor.triggerConfig?.text ?? "Start tutorial"}
134+
onChange={(e) => editor.setTriggerConfig({ text: e.target.value })}
135+
placeholder="Start tutorial" />
136+
</Field>
137+
<Field label="Mode">
138+
<div className="eto-panel-toggles">
139+
{(["attention", "subtle", "minimal"] as const).map((m) => (
140+
<Pill key={m} label={m} active={(editor.triggerConfig?.mode ?? "subtle") === m}
141+
onChange={() => editor.setTriggerConfig({ mode: m })} />
142+
))}
143+
</div>
144+
</Field>
145+
<div className="eto-panel-toggles">
146+
<Pill label="Show icon" active={editor.triggerConfig?.icon !== false}
147+
onChange={(on) => editor.setTriggerConfig({ icon: on })} />
148+
</div>
149+
</div>
150+
</div>
151+
127152
<div className="eto-panel-steps">
128153
{steps.map((s, i) => (
129154
<StepCard key={s.id} step={s} index={i} total={steps.length}

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export type { TooltipProps } from "./overlay/Tooltip";
2020

2121
export { Labels } from "./overlay/Labels";
2222

23+
export { TriggerButton } from "./overlay/TriggerButton";
24+
export type { TriggerButtonProps, TriggerMode } from "./overlay/TriggerButton";
25+
2326
// Editor
2427
export { Editor, useEditorState } from "./editor/Editor";
2528
export type { EditorProps } from "./editor/Editor";
@@ -31,7 +34,7 @@ export type {
3134
TargetPoint, ViewportAnchor,
3235
Arrow as ArrowAnnotation, ArrowStyle, Circle, Annotations,
3336
Step, StepAction, WaitCondition, HighlightEffect, TransitionConfig,
34-
TutorialStatus, TutorialApi, TutorialProps, TutorialTheme, TextLabel,
37+
TutorialStatus, TutorialApi, TutorialProps, TutorialTheme, TriggerConfig, TextLabel,
3538
TextLabelAnimation, TextLabelFrame,
3639
CanEdit, EditorState, SaveHandler,
3740
} from "./types";

src/overlay/TriggerButton.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
/**
4+
* @module overlay/TriggerButton
5+
*
6+
* Alpha.20: configurable tutorial trigger button.
7+
*
8+
* Modes:
9+
* - "attention" — animated gradient pulse, impossible to miss
10+
* - "subtle" — calm accent-tinted button
11+
* - "minimal" — icon-only circle
12+
*
13+
* Hides automatically when the tutorial is active (stepId !== null).
14+
* The host places it anywhere in their layout and provides `onClick`.
15+
*
16+
* ```tsx
17+
* <TriggerButton
18+
* text="Take the tour"
19+
* mode="attention"
20+
* onClick={() => setStepId(steps[0].id)}
21+
* />
22+
* ```
23+
*/
24+
25+
import * as React from "react";
26+
import { useTutorial } from "../core/Tutorial";
27+
28+
// ── Types ───────────────────────────────────────────────────────────────
29+
30+
export type TriggerMode = "attention" | "subtle" | "minimal";
31+
32+
export interface TriggerButtonProps {
33+
/** Click handler — typically calls setStepId(firstStep.id). */
34+
onClick: () => void;
35+
/** Button text. Default "Start tutorial". Ignored in "minimal" mode. */
36+
text?: string;
37+
/** Visual mode. Default "subtle". */
38+
mode?: TriggerMode;
39+
/** Show the help-circle icon. Default true. */
40+
icon?: boolean;
41+
/** Override the automatic hide-when-active behaviour. */
42+
visible?: boolean;
43+
/** Additional CSS class on the wrapper. */
44+
className?: string;
45+
/** Additional inline styles on the wrapper. */
46+
style?: React.CSSProperties;
47+
}
48+
49+
// ── Icon ────────────────────────────────────────────────────────────────
50+
51+
function HelpIcon({ size = 14 }: { size?: number }) {
52+
return (
53+
<svg viewBox="0 0 24 24" width={size} height={size} fill="none"
54+
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
55+
<circle cx="12" cy="12" r="10" />
56+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
57+
<line x1="12" y1="17" x2="12.01" y2="17" />
58+
</svg>
59+
);
60+
}
61+
62+
// ── Component ───────────────────────────────────────────────────────────
63+
64+
export function TriggerButton(props: TriggerButtonProps) {
65+
const {
66+
onClick,
67+
text = "Start tutorial",
68+
mode = "subtle",
69+
icon = true,
70+
visible: visibleProp,
71+
className,
72+
style,
73+
} = props;
74+
75+
const { step } = useTutorial();
76+
const isActive = step !== null;
77+
78+
// Auto-hide when tutorial is running (unless overridden)
79+
const visible = visibleProp ?? !isActive;
80+
if (!visible) return null;
81+
82+
const cls = [
83+
"eto-trigger",
84+
`eto-trigger--${mode}`,
85+
className,
86+
].filter(Boolean).join(" ");
87+
88+
return (
89+
<button type="button" className={cls} style={style} onClick={onClick}>
90+
{icon && <HelpIcon size={mode === "minimal" ? 16 : 14} />}
91+
{mode !== "minimal" && <span className="eto-trigger-text">{text}</span>}
92+
</button>
93+
);
94+
}

src/styles.css

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,13 @@
526526
/* Step list */
527527
.eto-panel-steps { flex: 1; overflow-y: auto; padding: 2px 0; }
528528

529+
/* Trigger button config section */
530+
.eto-panel-trigger-section {
531+
padding: 4px 8px 6px;
532+
border-bottom: 1px solid var(--eto-border-soft);
533+
flex-shrink: 0;
534+
}
535+
529536
.eto-panel-step { border-bottom: 1px solid var(--eto-border-soft); }
530537
.eto-panel-step--active { background: color-mix(in srgb, var(--eto-accent) 5%, transparent); }
531538

@@ -1053,4 +1060,105 @@
10531060
align-items: center;
10541061
justify-content: center;
10551062
pointer-events: none;
1063+
}
1064+
1065+
/* ── TriggerButton (alpha.20) ────────────────────────────────────────
1066+
Tutorial start button with three visual modes. */
1067+
1068+
.eto-trigger {
1069+
all: unset;
1070+
box-sizing: border-box;
1071+
cursor: pointer;
1072+
display: inline-flex;
1073+
align-items: center;
1074+
gap: 6px;
1075+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1076+
user-select: none;
1077+
-webkit-tap-highlight-color: transparent;
1078+
}
1079+
1080+
/* ── Subtle mode (default) ── */
1081+
1082+
.eto-trigger--subtle {
1083+
padding: 5px 14px;
1084+
border-radius: 9999px;
1085+
font-size: 11px;
1086+
font-weight: 600;
1087+
color: var(--eto-accent);
1088+
border: 1px solid color-mix(in srgb, var(--eto-accent) 30%, transparent);
1089+
background: color-mix(in srgb, var(--eto-accent) 6%, transparent);
1090+
box-shadow: 0 0 16px -4px color-mix(in srgb, var(--eto-accent) 20%, transparent);
1091+
transition: background 150ms, border-color 150ms, box-shadow 150ms;
1092+
animation: eto-trigger-enter 250ms ease-out;
1093+
}
1094+
.eto-trigger--subtle:hover {
1095+
background: color-mix(in srgb, var(--eto-accent) 12%, transparent);
1096+
border-color: color-mix(in srgb, var(--eto-accent) 50%, transparent);
1097+
box-shadow: 0 0 20px -2px color-mix(in srgb, var(--eto-accent) 30%, transparent);
1098+
}
1099+
.eto-trigger--subtle svg {
1100+
color: var(--eto-accent);
1101+
}
1102+
1103+
/* ── Attention mode (flashy) ── */
1104+
1105+
.eto-trigger--attention {
1106+
padding: 6px 16px;
1107+
border-radius: 9999px;
1108+
font-size: 12px;
1109+
font-weight: 700;
1110+
color: #fff;
1111+
border: 1px solid rgba(255, 100, 100, 0.5);
1112+
background:
1113+
linear-gradient(90deg,
1114+
rgba(220,38,38,0.6) 0%,
1115+
rgba(255,100,100,0.85) 25%,
1116+
rgba(220,38,38,0.6) 50%,
1117+
rgba(180,20,20,0.7) 100%);
1118+
background-size: 200% 100%;
1119+
animation:
1120+
eto-trigger-sweep 1.2s linear infinite,
1121+
eto-trigger-glow 1.5s ease-in-out infinite,
1122+
eto-trigger-enter 250ms ease-out;
1123+
}
1124+
.eto-trigger--attention:hover {
1125+
filter: brightness(1.1);
1126+
}
1127+
.eto-trigger--attention svg {
1128+
color: rgba(255, 255, 255, 0.9);
1129+
}
1130+
1131+
/* ── Minimal mode (icon-only circle) ── */
1132+
1133+
.eto-trigger--minimal {
1134+
padding: 6px;
1135+
border-radius: 9999px;
1136+
color: var(--eto-accent);
1137+
border: 1px solid color-mix(in srgb, var(--eto-accent) 25%, transparent);
1138+
background: color-mix(in srgb, var(--eto-accent) 5%, transparent);
1139+
transition: background 150ms, border-color 150ms;
1140+
animation: eto-trigger-enter 250ms ease-out;
1141+
}
1142+
.eto-trigger--minimal:hover {
1143+
background: color-mix(in srgb, var(--eto-accent) 12%, transparent);
1144+
border-color: color-mix(in srgb, var(--eto-accent) 40%, transparent);
1145+
}
1146+
1147+
.eto-trigger-text {
1148+
white-space: nowrap;
1149+
}
1150+
1151+
/* ── Trigger keyframes ── */
1152+
1153+
@keyframes eto-trigger-enter {
1154+
from { opacity: 0; transform: scale(0.9); }
1155+
to { opacity: 1; transform: scale(1); }
1156+
}
1157+
@keyframes eto-trigger-sweep {
1158+
from { background-position: 200% 0; }
1159+
to { background-position: 0% 0; }
1160+
}
1161+
@keyframes eto-trigger-glow {
1162+
0%, 100% { box-shadow: 0 0 8px rgba(220, 38, 38, 0.3); }
1163+
50% { box-shadow: 0 0 20px rgba(255, 100, 100, 0.6), 0 0 40px rgba(220, 38, 38, 0.2); }
10561164
}

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,14 @@ export interface TutorialTheme {
318318
cardWidth?: string;
319319
/** Card border radius. Default "0.5rem". */
320320
cardRadius?: string;
321+
}
322+
323+
/** Configuration for the tutorial trigger button. */
324+
export interface TriggerConfig {
325+
/** Button text. Default "Start tutorial". */
326+
text?: string;
327+
/** Visual mode. Default "subtle". */
328+
mode?: "attention" | "subtle" | "minimal";
329+
/** Show help-circle icon. Default true. */
330+
icon?: boolean;
321331
}

0 commit comments

Comments
 (0)