Skip to content

Commit 82872d3

Browse files
feat: alpha.19 — remove branding, TutorialTheme styling API, MiniUMAP reset on close
1 parent 16045e5 commit 82872d3

8 files changed

Lines changed: 92 additions & 364 deletions

File tree

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.18",
3+
"version": "0.3.0-alpha.19",
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/core/Tutorial.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,47 @@ export function Tutorial<Meta = never>(props: TutorialProps<Meta>) {
9595
scrollIntoView: globalScrollIntoView,
9696
defaultCardAnchor,
9797
cardPositioning: cardPositioningProp,
98+
theme,
9899
children,
99100
} = props;
100101

101102
const cardPositioning = cardPositioningProp ?? "fixed";
102103

104+
// ── Theme injection ─────────────────────────────────────────────────
105+
// Sets CSS custom properties on :root so portaled overlays inherit them.
106+
useEffect(() => {
107+
if (!theme) return;
108+
const root = document.documentElement;
109+
const mapping: Record<string, string> = {
110+
accent: "--eto-accent",
111+
surface: "--eto-surface",
112+
fg: "--eto-fg",
113+
muted: "--eto-muted",
114+
mutedSoft: "--eto-muted-soft",
115+
hoverBg: "--eto-hover-bg",
116+
border: "--eto-border",
117+
borderSoft: "--eto-border-soft",
118+
arrowColor: "--eto-arrow",
119+
arrowOpacity: "--eto-arrow-opacity",
120+
cardWidth: "--eto-card-width",
121+
cardRadius: "--eto-card-radius",
122+
};
123+
const applied: string[] = [];
124+
for (const [key, cssVar] of Object.entries(mapping)) {
125+
const val = (theme as Record<string, string | undefined>)[key];
126+
if (val !== undefined) {
127+
root.style.setProperty(cssVar, val);
128+
applied.push(cssVar);
129+
}
130+
}
131+
return () => {
132+
// Clean up: remove only the vars we set
133+
for (const cssVar of applied) {
134+
root.style.removeProperty(cssVar);
135+
}
136+
};
137+
}, [theme]);
138+
103139
// ── Validation (dev-only) ─────────────────────────────────────────────
104140
const validatedRef = useRef(false);
105141
if (

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export type {
3131
TargetPoint, ViewportAnchor,
3232
Arrow as ArrowAnnotation, ArrowStyle, Circle, Annotations,
3333
Step, StepAction, WaitCondition, HighlightEffect, TransitionConfig,
34-
TutorialStatus, TutorialApi, TutorialProps, TextLabel,
34+
TutorialStatus, TutorialApi, TutorialProps, TutorialTheme, TextLabel,
3535
TextLabelAnimation, TextLabelFrame,
36-
CanEdit, EditorState, SaveHandler, CardVariant, BrandedCardProps,
36+
CanEdit, EditorState, SaveHandler,
3737
} from "./types";
3838

3939
// Constructors

src/overlay/Card.tsx

Lines changed: 5 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { OverlayPortal } from "../core/OverlayPortal";
2424
import { useEnterAnimation } from "../core/animation";
2525
import { useCardRectSetter } from "./Arrow";
2626
import type { Rect } from "../core/useTargetRect";
27-
import type { TutorialApi, CardVariant, BrandedCardProps, TransitionConfig } from "../types";
27+
import type { TutorialApi, TransitionConfig } from "../types";
2828

2929
// ── Render-prop signature ───────────────────────────────────────────────
3030

@@ -45,9 +45,8 @@ type CardChildren<Meta = unknown> =
4545
| React.ReactNode
4646
| ((args: CardRenderArgs<Meta>) => React.ReactNode);
4747

48-
export interface CardProps<Meta = unknown> extends BrandedCardProps {
48+
export interface CardProps<Meta = unknown> {
4949
children?: CardChildren<Meta>;
50-
variant?: CardVariant;
5150
disableKeyboard?: boolean;
5251
/** Override the global transition config. */
5352
transition?: TransitionConfig;
@@ -59,12 +58,6 @@ export function Card<Meta = unknown>(props: CardProps<Meta>) {
5958
const {
6059
children,
6160
disableKeyboard = false,
62-
variant = "default",
63-
logo,
64-
logoAlt,
65-
labels,
66-
accent,
67-
hideProgress,
6861
transition: transitionProp,
6962
} = props;
7063
const api = useTutorial<Meta>();
@@ -199,28 +192,23 @@ export function Card<Meta = unknown>(props: CardProps<Meta>) {
199192
? (children as (a: CardRenderArgs<Meta>) => React.ReactNode)(renderArgs)
200193
: null;
201194

202-
const isBranded = variant === "branded" && !children;
203195
// When render-prop mode is active, the wrapper is just a positioning
204196
// container — the host's render-prop provides all visual styling.
205197
// .eto-card--custom strips border/shadow/bg from the wrapper.
206198
const className = [
207199
"eto-card",
208200
isRenderProp ? "eto-card--custom" : "",
209-
isBranded ? "eto-card--branded" : "",
210201
animClass,
211202
].filter(Boolean).join(" ");
212203

213-
const styleWithAccent: React.CSSProperties = {
214-
...positionStyle,
215-
...(accent ? ({ ["--eto-branded-accent" as string]: accent } as React.CSSProperties) : {}),
216-
};
204+
const styleObj: React.CSSProperties = { ...positionStyle };
217205

218206
return (
219207
<OverlayPortal>
220208
<div
221209
ref={cardRef}
222210
className={className}
223-
style={styleWithAccent}
211+
style={styleObj}
224212
role="dialog"
225213
aria-modal="false"
226214
aria-labelledby={!isRenderProp ? titleId : undefined}
@@ -230,16 +218,6 @@ export function Card<Meta = unknown>(props: CardProps<Meta>) {
230218
rendered
231219
) : children ? (
232220
children
233-
) : isBranded ? (
234-
<BrandedCardBody
235-
api={api as TutorialApi<unknown>}
236-
titleId={titleId}
237-
bodyId={bodyId}
238-
logo={logo}
239-
logoAlt={logoAlt}
240-
labels={labels}
241-
hideProgress={hideProgress}
242-
/>
243221
) : (
244222
<DefaultCardBody
245223
api={api as TutorialApi<unknown>}
@@ -292,80 +270,4 @@ function DefaultCardBody(props: {
292270
);
293271
}
294272

295-
// ── Branded body ────────────────────────────────────────────────────────
296-
297-
function BrandedCardBody(props: {
298-
api: TutorialApi<unknown>;
299-
titleId: string;
300-
bodyId: string;
301-
logo?: string | React.ReactNode;
302-
logoAlt?: string;
303-
labels?: BrandedCardProps["labels"];
304-
hideProgress?: boolean;
305-
}) {
306-
const { api, titleId, bodyId, logo, logoAlt, labels, hideProgress } = props;
307-
const { step, index, total, isFirst, isLast, canAdvance, isWaiting, next, prev, close } = api;
308-
if (!step) return null;
309-
310-
const backLabel = labels?.back ?? "Back";
311-
const nextLabel = labels?.next ?? "Next";
312-
const doneLabel = labels?.done ?? "Done";
313-
const closeLabel = labels?.close ?? "Close tutorial";
314-
315-
const logoNode =
316-
typeof logo === "string" ? (
317-
<img src={logo} alt={logoAlt ?? ""} className="eto-branded-logo" draggable={false} />
318-
) : (logo ?? null);
319-
320-
const progressNode = (() => {
321-
if (hideProgress) return null;
322-
if (total <= 15) {
323-
return (
324-
<div className="eto-branded-progress" aria-hidden="true">
325-
{Array.from({ length: total }, (_, i) => (
326-
<span key={i} className={`eto-branded-dot${i === index ? " eto-active" : ""}`} />
327-
))}
328-
</div>
329-
);
330-
}
331-
return (
332-
<div className="eto-branded-progress">
333-
<span className="eto-branded-progress-numeric">{index + 1} / {total}</span>
334-
</div>
335-
);
336-
})();
337-
338-
return (
339-
<>
340-
<div className="eto-branded-header">
341-
{logoNode}
342-
<button type="button" className="eto-branded-close" onClick={close} aria-label={closeLabel}>
343-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
344-
<line x1="6" y1="6" x2="18" y2="18" />
345-
<line x1="18" y1="6" x2="6" y2="18" />
346-
</svg>
347-
</button>
348-
</div>
349-
<div className="eto-branded-body">
350-
{step.title && <h4 id={titleId} className="eto-branded-title">{step.title}</h4>}
351-
{step.content ? (
352-
<div id={bodyId} className="eto-branded-copy">{step.content}</div>
353-
) : step.body ? (
354-
<p id={bodyId} className="eto-branded-copy">{step.body}</p>
355-
) : null}
356-
</div>
357-
{progressNode}
358-
<div className="eto-branded-footer">
359-
<button type="button" className="eto-branded-btn eto-branded-btn-back" onClick={prev} disabled={isFirst}>{backLabel}</button>
360-
<button
361-
type="button"
362-
className={`eto-branded-btn eto-branded-btn-next${isWaiting ? " eto-waiting" : ""}`}
363-
onClick={next}
364-
disabled={!canAdvance}
365-
>
366-
{isWaiting ? "…" : isLast ? doneLabel : nextLabel}
367-
</button>
368-
</div>
369-
</>
370-
);
371-
}
273+
// (BrandedCardBody removed in alpha.19 — hosts use render-prop for custom card styling)

src/styles.css

Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,7 @@
201201

202202
/* ── Waiting state ── */
203203

204-
.eto-btn.eto-waiting,
205-
.eto-branded-btn.eto-waiting {
204+
.eto-btn.eto-waiting {
206205
opacity: 0.6;
207206
animation: eto-pulse-subtle 1.5s ease-in-out infinite;
208207
}
@@ -437,133 +436,6 @@
437436
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--eto-accent) 30%, transparent), 0 2px 8px rgba(0,0,0,0.25); }
438437
}
439438

440-
/* ── Branded card variant ── */
441-
442-
.eto-card--branded {
443-
padding: 0 !important;
444-
overflow: hidden;
445-
width: min(420px, calc(100vw - 2rem));
446-
border-radius: calc(var(--eto-card-radius) * 1.5);
447-
border: 1px solid var(--eto-border);
448-
background: var(--eto-surface);
449-
box-shadow:
450-
0 10px 25px -5px color-mix(in srgb, #000 25%, transparent),
451-
0 8px 10px -6px color-mix(in srgb, #000 10%, transparent);
452-
}
453-
454-
.eto-card--branded .eto-branded-header {
455-
position: relative;
456-
padding: 14px 20px;
457-
display: flex;
458-
align-items: center;
459-
justify-content: space-between;
460-
gap: 8px;
461-
background: linear-gradient(
462-
135deg,
463-
var(--eto-branded-accent, var(--eto-accent)) 0%,
464-
color-mix(in srgb, var(--eto-branded-accent, var(--eto-accent)) 75%, #000) 100%
465-
);
466-
color: #fff;
467-
}
468-
469-
.eto-card--branded .eto-branded-logo {
470-
height: 24px;
471-
width: auto;
472-
user-select: none;
473-
filter: brightness(0) invert(1);
474-
}
475-
476-
.eto-card--branded .eto-branded-close {
477-
all: unset;
478-
box-sizing: border-box;
479-
padding: 6px;
480-
border-radius: 6px;
481-
cursor: pointer;
482-
color: rgba(255, 255, 255, 0.8);
483-
transition: background-color 120ms ease, color 120ms ease;
484-
display: inline-flex;
485-
align-items: center;
486-
justify-content: center;
487-
}
488-
.eto-card--branded .eto-branded-close:hover {
489-
background-color: rgba(255, 255, 255, 0.15);
490-
color: #fff;
491-
}
492-
.eto-card--branded .eto-branded-close svg { width: 16px; height: 16px; }
493-
494-
.eto-card--branded .eto-branded-body {
495-
padding: 16px 20px 12px;
496-
display: flex;
497-
flex-direction: column;
498-
gap: 8px;
499-
}
500-
.eto-card--branded .eto-branded-title {
501-
font-size: 15px; font-weight: 600; line-height: 1.3; color: var(--eto-fg); margin: 0;
502-
}
503-
.eto-card--branded .eto-branded-copy {
504-
font-size: 13px; line-height: 1.55; color: var(--eto-muted); margin: 0; white-space: pre-wrap;
505-
}
506-
507-
.eto-card--branded .eto-branded-progress {
508-
padding: 8px 20px;
509-
display: flex;
510-
align-items: center;
511-
justify-content: center;
512-
gap: 6px;
513-
}
514-
.eto-card--branded .eto-branded-dot {
515-
display: block; height: 6px; width: 6px; border-radius: 9999px;
516-
background: color-mix(in srgb, var(--eto-branded-accent, var(--eto-accent)) 25%, transparent);
517-
transition: width 200ms ease, background-color 200ms ease;
518-
}
519-
.eto-card--branded .eto-branded-dot.eto-active {
520-
width: 18px;
521-
background: var(--eto-branded-accent, var(--eto-accent));
522-
}
523-
.eto-card--branded .eto-branded-progress-numeric {
524-
font-size: 11px; color: var(--eto-muted); font-variant-numeric: tabular-nums;
525-
}
526-
527-
.eto-card--branded .eto-branded-footer {
528-
padding: 12px 20px;
529-
display: flex;
530-
align-items: center;
531-
justify-content: space-between;
532-
gap: 12px;
533-
border-top: 1px solid var(--eto-border-soft);
534-
}
535-
.eto-card--branded .eto-branded-btn {
536-
all: unset;
537-
box-sizing: border-box;
538-
cursor: pointer;
539-
padding: 6px 12px;
540-
font-size: 12px;
541-
font-weight: 500;
542-
border-radius: 6px;
543-
transition: background-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
544-
user-select: none;
545-
}
546-
.eto-card--branded .eto-branded-btn:disabled,
547-
.eto-card--branded .eto-branded-btn[aria-disabled="true"] {
548-
opacity: 0.4;
549-
pointer-events: none;
550-
}
551-
.eto-card--branded .eto-branded-btn-back { color: var(--eto-muted); }
552-
.eto-card--branded .eto-branded-btn-back:hover:not(:disabled) {
553-
background-color: var(--eto-hover-bg);
554-
color: var(--eto-fg);
555-
}
556-
.eto-card--branded .eto-branded-btn-next {
557-
padding: 6px 16px;
558-
font-weight: 600;
559-
color: #fff;
560-
background: var(--eto-branded-accent, var(--eto-accent));
561-
box-shadow: 0 1px 2px color-mix(in srgb, #000 15%, transparent);
562-
}
563-
.eto-card--branded .eto-branded-btn-next:hover:not(:disabled) {
564-
box-shadow: 0 3px 6px color-mix(in srgb, #000 20%, transparent);
565-
}
566-
567439
/* ── Keyframes ── */
568440

569441
@keyframes eto-card-fade {

0 commit comments

Comments
 (0)