@wix/ditto-codegen-public 1.0.180 → 1.0.182
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/dist/examples-apps/custom-element/countdown-widget/components/ColorPickerField.tsx +27 -0
- package/dist/examples-apps/custom-element/countdown-widget/components/FontPickerField.tsx +34 -0
- package/dist/examples-apps/custom-element/countdown-widget/components/Separator.tsx +10 -0
- package/dist/examples-apps/custom-element/countdown-widget/components/TimeBlock.tsx +23 -0
- package/dist/examples-apps/custom-element/countdown-widget/extensions.ts +18 -0
- package/dist/examples-apps/custom-element/countdown-widget/panel.tsx +146 -0
- package/dist/examples-apps/custom-element/countdown-widget/styles.ts +73 -0
- package/dist/examples-apps/custom-element/countdown-widget/utils.ts +46 -0
- package/dist/examples-apps/custom-element/countdown-widget/widget.tsx +97 -0
- package/dist/out.js +67 -7
- package/dist/wix-cli-templates/src/site/widgets/custom-elements/my-widget/panel.tsx +53 -0
- package/dist/wix-cli-templates/src/site/widgets/custom-elements/my-widget/widget.tsx +33 -4
- package/package.json +2 -2
- package/dist/examples-apps/custom-element/src/widgets/custom-elements/countdown-timer/widget.tsx +0 -493
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { type FC } from 'react';
|
|
2
|
+
import { inputs } from '@wix/editor';
|
|
3
|
+
import { FormField, Box, FillPreview, SidePanel } from '@wix/design-system';
|
|
4
|
+
|
|
5
|
+
interface ColorPickerFieldProps {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ColorPickerField: FC<ColorPickerFieldProps> = ({
|
|
12
|
+
label,
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
}) => (
|
|
16
|
+
<SidePanel.Field>
|
|
17
|
+
<FormField label={label}>
|
|
18
|
+
<Box width="30px" height="30px">
|
|
19
|
+
<FillPreview
|
|
20
|
+
fill={value}
|
|
21
|
+
onClick={() => inputs.selectColor(value, { onChange })}
|
|
22
|
+
/>
|
|
23
|
+
</Box>
|
|
24
|
+
</FormField>
|
|
25
|
+
</SidePanel.Field>
|
|
26
|
+
);
|
|
27
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { type FC } from 'react';
|
|
2
|
+
import { inputs } from '@wix/editor';
|
|
3
|
+
import { FormField, Button, Text, SidePanel } from '@wix/design-system';
|
|
4
|
+
|
|
5
|
+
interface FontValue {
|
|
6
|
+
font: string;
|
|
7
|
+
textDecoration: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FontPickerFieldProps {
|
|
11
|
+
label: string;
|
|
12
|
+
value: FontValue;
|
|
13
|
+
onChange: (value: FontValue) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FontPickerField: FC<FontPickerFieldProps> = ({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
}) => (
|
|
21
|
+
<SidePanel.Field>
|
|
22
|
+
<FormField label={label}>
|
|
23
|
+
<Button
|
|
24
|
+
size="small"
|
|
25
|
+
priority="secondary"
|
|
26
|
+
onClick={() => inputs.selectFont(value, { onChange })}
|
|
27
|
+
fullWidth
|
|
28
|
+
>
|
|
29
|
+
<Text size="small" ellipsis>Change Font</Text>
|
|
30
|
+
</Button>
|
|
31
|
+
</FormField>
|
|
32
|
+
</SidePanel.Field>
|
|
33
|
+
);
|
|
34
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { type FC } from 'react';
|
|
2
|
+
|
|
3
|
+
interface TimeBlockProps {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
numberStyle: React.CSSProperties;
|
|
7
|
+
labelStyle: React.CSSProperties;
|
|
8
|
+
blockStyle: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TimeBlock: FC<TimeBlockProps> = ({
|
|
12
|
+
value,
|
|
13
|
+
label,
|
|
14
|
+
numberStyle,
|
|
15
|
+
labelStyle,
|
|
16
|
+
blockStyle,
|
|
17
|
+
}) => (
|
|
18
|
+
<div style={blockStyle}>
|
|
19
|
+
<div style={numberStyle}>{value}</div>
|
|
20
|
+
<div style={labelStyle}>{label}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { extensions } from '@wix/astro/builders';
|
|
2
|
+
export const sitewidgetcountdownWidget = extensions.customElement({
|
|
3
|
+
id: '54db089e-aa5b-436a-9dfa-074f2efad2ce',
|
|
4
|
+
name: 'Countdown Widget',
|
|
5
|
+
tagName: 'countdown-widget',
|
|
6
|
+
element: './site/widgets/custom-elements/countdown-widget/widget.tsx',
|
|
7
|
+
settings: './site/widgets/custom-elements/countdown-widget/panel.tsx',
|
|
8
|
+
installation: {
|
|
9
|
+
autoAdd: true
|
|
10
|
+
},
|
|
11
|
+
width: {
|
|
12
|
+
defaultWidth: 500,
|
|
13
|
+
allowStretch: true
|
|
14
|
+
},
|
|
15
|
+
height: {
|
|
16
|
+
defaultHeight: 500
|
|
17
|
+
}
|
|
18
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { type FC, useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { widget } from "@wix/editor";
|
|
3
|
+
import {
|
|
4
|
+
SidePanel,
|
|
5
|
+
WixDesignSystemProvider,
|
|
6
|
+
Input,
|
|
7
|
+
FormField,
|
|
8
|
+
TimeInput,
|
|
9
|
+
Box,
|
|
10
|
+
} from "@wix/design-system";
|
|
11
|
+
import "@wix/design-system/styles.global.css";
|
|
12
|
+
import { ColorPickerField } from "./components/ColorPickerField";
|
|
13
|
+
import { FontPickerField } from "./components/FontPickerField";
|
|
14
|
+
import { parseTimeValue } from "./utils";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_BG_COLOR = "#0a0e27";
|
|
17
|
+
const DEFAULT_TEXT_COLOR = "#00ff88";
|
|
18
|
+
const DEFAULT_TEXT_FONT = "";
|
|
19
|
+
const DEFAULT_TEXT_DECORATION = "";
|
|
20
|
+
|
|
21
|
+
const Panel: FC = () => {
|
|
22
|
+
const [title, setTitle] = useState<string>("Countdown");
|
|
23
|
+
const [targetDate, setTargetDate] = useState<string>("");
|
|
24
|
+
const [targetTime, setTargetTime] = useState<string>("00:00");
|
|
25
|
+
const [bgColor, setBgColor] = useState<string>(DEFAULT_BG_COLOR);
|
|
26
|
+
const [textColor, setTextColor] = useState<string>(DEFAULT_TEXT_COLOR);
|
|
27
|
+
const [font, setFont] = useState({ font: DEFAULT_TEXT_FONT, textDecoration: DEFAULT_TEXT_DECORATION });
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
Promise.all([
|
|
31
|
+
widget.getProp("title"),
|
|
32
|
+
widget.getProp("target-date"),
|
|
33
|
+
widget.getProp("target-time"),
|
|
34
|
+
widget.getProp("bg-color"),
|
|
35
|
+
widget.getProp("text-color"),
|
|
36
|
+
widget.getProp("font"),
|
|
37
|
+
])
|
|
38
|
+
.then(([titleVal, dateVal, timeVal, bgColorVal, textColorVal, fontString]) => {
|
|
39
|
+
setTitle(titleVal || "Countdown");
|
|
40
|
+
setTargetDate(dateVal || "");
|
|
41
|
+
setTargetTime(timeVal || "00:00");
|
|
42
|
+
setBgColor(bgColorVal || DEFAULT_BG_COLOR);
|
|
43
|
+
setTextColor(textColorVal || DEFAULT_TEXT_COLOR);
|
|
44
|
+
setFont(JSON.parse(fontString || "{}"));
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => console.error("Failed to fetch widget properties:", error));
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const handleTitleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
const newTitle = event.target.value;
|
|
51
|
+
setTitle(newTitle);
|
|
52
|
+
widget.setProp("title", newTitle);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const handleDateChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
56
|
+
const newDate = event.target.value;
|
|
57
|
+
setTargetDate(newDate);
|
|
58
|
+
widget.setProp("target-date", newDate);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const handleTimeChange = useCallback(({ date }: { date: Date }) => {
|
|
62
|
+
if (date) {
|
|
63
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
64
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
65
|
+
const newTime = `${hours}:${minutes}`;
|
|
66
|
+
setTargetTime(newTime);
|
|
67
|
+
widget.setProp("target-time", newTime);
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const handleBgColorChange = (value: string) => {
|
|
72
|
+
setBgColor(value);
|
|
73
|
+
widget.setProp("bg-color", value);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleTextColorChange = (value: string) => {
|
|
77
|
+
setTextColor(value);
|
|
78
|
+
widget.setProp("text-color", value);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleFontChange = (value: { font: string; textDecoration: string }) => {
|
|
82
|
+
setFont(value);
|
|
83
|
+
widget.setProp("font", JSON.stringify(value));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<WixDesignSystemProvider>
|
|
88
|
+
<SidePanel width="300" height="100vh">
|
|
89
|
+
<SidePanel.Header title="Countdown Settings" />
|
|
90
|
+
<SidePanel.Content noPadding stretchVertically>
|
|
91
|
+
<Box direction="vertical" gap="24px">
|
|
92
|
+
<SidePanel.Field>
|
|
93
|
+
<FormField label="Title" required>
|
|
94
|
+
<Input
|
|
95
|
+
type="text"
|
|
96
|
+
value={title}
|
|
97
|
+
onChange={handleTitleChange}
|
|
98
|
+
placeholder="Enter countdown title"
|
|
99
|
+
/>
|
|
100
|
+
</FormField>
|
|
101
|
+
</SidePanel.Field>
|
|
102
|
+
|
|
103
|
+
<SidePanel.Field>
|
|
104
|
+
<FormField label="Target Date" required>
|
|
105
|
+
<Input
|
|
106
|
+
type="date"
|
|
107
|
+
value={targetDate}
|
|
108
|
+
onChange={handleDateChange}
|
|
109
|
+
/>
|
|
110
|
+
</FormField>
|
|
111
|
+
</SidePanel.Field>
|
|
112
|
+
|
|
113
|
+
<SidePanel.Field>
|
|
114
|
+
<FormField label="Target Time" required>
|
|
115
|
+
<TimeInput
|
|
116
|
+
value={parseTimeValue(targetTime)}
|
|
117
|
+
onChange={handleTimeChange}
|
|
118
|
+
/>
|
|
119
|
+
</FormField>
|
|
120
|
+
</SidePanel.Field>
|
|
121
|
+
|
|
122
|
+
<ColorPickerField
|
|
123
|
+
label="Background Color"
|
|
124
|
+
value={bgColor}
|
|
125
|
+
onChange={handleBgColorChange}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
<ColorPickerField
|
|
129
|
+
label="Text Color"
|
|
130
|
+
value={textColor}
|
|
131
|
+
onChange={handleTextColorChange}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<FontPickerField
|
|
135
|
+
label="Text Font"
|
|
136
|
+
value={font}
|
|
137
|
+
onChange={handleFontChange}
|
|
138
|
+
/>
|
|
139
|
+
</Box>
|
|
140
|
+
</SidePanel.Content>
|
|
141
|
+
</SidePanel>
|
|
142
|
+
</WixDesignSystemProvider>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default Panel;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
interface StyleProps {
|
|
4
|
+
bgColor: string;
|
|
5
|
+
textColor: string;
|
|
6
|
+
textFont?: string;
|
|
7
|
+
textDecoration?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WidgetStyles {
|
|
11
|
+
wrapper: React.CSSProperties;
|
|
12
|
+
title: React.CSSProperties;
|
|
13
|
+
digits: React.CSSProperties;
|
|
14
|
+
block: React.CSSProperties;
|
|
15
|
+
number: React.CSSProperties;
|
|
16
|
+
label: React.CSSProperties;
|
|
17
|
+
separator: React.CSSProperties;
|
|
18
|
+
message: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createStyles({ bgColor, textColor, textFont, textDecoration }: StyleProps): WidgetStyles {
|
|
22
|
+
return {
|
|
23
|
+
wrapper: {
|
|
24
|
+
backgroundColor: bgColor,
|
|
25
|
+
padding: '24px 32px',
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
display: 'inline-block',
|
|
28
|
+
},
|
|
29
|
+
title: {
|
|
30
|
+
font: textFont || '600 24px sans-serif',
|
|
31
|
+
color: textColor,
|
|
32
|
+
textDecoration,
|
|
33
|
+
marginBottom: '16px',
|
|
34
|
+
},
|
|
35
|
+
digits: {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
gap: '16px',
|
|
40
|
+
},
|
|
41
|
+
block: {
|
|
42
|
+
display: 'flex',
|
|
43
|
+
flexDirection: 'column',
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
padding: '12px 16px',
|
|
46
|
+
},
|
|
47
|
+
number: {
|
|
48
|
+
font: textFont || '600 40px sans-serif',
|
|
49
|
+
color: textColor,
|
|
50
|
+
textDecoration,
|
|
51
|
+
lineHeight: 1,
|
|
52
|
+
},
|
|
53
|
+
label: {
|
|
54
|
+
fontSize: '12px',
|
|
55
|
+
marginTop: '8px',
|
|
56
|
+
color: textColor,
|
|
57
|
+
opacity: 0.7,
|
|
58
|
+
},
|
|
59
|
+
separator: {
|
|
60
|
+
font: textFont || '600 32px sans-serif',
|
|
61
|
+
color: textColor,
|
|
62
|
+
textDecoration,
|
|
63
|
+
lineHeight: 1,
|
|
64
|
+
opacity: 0.5,
|
|
65
|
+
marginBottom: '20px',
|
|
66
|
+
},
|
|
67
|
+
message: {
|
|
68
|
+
fontSize: '18px',
|
|
69
|
+
color: textColor,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface TimeRemaining {
|
|
2
|
+
days: number;
|
|
3
|
+
hours: number;
|
|
4
|
+
minutes: number;
|
|
5
|
+
seconds: number;
|
|
6
|
+
isExpired: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const INITIAL_TIME: TimeRemaining = {
|
|
10
|
+
days: 0,
|
|
11
|
+
hours: 0,
|
|
12
|
+
minutes: 0,
|
|
13
|
+
seconds: 0,
|
|
14
|
+
isExpired: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function calculateTimeRemaining(targetDate: string, targetTime: string): TimeRemaining {
|
|
18
|
+
const target = new Date(`${targetDate}T${targetTime}`);
|
|
19
|
+
const diff = target.getTime() - Date.now();
|
|
20
|
+
|
|
21
|
+
if (diff <= 0) {
|
|
22
|
+
return { ...INITIAL_TIME, isExpired: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
26
|
+
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
27
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
28
|
+
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
29
|
+
|
|
30
|
+
return { days, hours, minutes, seconds, isExpired: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function padNumber(value: number): string {
|
|
34
|
+
return String(value).padStart(2, '0');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseTimeValue(timeString: string): Date {
|
|
38
|
+
const [hours, minutes] = timeString.split(':').map(Number);
|
|
39
|
+
const date = new Date();
|
|
40
|
+
date.setHours(hours || 0);
|
|
41
|
+
date.setMinutes(minutes || 0);
|
|
42
|
+
date.setSeconds(0);
|
|
43
|
+
date.setMilliseconds(0);
|
|
44
|
+
return date;
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { type FC, useState, useEffect } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import reactToWebComponent from 'react-to-webcomponent';
|
|
4
|
+
import { TimeBlock } from './components/TimeBlock';
|
|
5
|
+
import { Separator } from './components/Separator';
|
|
6
|
+
import { calculateTimeRemaining, padNumber, INITIAL_TIME, type TimeRemaining } from './utils';
|
|
7
|
+
import { createStyles } from './styles';
|
|
8
|
+
|
|
9
|
+
interface WidgetProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
targetDate?: string;
|
|
12
|
+
targetTime?: string;
|
|
13
|
+
bgColor?: string;
|
|
14
|
+
textColor?: string;
|
|
15
|
+
font?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CustomElement: FC<WidgetProps> = ({
|
|
19
|
+
title = 'Countdown',
|
|
20
|
+
targetDate = '',
|
|
21
|
+
targetTime = '00:00',
|
|
22
|
+
bgColor = '#ffffff',
|
|
23
|
+
textColor = '#333333',
|
|
24
|
+
font = "{}",
|
|
25
|
+
}) => {
|
|
26
|
+
const { font: textFont, textDecoration } = JSON.parse(font);
|
|
27
|
+
const [time, setTime] = useState<TimeRemaining>(INITIAL_TIME);
|
|
28
|
+
const styles = createStyles({ bgColor, textColor, textFont, textDecoration });
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!targetDate) return;
|
|
32
|
+
|
|
33
|
+
const update = () => setTime(calculateTimeRemaining(targetDate, targetTime));
|
|
34
|
+
update();
|
|
35
|
+
|
|
36
|
+
const interval = setInterval(update, 1000);
|
|
37
|
+
return () => clearInterval(interval);
|
|
38
|
+
}, [targetDate, targetTime]);
|
|
39
|
+
|
|
40
|
+
if (!targetDate) {
|
|
41
|
+
return (
|
|
42
|
+
<div style={styles.wrapper}>
|
|
43
|
+
{title && <div style={styles.title}>{title}</div>}
|
|
44
|
+
<div style={styles.message}>Set a target date</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (time.isExpired) {
|
|
50
|
+
return (
|
|
51
|
+
<div style={styles.wrapper}>
|
|
52
|
+
{title && <div style={styles.title}>{title}</div>}
|
|
53
|
+
<div style={styles.message}>Event has started!</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timeUnits = [
|
|
59
|
+
{ value: time.days, label: 'Days' },
|
|
60
|
+
{ value: time.hours, label: 'Hours' },
|
|
61
|
+
{ value: time.minutes, label: 'Minutes' },
|
|
62
|
+
{ value: time.seconds, label: 'Seconds' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div style={styles.wrapper}>
|
|
67
|
+
{title && <div style={styles.title}>{title}</div>}
|
|
68
|
+
<div style={styles.digits}>
|
|
69
|
+
{timeUnits.map((unit, index) => (
|
|
70
|
+
<React.Fragment key={unit.label}>
|
|
71
|
+
{index > 0 && <Separator style={styles.separator} />}
|
|
72
|
+
<TimeBlock
|
|
73
|
+
value={padNumber(unit.value)}
|
|
74
|
+
label={unit.label}
|
|
75
|
+
numberStyle={styles.number}
|
|
76
|
+
labelStyle={styles.label}
|
|
77
|
+
blockStyle={styles.block}
|
|
78
|
+
/>
|
|
79
|
+
</React.Fragment>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const customElement = reactToWebComponent(CustomElement, React, ReactDOM, {
|
|
87
|
+
props: {
|
|
88
|
+
title: 'string',
|
|
89
|
+
targetDate: 'string',
|
|
90
|
+
targetTime: 'string',
|
|
91
|
+
bgColor: 'string',
|
|
92
|
+
textColor: 'string',
|
|
93
|
+
font: 'string',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default customElement;
|
package/dist/out.js
CHANGED
|
@@ -36567,6 +36567,12 @@ var require_CodegenAIProxyService = __commonJS({
|
|
|
36567
36567
|
maxRetries: 3,
|
|
36568
36568
|
temperature: 0
|
|
36569
36569
|
});
|
|
36570
|
+
if (response.finishReason === "length") {
|
|
36571
|
+
throw new ditto_codegen_types_12.GenerateObjectError(`Output truncated due to max output tokens limit. Agent: ${payload.agentName}`, {
|
|
36572
|
+
status: 413,
|
|
36573
|
+
statusText: "Output truncated - max tokens reached"
|
|
36574
|
+
});
|
|
36575
|
+
}
|
|
36570
36576
|
return response;
|
|
36571
36577
|
};
|
|
36572
36578
|
}
|
|
@@ -37449,7 +37455,23 @@ When the blueprint includes EMBEDDED_SCRIPT extensions:
|
|
|
37449
37455
|
* User-generated content (reviews, comments, submissions)
|
|
37450
37456
|
* Event logs (if explicitly requested for analytics/tracking purposes)
|
|
37451
37457
|
* Multi-record relational data that is NOT configuration
|
|
37452
|
-
- If in doubt whether something is "configuration" or "data": If it controls HOW the embedded script works or looks, it's configuration \u2192 use embedded script parameters
|
|
37458
|
+
- If in doubt whether something is "configuration" or "data": If it controls HOW the embedded script works or looks, it's configuration \u2192 use embedded script parameters
|
|
37459
|
+
|
|
37460
|
+
CRITICAL - SITE WIDGET SETTINGS vs CMS COLLECTIONS:
|
|
37461
|
+
When the blueprint includes SITE_WIDGET extensions:
|
|
37462
|
+
- SITE_WIDGET has a built-in settings panel (panel.tsx) that handles ALL widget configuration - dates, titles, colors, numbers, toggles, text, etc.
|
|
37463
|
+
- For SITE_WIDGET-ONLY blueprints (no DASHBOARD_PAGE or other extensions): return collections: [] - the widget panel handles everything
|
|
37464
|
+
- NEVER create collections to store widget configuration data - this is ALWAYS handled by the widget's settings panel
|
|
37465
|
+
- Only create collections for SITE_WIDGET when ALL of these conditions are met:
|
|
37466
|
+
* The blueprint ALSO includes a DASHBOARD_PAGE extension that needs to manage data the widget displays
|
|
37467
|
+
* OR the widget explicitly needs to display MULTIPLE records of user-generated/business data (not configuration)
|
|
37468
|
+
* AND the data is NOT configuration (dates, colors, settings, display options are ALWAYS configuration)
|
|
37469
|
+
- Examples of what is CONFIGURATION (use widget panel, NOT collections):
|
|
37470
|
+
* Target date/time for countdown \u2192 widget panel
|
|
37471
|
+
* Display colors, fonts, sizes \u2192 widget panel
|
|
37472
|
+
* Headlines, labels, messages \u2192 widget panel
|
|
37473
|
+
* Feature toggles, show/hide options \u2192 widget panel
|
|
37474
|
+
* Numeric settings (delays, intervals, thresholds) \u2192 widget panel`;
|
|
37453
37475
|
var cmsPlannerExample = `Blueprint indicates a handling fees system for products.
|
|
37454
37476
|
|
|
37455
37477
|
For the collections field, return:
|
|
@@ -37470,6 +37492,12 @@ For the collections field, return:
|
|
|
37470
37492
|
- Example (WRONG):
|
|
37471
37493
|
* Creating both embeddedScriptParameters AND a "popup-configurations" CMS collection - this is duplicate storage
|
|
37472
37494
|
- Only create CMS collections if there's genuinely new data needed beyond what's in embeddedScriptParameters (e.g., user-generated content, event logs if explicitly requested, business entities)
|
|
37495
|
+
|
|
37496
|
+
Avoid Duplicate Data Storage - SITE WIDGET CONFIGURATION:
|
|
37497
|
+
- Site widgets have a built-in settings panel (panel.tsx) - ALL widget configuration goes there
|
|
37498
|
+
- For SITE_WIDGET-ONLY blueprints: ALWAYS return collections: [] (empty array)
|
|
37499
|
+
- The widget panel handles: dates, times, colors, text, numbers, toggles, display settings, countdown targets, etc.
|
|
37500
|
+
- ONLY create collections for site widgets when there's a DASHBOARD_PAGE that manages multi-record data the widget displays
|
|
37473
37501
|
5) Initial Data Assessment:
|
|
37474
37502
|
- After defining the collection schema, evaluate if initial/seed data can be implied from the blueprint
|
|
37475
37503
|
- Consider if the blueprint describes example data, default configurations, sample content, or starter records that would help demonstrate the app's functionality
|
|
@@ -64449,6 +64477,7 @@ var require_dashboard_page_instructions = __commonJS({
|
|
|
64449
64477
|
"Page.Section",
|
|
64450
64478
|
"RichTextInputArea",
|
|
64451
64479
|
"SectionHeader",
|
|
64480
|
+
"SidePanel",
|
|
64452
64481
|
"Table",
|
|
64453
64482
|
"TableActionCell",
|
|
64454
64483
|
"TableListHeader",
|
|
@@ -64598,7 +64627,14 @@ var require_load_examples = __commonJS({
|
|
|
64598
64627
|
description: "A Custom Element that displays a countdown timer",
|
|
64599
64628
|
files: {
|
|
64600
64629
|
[types_1.ExtensionType.SITE_WIDGET]: [
|
|
64601
|
-
"custom-element/
|
|
64630
|
+
"custom-element/countdown-widget/components/ColorPickerField.tsx",
|
|
64631
|
+
"custom-element/countdown-widget/components/FontPickerField.tsx",
|
|
64632
|
+
"custom-element/countdown-widget/components/Separator.tsx",
|
|
64633
|
+
"custom-element/countdown-widget/components/TimeBlock.tsx",
|
|
64634
|
+
"custom-element/countdown-widget/panel.tsx",
|
|
64635
|
+
"custom-element/countdown-widget/styles.ts",
|
|
64636
|
+
"custom-element/countdown-widget/utils.ts",
|
|
64637
|
+
"custom-element/countdown-widget/widget.tsx"
|
|
64602
64638
|
]
|
|
64603
64639
|
}
|
|
64604
64640
|
},
|
|
@@ -65791,18 +65827,39 @@ var require_custom_element_instructions = __commonJS({
|
|
|
65791
65827
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
65792
65828
|
exports2.customElementInstructions = void 0;
|
|
65793
65829
|
var typescript_quality_guidelines_1 = require_typescript_quality_guidelines();
|
|
65830
|
+
var wdsPackage_1 = require_wdsPackage();
|
|
65831
|
+
var dashboard_page_instructions_1 = require_dashboard_page_instructions();
|
|
65794
65832
|
var customElementRole = `You are a senior Wix CLI App Developer. Your job is to produce a beautiful, production\u2011quality site component quickly and correctly.`;
|
|
65795
65833
|
var customElementImplementationGuidelines = `<rules>
|
|
65796
65834
|
- Return ONLY a JSON object matching the schema { path, content, type } (no prose, no markdown)
|
|
65797
65835
|
- type must be "typescript"; content must be the COMPLETE file source
|
|
65798
|
-
- Do NOT add dependencies; do NOT use @wix/design-system or @wix/wix-ui-icons-common
|
|
65799
65836
|
- Do NOT invent types/modules/props; use only what exists in the scaffold and standard libs
|
|
65800
65837
|
</rules>
|
|
65801
65838
|
|
|
65839
|
+
<architecture>
|
|
65840
|
+
Site widgets consist of two files:
|
|
65841
|
+
|
|
65842
|
+
1. **widget.tsx** - React component converted to Web Component
|
|
65843
|
+
- Import React, ReactDOM, and react-to-webcomponent
|
|
65844
|
+
- Define a Props interface for configurable properties (camelCase)
|
|
65845
|
+
- Create a React FC that renders the widget UI
|
|
65846
|
+
- Convert to web component using reactToWebComponent with props mapping
|
|
65847
|
+
- Props in reactToWebComponent config use camelCase keys with 'string' type
|
|
65848
|
+
- Use inline styles for styling
|
|
65849
|
+
|
|
65850
|
+
2. **panel.tsx** - Settings panel shown in the Wix Editor
|
|
65851
|
+
- Import React hooks, @wix/editor widget API, and @wix/design-system components
|
|
65852
|
+
- Import '@wix/design-system/styles.global.css' for design system styles
|
|
65853
|
+
- Use useState for each configurable property
|
|
65854
|
+
- Use useEffect to load initial values via widget.getProp('kebab-case-name')
|
|
65855
|
+
- Create onChange handlers that update local state AND call widget.setProp('kebab-case-name', value)
|
|
65856
|
+
- Wrap content in WixDesignSystemProvider > SidePanel > SidePanel.Content
|
|
65857
|
+
- Use ONLY WDS components listed in the <wds_reference> section
|
|
65858
|
+
</architecture>
|
|
65859
|
+
|
|
65802
65860
|
<engineering_guidelines>
|
|
65803
65861
|
- Implement a TypeScript custom element with proper typing and clear interfaces
|
|
65804
|
-
-
|
|
65805
|
-
- Name things clearly; avoid magic numbers; extract tiny helpers if it improves clarity
|
|
65862
|
+
- Ensure prop names are consistent between widget.tsx (camelCase) and panel.tsx (kebab-case)
|
|
65806
65863
|
</engineering_guidelines>
|
|
65807
65864
|
|
|
65808
65865
|
${typescript_quality_guidelines_1.typescriptQualityGuidelines}
|
|
@@ -65837,7 +65894,8 @@ var require_custom_element_instructions = __commonJS({
|
|
|
65837
65894
|
exports2.customElementInstructions = {
|
|
65838
65895
|
role: customElementRole,
|
|
65839
65896
|
implementationGuidelines: customElementImplementationGuidelines,
|
|
65840
|
-
implementationRequirements: customElementImplementationRequirements
|
|
65897
|
+
implementationRequirements: customElementImplementationRequirements,
|
|
65898
|
+
wdsReference: () => (0, wdsPackage_1.buildWdsSystemPrompt)(dashboard_page_instructions_1.supportedWDSComponents)
|
|
65841
65899
|
};
|
|
65842
65900
|
}
|
|
65843
65901
|
});
|
|
@@ -65857,7 +65915,7 @@ var require_custom_element_prompt = __commonJS({
|
|
|
65857
65915
|
apiNames,
|
|
65858
65916
|
useData,
|
|
65859
65917
|
useIteration
|
|
65860
|
-
}).withRole(custom_element_instructions_1.customElementInstructions.role).withContextExplanation().withCorePrinciples().withImplementationGuidelines(custom_element_instructions_1.customElementInstructions.implementationGuidelines).withApiDocs().withExamples();
|
|
65918
|
+
}).withRole(custom_element_instructions_1.customElementInstructions.role).withContextExplanation().withCorePrinciples().withImplementationGuidelines(custom_element_instructions_1.customElementInstructions.implementationGuidelines).withSection("wds_reference", custom_element_instructions_1.customElementInstructions.wdsReference()).withApiDocs().withExamples();
|
|
65861
65919
|
if (useData) {
|
|
65862
65920
|
systemPrompt.withSection("implementation_requirements", custom_element_instructions_1.customElementInstructions.implementationRequirements);
|
|
65863
65921
|
}
|
|
@@ -78557,11 +78615,13 @@ var require_extensionGenerators = __commonJS({
|
|
|
78557
78615
|
static createCustomElementData(id, name, scaffoldDir) {
|
|
78558
78616
|
const kebabCaseComponentName = (0, ditto_scaffolding_2.toKebabCase)(name);
|
|
78559
78617
|
const componentPath = getScaffoldPath(scaffoldDir, "widget.tsx");
|
|
78618
|
+
const settingsPath = getScaffoldPath(scaffoldDir, "panel.tsx");
|
|
78560
78619
|
return {
|
|
78561
78620
|
id,
|
|
78562
78621
|
name,
|
|
78563
78622
|
tagName: kebabCaseComponentName,
|
|
78564
78623
|
element: componentPath,
|
|
78624
|
+
settings: settingsPath,
|
|
78565
78625
|
installation: { autoAdd: true },
|
|
78566
78626
|
width: { defaultWidth: 500, allowStretch: true },
|
|
78567
78627
|
height: { defaultHeight: 500 }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { type FC, useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { widget } from "@wix/editor";
|
|
3
|
+
import {
|
|
4
|
+
SidePanel,
|
|
5
|
+
WixDesignSystemProvider,
|
|
6
|
+
Input,
|
|
7
|
+
FormField,
|
|
8
|
+
} from "@wix/design-system";
|
|
9
|
+
import "@wix/design-system/styles.global.css";
|
|
10
|
+
|
|
11
|
+
const Panel: FC = () => {
|
|
12
|
+
const [displayName, setDisplayName] = useState<string>("");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
widget
|
|
16
|
+
.getProp("display-name")
|
|
17
|
+
.then((displayName) =>
|
|
18
|
+
setDisplayName(displayName || `Your Widget's Title`),
|
|
19
|
+
)
|
|
20
|
+
.catch((error) => console.error("Failed to fetch display-name:", error));
|
|
21
|
+
}, [setDisplayName]);
|
|
22
|
+
|
|
23
|
+
const handleDisplayNameChange = useCallback(
|
|
24
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
25
|
+
const newDisplayName = event.target.value;
|
|
26
|
+
setDisplayName(newDisplayName);
|
|
27
|
+
widget.setProp("display-name", newDisplayName);
|
|
28
|
+
},
|
|
29
|
+
[setDisplayName],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<WixDesignSystemProvider>
|
|
34
|
+
<SidePanel width="300" height="100vh">
|
|
35
|
+
<SidePanel.Content noPadding stretchVertically>
|
|
36
|
+
<SidePanel.Field>
|
|
37
|
+
<FormField label="Display Name">
|
|
38
|
+
<Input
|
|
39
|
+
type="text"
|
|
40
|
+
value={displayName}
|
|
41
|
+
onChange={handleDisplayNameChange}
|
|
42
|
+
aria-label="Display Name"
|
|
43
|
+
/>
|
|
44
|
+
</FormField>
|
|
45
|
+
</SidePanel.Field>
|
|
46
|
+
</SidePanel.Content>
|
|
47
|
+
</SidePanel>
|
|
48
|
+
</WixDesignSystemProvider>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default Panel;
|
|
53
|
+
|
|
@@ -1,5 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import React, { type FC } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import reactToWebComponent from 'react-to-webcomponent';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
displayName?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const CustomElement: FC<Props> = ({
|
|
10
|
+
displayName = `Your Widget's Title`,
|
|
11
|
+
}) => {
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h2>{displayName}</h2>
|
|
15
|
+
<hr />
|
|
16
|
+
<p>
|
|
17
|
+
This is a Site Widget generated by Wix CLI.<br />
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const customElement = reactToWebComponent(
|
|
24
|
+
CustomElement,
|
|
25
|
+
React,
|
|
26
|
+
ReactDOM as any,
|
|
27
|
+
{
|
|
28
|
+
props: {
|
|
29
|
+
displayName: 'string',
|
|
30
|
+
},
|
|
4
31
|
}
|
|
5
|
-
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export default customElement;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/ditto-codegen-public",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.182",
|
|
4
4
|
"description": "AI-powered Wix CLI app generator - standalone executable",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "node build.mjs",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"@wix/ditto-codegen": "1.0.0",
|
|
25
25
|
"esbuild": "^0.25.9"
|
|
26
26
|
},
|
|
27
|
-
"falconPackageHash": "
|
|
27
|
+
"falconPackageHash": "2d00f26181910908e52a2ac219529e18c316ff62684e562f64fec209"
|
|
28
28
|
}
|
package/dist/examples-apps/custom-element/src/widgets/custom-elements/countdown-timer/widget.tsx
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
import { items } from '@wix/data';
|
|
2
|
-
import { window as wixWindow } from '@wix/site-window';
|
|
3
|
-
|
|
4
|
-
interface CountdownConfig {
|
|
5
|
-
_id: string;
|
|
6
|
-
targetDate: Date;
|
|
7
|
-
title?: string;
|
|
8
|
-
isActive: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface TimeRemaining {
|
|
12
|
-
days: number;
|
|
13
|
-
hours: number;
|
|
14
|
-
minutes: number;
|
|
15
|
-
seconds: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default class extends HTMLElement {
|
|
19
|
-
private intervalId: number | null = null;
|
|
20
|
-
private config: CountdownConfig | null = null;
|
|
21
|
-
private isEditorMode = false;
|
|
22
|
-
|
|
23
|
-
public async connectedCallback(): Promise<void> {
|
|
24
|
-
await this.checkEnvironment();
|
|
25
|
-
if (this.isEditorMode) {
|
|
26
|
-
this.renderEditorPlaceholder();
|
|
27
|
-
} else {
|
|
28
|
-
await this.loadConfig();
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public disconnectedCallback(): void {
|
|
33
|
-
if (this.intervalId !== null) {
|
|
34
|
-
clearInterval(this.intervalId);
|
|
35
|
-
this.intervalId = null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private async checkEnvironment(): Promise<void> {
|
|
40
|
-
try {
|
|
41
|
-
const currentViewMode = await wixWindow.viewMode();
|
|
42
|
-
this.isEditorMode = currentViewMode === 'Editor';
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error('Failed to check view mode:', error);
|
|
45
|
-
this.isEditorMode = false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async loadConfig(): Promise<void> {
|
|
50
|
-
try {
|
|
51
|
-
const result = await items.query('countdown-config')
|
|
52
|
-
.eq('isActive', true)
|
|
53
|
-
.limit(1)
|
|
54
|
-
.find();
|
|
55
|
-
|
|
56
|
-
if (result.items.length > 0) {
|
|
57
|
-
const item = result.items[0];
|
|
58
|
-
this.config = {
|
|
59
|
-
_id: item._id as string,
|
|
60
|
-
targetDate: new Date(item.targetDate as string),
|
|
61
|
-
title: item.title as string | undefined,
|
|
62
|
-
isActive: item.isActive as boolean
|
|
63
|
-
};
|
|
64
|
-
this.render();
|
|
65
|
-
this.startCountdown();
|
|
66
|
-
} else {
|
|
67
|
-
this.renderNoConfig();
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error('Failed to load countdown config:', error);
|
|
71
|
-
this.renderError();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private calculateTimeRemaining(targetDate: Date): TimeRemaining | null {
|
|
76
|
-
const now = new Date().getTime();
|
|
77
|
-
const target = targetDate.getTime();
|
|
78
|
-
const difference = target - now;
|
|
79
|
-
|
|
80
|
-
if (difference <= 0) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
|
85
|
-
const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
86
|
-
const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
|
|
87
|
-
const seconds = Math.floor((difference % (1000 * 60)) / 1000);
|
|
88
|
-
|
|
89
|
-
return { days, hours, minutes, seconds };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private startCountdown(): void {
|
|
93
|
-
if (!this.config || !this.config.isActive) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.intervalId = window.setInterval(() => {
|
|
98
|
-
this.updateCountdown();
|
|
99
|
-
}, 1000);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private updateCountdown(): void {
|
|
103
|
-
if (!this.config) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const timeRemaining = this.calculateTimeRemaining(this.config.targetDate);
|
|
108
|
-
|
|
109
|
-
if (timeRemaining === null) {
|
|
110
|
-
this.renderExpired();
|
|
111
|
-
if (this.intervalId !== null) {
|
|
112
|
-
clearInterval(this.intervalId);
|
|
113
|
-
this.intervalId = null;
|
|
114
|
-
}
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
this.renderCountdown(timeRemaining);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private render(): void {
|
|
122
|
-
if (!this.config) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const timeRemaining = this.calculateTimeRemaining(this.config.targetDate);
|
|
127
|
-
|
|
128
|
-
if (timeRemaining === null) {
|
|
129
|
-
this.renderExpired();
|
|
130
|
-
} else {
|
|
131
|
-
this.renderCountdown(timeRemaining);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private getStyles(): string {
|
|
136
|
-
return `
|
|
137
|
-
<style>
|
|
138
|
-
/* Editor Placeholder Styles */
|
|
139
|
-
.editor-container {
|
|
140
|
-
font-family: 'Courier New', monospace;
|
|
141
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
142
|
-
color: #00ff88;
|
|
143
|
-
padding: 48px 24px;
|
|
144
|
-
border-radius: 16px;
|
|
145
|
-
text-align: center;
|
|
146
|
-
box-shadow: 0 8px 32px rgba(0, 255, 136, 0.2);
|
|
147
|
-
max-width: 600px;
|
|
148
|
-
margin: 0 auto;
|
|
149
|
-
border: 2px solid #00ff88;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.editor-icon {
|
|
153
|
-
font-size: 64px;
|
|
154
|
-
margin-bottom: 16px;
|
|
155
|
-
animation: pulse 2s ease-in-out infinite;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.editor-title {
|
|
159
|
-
font-size: 28px;
|
|
160
|
-
font-weight: 700;
|
|
161
|
-
margin: 0 0 12px 0;
|
|
162
|
-
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
|
163
|
-
letter-spacing: 2px;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.editor-message {
|
|
167
|
-
font-size: 16px;
|
|
168
|
-
opacity: 0.85;
|
|
169
|
-
margin: 0;
|
|
170
|
-
line-height: 1.6;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* Countdown Timer Styles */
|
|
174
|
-
.timer-container {
|
|
175
|
-
font-family: 'Orbitron', 'Rajdhani', 'Exo 2', monospace, sans-serif;
|
|
176
|
-
background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
|
|
177
|
-
color: #00d9ff;
|
|
178
|
-
padding: 40px 24px;
|
|
179
|
-
border-radius: 20px;
|
|
180
|
-
text-align: center;
|
|
181
|
-
box-shadow: 0 20px 60px rgba(0, 217, 255, 0.3), inset 0 0 40px rgba(0, 217, 255, 0.1);
|
|
182
|
-
max-width: 700px;
|
|
183
|
-
margin: 0 auto;
|
|
184
|
-
position: relative;
|
|
185
|
-
overflow: hidden;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.timer-glow {
|
|
189
|
-
content: '';
|
|
190
|
-
position: absolute;
|
|
191
|
-
top: -50%;
|
|
192
|
-
left: -50%;
|
|
193
|
-
width: 200%;
|
|
194
|
-
height: 200%;
|
|
195
|
-
background: radial-gradient(circle, rgba(0, 217, 255, 0.1) 0%, transparent 70%);
|
|
196
|
-
animation: rotate 10s linear infinite;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.timer-title {
|
|
200
|
-
font-size: 32px;
|
|
201
|
-
font-weight: 900;
|
|
202
|
-
margin: 0 0 12px 0;
|
|
203
|
-
text-shadow: 0 0 20px rgba(0, 217, 255, 0.8), 0 0 40px rgba(0, 217, 255, 0.4);
|
|
204
|
-
letter-spacing: 3px;
|
|
205
|
-
text-transform: uppercase;
|
|
206
|
-
position: relative;
|
|
207
|
-
z-index: 1;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.timer-grid {
|
|
211
|
-
display: grid;
|
|
212
|
-
grid-template-columns: repeat(4, 1fr);
|
|
213
|
-
gap: 20px;
|
|
214
|
-
margin: 32px 0;
|
|
215
|
-
position: relative;
|
|
216
|
-
z-index: 1;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.time-unit {
|
|
220
|
-
background: rgba(0, 217, 255, 0.15);
|
|
221
|
-
border-radius: 12px;
|
|
222
|
-
padding: 24px 12px;
|
|
223
|
-
backdrop-filter: blur(10px);
|
|
224
|
-
border: 2px solid rgba(0, 217, 255, 0.3);
|
|
225
|
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(0, 217, 255, 0.1);
|
|
226
|
-
transition: all 0.3s ease;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.time-unit:hover {
|
|
230
|
-
transform: translateY(-4px);
|
|
231
|
-
border-color: rgba(0, 217, 255, 0.6);
|
|
232
|
-
box-shadow: 0 12px 40px rgba(0, 217, 255, 0.4), inset 0 0 30px rgba(0, 217, 255, 0.2);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.time-number {
|
|
236
|
-
font-size: 48px;
|
|
237
|
-
font-weight: 900;
|
|
238
|
-
margin: 0;
|
|
239
|
-
text-shadow: 0 0 15px rgba(0, 217, 255, 0.8), 0 0 30px rgba(0, 217, 255, 0.4);
|
|
240
|
-
font-family: 'Orbitron', monospace;
|
|
241
|
-
letter-spacing: 2px;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.time-label {
|
|
245
|
-
font-size: 13px;
|
|
246
|
-
text-transform: uppercase;
|
|
247
|
-
letter-spacing: 2px;
|
|
248
|
-
margin: 8px 0 0 0;
|
|
249
|
-
opacity: 0.9;
|
|
250
|
-
font-weight: 700;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/* Expired State Styles */
|
|
254
|
-
.expired-container {
|
|
255
|
-
font-family: 'Bebas Neue', 'Anton', 'Oswald', sans-serif;
|
|
256
|
-
background: linear-gradient(135deg, #2d1b00 0%, #5c3a1f 50%, #8b4513 100%);
|
|
257
|
-
color: #ffa500;
|
|
258
|
-
padding: 48px 24px;
|
|
259
|
-
border-radius: 20px;
|
|
260
|
-
text-align: center;
|
|
261
|
-
box-shadow: 0 20px 60px rgba(255, 165, 0, 0.4), inset 0 0 40px rgba(255, 165, 0, 0.1);
|
|
262
|
-
max-width: 600px;
|
|
263
|
-
margin: 0 auto;
|
|
264
|
-
border: 3px solid #ffa500;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
.expired-icon {
|
|
268
|
-
font-size: 80px;
|
|
269
|
-
margin-bottom: 16px;
|
|
270
|
-
animation: bounce 1s ease-in-out infinite;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.expired-title {
|
|
274
|
-
font-size: 48px;
|
|
275
|
-
font-weight: 900;
|
|
276
|
-
margin: 0 0 16px 0;
|
|
277
|
-
text-shadow: 0 0 20px rgba(255, 165, 0, 0.8), 0 4px 8px rgba(0, 0, 0, 0.5);
|
|
278
|
-
letter-spacing: 4px;
|
|
279
|
-
text-transform: uppercase;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
.expired-message {
|
|
283
|
-
font-size: 20px;
|
|
284
|
-
margin: 0;
|
|
285
|
-
opacity: 0.95;
|
|
286
|
-
font-weight: 600;
|
|
287
|
-
letter-spacing: 1px;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/* No Config State Styles */
|
|
291
|
-
.no-config-container {
|
|
292
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
293
|
-
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
294
|
-
color: #2c3e50;
|
|
295
|
-
padding: 40px 24px;
|
|
296
|
-
border-radius: 16px;
|
|
297
|
-
text-align: center;
|
|
298
|
-
border: 2px dashed #95a5a6;
|
|
299
|
-
max-width: 500px;
|
|
300
|
-
margin: 0 auto;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.no-config-icon {
|
|
304
|
-
font-size: 64px;
|
|
305
|
-
margin-bottom: 16px;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.no-config-title {
|
|
309
|
-
font-size: 24px;
|
|
310
|
-
font-weight: 700;
|
|
311
|
-
margin: 0 0 12px 0;
|
|
312
|
-
color: #34495e;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
.no-config-message {
|
|
316
|
-
font-size: 16px;
|
|
317
|
-
margin: 0;
|
|
318
|
-
line-height: 1.6;
|
|
319
|
-
color: #7f8c8d;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/* Error State Styles */
|
|
323
|
-
.error-container {
|
|
324
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
325
|
-
background: #fff5f5;
|
|
326
|
-
color: #c53030;
|
|
327
|
-
padding: 32px 24px;
|
|
328
|
-
border-radius: 12px;
|
|
329
|
-
text-align: center;
|
|
330
|
-
border: 2px solid #fc8181;
|
|
331
|
-
max-width: 500px;
|
|
332
|
-
margin: 0 auto;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
.error-icon {
|
|
336
|
-
font-size: 48px;
|
|
337
|
-
margin-bottom: 12px;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
.error-message {
|
|
341
|
-
margin: 0;
|
|
342
|
-
font-size: 16px;
|
|
343
|
-
font-weight: 600;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/* Animations */
|
|
347
|
-
@keyframes pulse {
|
|
348
|
-
0%, 100% {
|
|
349
|
-
opacity: 1;
|
|
350
|
-
transform: scale(1);
|
|
351
|
-
}
|
|
352
|
-
50% {
|
|
353
|
-
opacity: 0.6;
|
|
354
|
-
transform: scale(1.1);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
@keyframes rotate {
|
|
359
|
-
from {
|
|
360
|
-
transform: rotate(0deg);
|
|
361
|
-
}
|
|
362
|
-
to {
|
|
363
|
-
transform: rotate(360deg);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
@keyframes bounce {
|
|
368
|
-
0%, 100% {
|
|
369
|
-
transform: translateY(0);
|
|
370
|
-
}
|
|
371
|
-
50% {
|
|
372
|
-
transform: translateY(-20px);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/* Responsive Styles */
|
|
377
|
-
@media (max-width: 600px) {
|
|
378
|
-
.timer-grid {
|
|
379
|
-
grid-template-columns: repeat(2, 1fr);
|
|
380
|
-
gap: 16px;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
.time-number {
|
|
384
|
-
font-size: 36px;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.timer-container {
|
|
388
|
-
padding: 24px 16px;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.timer-title {
|
|
392
|
-
font-size: 24px;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
</style>
|
|
396
|
-
`;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
private renderEditorPlaceholder(): void {
|
|
400
|
-
this.innerHTML = `
|
|
401
|
-
${this.getStyles()}
|
|
402
|
-
<div class="editor-container" data-testid="countdown-timer-editor">
|
|
403
|
-
<div class="editor-icon">⏱️</div>
|
|
404
|
-
<h2 class="editor-title">COUNTDOWN TIMER</h2>
|
|
405
|
-
<p class="editor-message">
|
|
406
|
-
This widget will display a live countdown on your published site.<br>
|
|
407
|
-
Configure the target date in your dashboard to get started.
|
|
408
|
-
</p>
|
|
409
|
-
</div>
|
|
410
|
-
`;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private renderCountdown(timeRemaining: TimeRemaining): void {
|
|
414
|
-
if (!this.config) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const timeUnits = [
|
|
419
|
-
{ value: timeRemaining.days, label: 'Days' },
|
|
420
|
-
{ value: timeRemaining.hours, label: 'Hours' },
|
|
421
|
-
{ value: timeRemaining.minutes, label: 'Minutes' },
|
|
422
|
-
{ value: timeRemaining.seconds, label: 'Seconds' }
|
|
423
|
-
];
|
|
424
|
-
|
|
425
|
-
const timeUnitsHtml = timeUnits
|
|
426
|
-
.map(unit => `
|
|
427
|
-
<div class="time-unit">
|
|
428
|
-
<div class="time-number">${unit.value.toString().padStart(2, '0')}</div>
|
|
429
|
-
<div class="time-label">${unit.label}</div>
|
|
430
|
-
</div>
|
|
431
|
-
`)
|
|
432
|
-
.join('');
|
|
433
|
-
|
|
434
|
-
const titleHtml = this.config.title
|
|
435
|
-
? `<h2 class="timer-title">${this.escapeHtml(this.config.title)}</h2>`
|
|
436
|
-
: '';
|
|
437
|
-
|
|
438
|
-
this.innerHTML = `
|
|
439
|
-
${this.getStyles()}
|
|
440
|
-
<div class="timer-container" data-testid="countdown-timer">
|
|
441
|
-
<div class="timer-glow"></div>
|
|
442
|
-
${titleHtml}
|
|
443
|
-
<div class="timer-grid">
|
|
444
|
-
${timeUnitsHtml}
|
|
445
|
-
</div>
|
|
446
|
-
</div>
|
|
447
|
-
`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
private renderExpired(): void {
|
|
451
|
-
const message = this.config && this.config.title
|
|
452
|
-
? `${this.escapeHtml(this.config.title)} has ended.`
|
|
453
|
-
: 'The countdown has ended.';
|
|
454
|
-
|
|
455
|
-
this.innerHTML = `
|
|
456
|
-
${this.getStyles()}
|
|
457
|
-
<div class="expired-container" data-testid="countdown-timer-expired">
|
|
458
|
-
<div class="expired-icon">🎉</div>
|
|
459
|
-
<h2 class="expired-title">Time's Up!</h2>
|
|
460
|
-
<p class="expired-message">${message}</p>
|
|
461
|
-
</div>
|
|
462
|
-
`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
private renderNoConfig(): void {
|
|
466
|
-
this.innerHTML = `
|
|
467
|
-
${this.getStyles()}
|
|
468
|
-
<div class="no-config-container" data-testid="countdown-timer-no-config">
|
|
469
|
-
<div class="no-config-icon">⚙️</div>
|
|
470
|
-
<h2 class="no-config-title">No Active Countdown</h2>
|
|
471
|
-
<p class="no-config-message">
|
|
472
|
-
Please configure a countdown timer in your dashboard to display it here.
|
|
473
|
-
</p>
|
|
474
|
-
</div>
|
|
475
|
-
`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private renderError(): void {
|
|
479
|
-
this.innerHTML = `
|
|
480
|
-
${this.getStyles()}
|
|
481
|
-
<div class="error-container" data-testid="countdown-timer-error">
|
|
482
|
-
<div class="error-icon">⚠️</div>
|
|
483
|
-
<p class="error-message">Unable to load countdown timer</p>
|
|
484
|
-
</div>
|
|
485
|
-
`;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private escapeHtml(text: string): string {
|
|
489
|
-
const div = document.createElement('div');
|
|
490
|
-
div.textContent = text;
|
|
491
|
-
return div.innerHTML;
|
|
492
|
-
}
|
|
493
|
-
}
|