@wix/ditto-codegen-public 1.0.181 → 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 +61 -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
|
@@ -37455,7 +37455,23 @@ When the blueprint includes EMBEDDED_SCRIPT extensions:
|
|
|
37455
37455
|
* User-generated content (reviews, comments, submissions)
|
|
37456
37456
|
* Event logs (if explicitly requested for analytics/tracking purposes)
|
|
37457
37457
|
* Multi-record relational data that is NOT configuration
|
|
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
|
|
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`;
|
|
37459
37475
|
var cmsPlannerExample = `Blueprint indicates a handling fees system for products.
|
|
37460
37476
|
|
|
37461
37477
|
For the collections field, return:
|
|
@@ -37476,6 +37492,12 @@ For the collections field, return:
|
|
|
37476
37492
|
- Example (WRONG):
|
|
37477
37493
|
* Creating both embeddedScriptParameters AND a "popup-configurations" CMS collection - this is duplicate storage
|
|
37478
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
|
|
37479
37501
|
5) Initial Data Assessment:
|
|
37480
37502
|
- After defining the collection schema, evaluate if initial/seed data can be implied from the blueprint
|
|
37481
37503
|
- Consider if the blueprint describes example data, default configurations, sample content, or starter records that would help demonstrate the app's functionality
|
|
@@ -64455,6 +64477,7 @@ var require_dashboard_page_instructions = __commonJS({
|
|
|
64455
64477
|
"Page.Section",
|
|
64456
64478
|
"RichTextInputArea",
|
|
64457
64479
|
"SectionHeader",
|
|
64480
|
+
"SidePanel",
|
|
64458
64481
|
"Table",
|
|
64459
64482
|
"TableActionCell",
|
|
64460
64483
|
"TableListHeader",
|
|
@@ -64604,7 +64627,14 @@ var require_load_examples = __commonJS({
|
|
|
64604
64627
|
description: "A Custom Element that displays a countdown timer",
|
|
64605
64628
|
files: {
|
|
64606
64629
|
[types_1.ExtensionType.SITE_WIDGET]: [
|
|
64607
|
-
"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"
|
|
64608
64638
|
]
|
|
64609
64639
|
}
|
|
64610
64640
|
},
|
|
@@ -65797,18 +65827,39 @@ var require_custom_element_instructions = __commonJS({
|
|
|
65797
65827
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
65798
65828
|
exports2.customElementInstructions = void 0;
|
|
65799
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();
|
|
65800
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.`;
|
|
65801
65833
|
var customElementImplementationGuidelines = `<rules>
|
|
65802
65834
|
- Return ONLY a JSON object matching the schema { path, content, type } (no prose, no markdown)
|
|
65803
65835
|
- type must be "typescript"; content must be the COMPLETE file source
|
|
65804
|
-
- Do NOT add dependencies; do NOT use @wix/design-system or @wix/wix-ui-icons-common
|
|
65805
65836
|
- Do NOT invent types/modules/props; use only what exists in the scaffold and standard libs
|
|
65806
65837
|
</rules>
|
|
65807
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
|
+
|
|
65808
65860
|
<engineering_guidelines>
|
|
65809
65861
|
- Implement a TypeScript custom element with proper typing and clear interfaces
|
|
65810
|
-
-
|
|
65811
|
-
- 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)
|
|
65812
65863
|
</engineering_guidelines>
|
|
65813
65864
|
|
|
65814
65865
|
${typescript_quality_guidelines_1.typescriptQualityGuidelines}
|
|
@@ -65843,7 +65894,8 @@ var require_custom_element_instructions = __commonJS({
|
|
|
65843
65894
|
exports2.customElementInstructions = {
|
|
65844
65895
|
role: customElementRole,
|
|
65845
65896
|
implementationGuidelines: customElementImplementationGuidelines,
|
|
65846
|
-
implementationRequirements: customElementImplementationRequirements
|
|
65897
|
+
implementationRequirements: customElementImplementationRequirements,
|
|
65898
|
+
wdsReference: () => (0, wdsPackage_1.buildWdsSystemPrompt)(dashboard_page_instructions_1.supportedWDSComponents)
|
|
65847
65899
|
};
|
|
65848
65900
|
}
|
|
65849
65901
|
});
|
|
@@ -65863,7 +65915,7 @@ var require_custom_element_prompt = __commonJS({
|
|
|
65863
65915
|
apiNames,
|
|
65864
65916
|
useData,
|
|
65865
65917
|
useIteration
|
|
65866
|
-
}).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();
|
|
65867
65919
|
if (useData) {
|
|
65868
65920
|
systemPrompt.withSection("implementation_requirements", custom_element_instructions_1.customElementInstructions.implementationRequirements);
|
|
65869
65921
|
}
|
|
@@ -78563,11 +78615,13 @@ var require_extensionGenerators = __commonJS({
|
|
|
78563
78615
|
static createCustomElementData(id, name, scaffoldDir) {
|
|
78564
78616
|
const kebabCaseComponentName = (0, ditto_scaffolding_2.toKebabCase)(name);
|
|
78565
78617
|
const componentPath = getScaffoldPath(scaffoldDir, "widget.tsx");
|
|
78618
|
+
const settingsPath = getScaffoldPath(scaffoldDir, "panel.tsx");
|
|
78566
78619
|
return {
|
|
78567
78620
|
id,
|
|
78568
78621
|
name,
|
|
78569
78622
|
tagName: kebabCaseComponentName,
|
|
78570
78623
|
element: componentPath,
|
|
78624
|
+
settings: settingsPath,
|
|
78571
78625
|
installation: { autoAdd: true },
|
|
78572
78626
|
width: { defaultWidth: 500, allowStretch: true },
|
|
78573
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
|
-
}
|