@djangocfg/ui-tools 2.1.166 → 2.1.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.168",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"check": "tsc --noEmit"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@djangocfg/i18n": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
81
|
+
"@djangocfg/i18n": "^2.1.168",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.168",
|
|
83
83
|
"lucide-react": "^0.545.0",
|
|
84
84
|
"react": "^19.0.0",
|
|
85
85
|
"react-dom": "^19.0.0",
|
|
@@ -112,10 +112,10 @@
|
|
|
112
112
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
113
113
|
},
|
|
114
114
|
"devDependencies": {
|
|
115
|
-
"@djangocfg/i18n": "^2.1.
|
|
115
|
+
"@djangocfg/i18n": "^2.1.168",
|
|
116
116
|
"@djangocfg/playground": "workspace:*",
|
|
117
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
118
|
-
"@djangocfg/ui-core": "^2.1.
|
|
117
|
+
"@djangocfg/typescript-config": "^2.1.168",
|
|
118
|
+
"@djangocfg/ui-core": "^2.1.168",
|
|
119
119
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
120
120
|
"@types/node": "^24.7.2",
|
|
121
121
|
"@types/react": "^19.1.0",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
4
5
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
6
|
import {
|
|
6
7
|
Popover,
|
|
7
8
|
PopoverAnchor,
|
|
8
9
|
PopoverContent,
|
|
9
10
|
PopoverArrow,
|
|
11
|
+
Button,
|
|
10
12
|
} from '@djangocfg/ui-core/components';
|
|
11
13
|
import { TourProgress } from './TourProgress';
|
|
12
14
|
import { TourNavigation } from './TourNavigation';
|
|
@@ -103,11 +105,21 @@ export function TourContent({
|
|
|
103
105
|
>
|
|
104
106
|
{showArrow && <PopoverArrow className="fill-popover" />}
|
|
105
107
|
|
|
106
|
-
{/* Header */}
|
|
107
|
-
<div className="pb-2">
|
|
108
|
+
{/* Header with close button */}
|
|
109
|
+
<div className="flex items-start justify-between gap-2 pb-2">
|
|
108
110
|
<h3 id="tour-title" className="text-base font-semibold leading-none tracking-tight">
|
|
109
111
|
{step.title}
|
|
110
112
|
</h3>
|
|
113
|
+
{showSkipButton && (
|
|
114
|
+
<Button
|
|
115
|
+
variant="ghost"
|
|
116
|
+
size="icon"
|
|
117
|
+
className="h-6 w-6 shrink-0 -mt-1 -mr-2"
|
|
118
|
+
onClick={onClose}
|
|
119
|
+
>
|
|
120
|
+
<X className="h-4 w-4" />
|
|
121
|
+
</Button>
|
|
122
|
+
)}
|
|
111
123
|
</div>
|
|
112
124
|
|
|
113
125
|
{/* Content */}
|
|
@@ -127,14 +139,11 @@ export function TourContent({
|
|
|
127
139
|
<TourNavigation
|
|
128
140
|
onNext={onNext}
|
|
129
141
|
onPrevious={onPrevious}
|
|
130
|
-
onSkip={onClose}
|
|
131
142
|
isFirstStep={isFirstStep}
|
|
132
143
|
isLastStep={isLastStep}
|
|
133
144
|
nextLabel={step.nextLabel}
|
|
134
145
|
previousLabel={step.previousLabel}
|
|
135
|
-
skipLabel={skipLabel}
|
|
136
146
|
finishLabel={finishLabel}
|
|
137
|
-
showSkipButton={showSkipButton}
|
|
138
147
|
nextRoute={step.nextRoute}
|
|
139
148
|
previousRoute={step.previousRoute}
|
|
140
149
|
className="w-full"
|
|
@@ -3,25 +3,23 @@
|
|
|
3
3
|
import { useCallback, useMemo } from 'react';
|
|
4
4
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
5
|
import { Button } from '@djangocfg/ui-core/components';
|
|
6
|
-
import { ChevronLeft, ChevronRight
|
|
6
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
7
7
|
import { useT } from '@djangocfg/i18n';
|
|
8
8
|
import type { TourNavigationProps } from '../types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Navigation buttons for tour steps.
|
|
12
12
|
* For multi-page tours, use nextRoute/previousRoute with your router.
|
|
13
|
+
* Skip button is now in TourContent header as × icon.
|
|
13
14
|
*/
|
|
14
15
|
export function TourNavigation({
|
|
15
16
|
onNext,
|
|
16
17
|
onPrevious,
|
|
17
|
-
onSkip,
|
|
18
18
|
isFirstStep,
|
|
19
19
|
isLastStep,
|
|
20
20
|
nextLabel,
|
|
21
21
|
previousLabel,
|
|
22
|
-
skipLabel,
|
|
23
22
|
finishLabel,
|
|
24
|
-
showSkipButton = true,
|
|
25
23
|
nextRoute,
|
|
26
24
|
previousRoute,
|
|
27
25
|
className,
|
|
@@ -34,13 +32,11 @@ export function TourNavigation({
|
|
|
34
32
|
return {
|
|
35
33
|
next: nextLabel ?? finishLabel ?? defaultNext,
|
|
36
34
|
previous: previousLabel ?? t('tools.tour.previous'),
|
|
37
|
-
skip: skipLabel ?? t('tools.tour.skip'),
|
|
38
35
|
};
|
|
39
|
-
}, [isLastStep, nextLabel, finishLabel, previousLabel,
|
|
36
|
+
}, [isLastStep, nextLabel, finishLabel, previousLabel, t]);
|
|
40
37
|
|
|
41
38
|
// Prepare visibility flags
|
|
42
39
|
const showPrevious = !isFirstStep;
|
|
43
|
-
const showSkip = showSkipButton && !isLastStep && !!onSkip;
|
|
44
40
|
const showNextArrow = !isLastStep;
|
|
45
41
|
|
|
46
42
|
// Handlers
|
|
@@ -59,32 +55,23 @@ export function TourNavigation({
|
|
|
59
55
|
}, [previousRoute, onPrevious]);
|
|
60
56
|
|
|
61
57
|
// Styles
|
|
62
|
-
const containerClassName = cn('flex items-center
|
|
58
|
+
const containerClassName = cn('flex items-center gap-2', className);
|
|
63
59
|
|
|
64
60
|
return (
|
|
65
61
|
<div className={containerClassName}>
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
{labels.previous}
|
|
71
|
-
</Button>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div className="flex items-center gap-2">
|
|
76
|
-
{showSkip && (
|
|
77
|
-
<Button variant="ghost" size="sm" onClick={onSkip}>
|
|
78
|
-
{labels.skip}
|
|
79
|
-
<X className="ml-1 h-3 w-3" />
|
|
80
|
-
</Button>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
<Button size="sm" onClick={handleNext} className="gap-1">
|
|
84
|
-
{labels.next}
|
|
85
|
-
{showNextArrow && <ChevronRight className="h-4 w-4" />}
|
|
62
|
+
{showPrevious ? (
|
|
63
|
+
<Button variant="ghost" size="sm" onClick={handlePrevious} className="flex-1 gap-1">
|
|
64
|
+
<ChevronLeft className="h-4 w-4 shrink-0" />
|
|
65
|
+
<span className="truncate">{labels.previous}</span>
|
|
86
66
|
</Button>
|
|
87
|
-
|
|
67
|
+
) : (
|
|
68
|
+
<div className="flex-1" />
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<Button size="sm" onClick={handleNext} className="flex-1 gap-1">
|
|
72
|
+
<span className="truncate">{labels.next}</span>
|
|
73
|
+
{showNextArrow && <ChevronRight className="h-4 w-4 shrink-0" />}
|
|
74
|
+
</Button>
|
|
88
75
|
</div>
|
|
89
76
|
);
|
|
90
77
|
}
|
|
@@ -192,14 +192,11 @@ export interface TourProgressProps {
|
|
|
192
192
|
export interface TourNavigationProps {
|
|
193
193
|
onNext: () => void;
|
|
194
194
|
onPrevious: () => void;
|
|
195
|
-
onSkip?: () => void;
|
|
196
195
|
isFirstStep: boolean;
|
|
197
196
|
isLastStep: boolean;
|
|
198
197
|
nextLabel?: ReactNode;
|
|
199
198
|
previousLabel?: ReactNode;
|
|
200
|
-
skipLabel?: ReactNode;
|
|
201
199
|
finishLabel?: ReactNode;
|
|
202
|
-
showSkipButton?: boolean;
|
|
203
200
|
nextRoute?: string;
|
|
204
201
|
previousRoute?: string;
|
|
205
202
|
className?: string;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { emitRuntimeError } from '@djangocfg/ui-core/utils';
|
|
2
|
+
|
|
1
3
|
import { logger } from './logger';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Safely escape string for use in CSS attribute selector
|
|
7
|
+
*/
|
|
8
|
+
function escapeCSS(str: string): string {
|
|
9
|
+
return str.replace(/["\\]/g, '\\$&');
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
13
|
* Find target elements for a tour step.
|
|
5
14
|
* Supports CSS selectors and data-tour-step-id attribute.
|
|
@@ -10,10 +19,18 @@ export function findTargetElements(
|
|
|
10
19
|
): HTMLElement[] {
|
|
11
20
|
if (!target) {
|
|
12
21
|
// Fallback to data-tour-step-id attribute
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
try {
|
|
23
|
+
const escapedStepId = escapeCSS(stepId);
|
|
24
|
+
const elements = document.querySelectorAll<HTMLElement>(
|
|
25
|
+
`[data-tour-step-id="${escapedStepId}"], [data-tour-step-id*="${escapedStepId}"]`
|
|
26
|
+
);
|
|
27
|
+
return filterValidElements(Array.from(elements));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
30
|
+
logger.error(`Tour: Invalid stepId selector "${stepId}"`, { error: err.message, stepId });
|
|
31
|
+
emitRuntimeError('Tour', `Invalid stepId: ${stepId}`, err);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
// Try as CSS selector first
|
|
@@ -21,15 +38,26 @@ export function findTargetElements(
|
|
|
21
38
|
const elements = document.querySelectorAll<HTMLElement>(target);
|
|
22
39
|
const valid = filterValidElements(Array.from(elements));
|
|
23
40
|
if (valid.length > 0) return valid;
|
|
24
|
-
} catch {
|
|
25
|
-
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
43
|
+
logger.warn(`Tour: Invalid target selector "${target}"`, { error: err.message, target, stepId });
|
|
44
|
+
emitRuntimeError('Tour', `Invalid selector: ${target}`, err);
|
|
45
|
+
return [];
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
// Fallback to data attribute
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
try {
|
|
50
|
+
const escapedTarget = escapeCSS(target);
|
|
51
|
+
const elements = document.querySelectorAll<HTMLElement>(
|
|
52
|
+
`[data-tour-step-id="${escapedTarget}"], [data-tour-step-id*="${escapedTarget}"]`
|
|
53
|
+
);
|
|
54
|
+
return filterValidElements(Array.from(elements));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
57
|
+
logger.error(`Tour: Invalid fallback selector for "${target}"`, { error: err.message, target, stepId });
|
|
58
|
+
emitRuntimeError('Tour', `Invalid fallback selector: ${target}`, err);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
/**
|