slice-machine-ui 2.2.2-beta.8 → 2.3.0

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.
Files changed (95) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{18539003-4c4fdc1584f243b4.js → 18539003-afe9213a677bd9f0.js} +3 -3
  3. package/out/_next/static/chunks/212-b6a2b8d9d16ef3c5.js +1 -0
  4. package/out/_next/static/chunks/{210-565d01ee9777d1a1.js → 326-2d6d04d623de16c6.js} +2 -2
  5. package/out/_next/static/chunks/448-ae7b5e576a84b47d.js +1 -0
  6. package/out/_next/static/chunks/465-d5dd75edd8e6bb65.js +1 -0
  7. package/out/_next/static/chunks/574-a23212bd78e563de.js +1 -0
  8. package/out/_next/static/chunks/59b4e022-8e544a511d670317.js +1 -0
  9. package/out/_next/static/chunks/{756-0b198fb40a3e6ae2.js → 695-bfe7255199983590.js} +6 -4
  10. package/out/_next/static/chunks/816-17afd84d2642f4c4.js +1 -0
  11. package/out/_next/static/chunks/f36c6662-1f3a854183168b10.js +1 -0
  12. package/out/_next/static/chunks/pages/_app-07a2573662865a6b.js +547 -0
  13. package/out/_next/static/chunks/pages/changelog-1d15a7ef498f499a.js +1 -0
  14. package/out/_next/static/chunks/pages/changes-0af2a1a5244418f9.js +1 -0
  15. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-a6ff1618238b3510.js → [customTypeId]-b714ad9814898d23.js} +1 -1
  16. package/out/_next/static/chunks/pages/index-2610e550d79c5bb1.js +1 -0
  17. package/out/_next/static/chunks/pages/labs-240ba57bfdabe998.js +1 -0
  18. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-c300f963db847451.js → [pageTypeId]-aa881786f6e5aee2.js} +1 -1
  19. package/out/_next/static/chunks/pages/settings-039476a9a63d25a1.js +1 -0
  20. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-cfc19e3e799745bb.js +1 -0
  21. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-58e6cb0f278ea03c.js +1 -0
  22. package/out/_next/static/chunks/pages/slices-9894ce968d74baf6.js +1 -0
  23. package/out/_next/static/css/447984631bacb88c.css +1 -0
  24. package/out/_next/static/yi8ItcVBmEtxCSvLChc0b/_buildManifest.js +1 -0
  25. package/out/changelog.html +1 -1
  26. package/out/changes.html +1 -1
  27. package/out/custom-types/[customTypeId].html +1 -1
  28. package/out/custom-types.html +1 -1
  29. package/out/index.html +1 -1
  30. package/out/labs.html +1 -1
  31. package/out/page-types/[pageTypeId].html +1 -1
  32. package/out/settings.html +1 -1
  33. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  34. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  35. package/out/slices.html +1 -1
  36. package/package.json +9 -9
  37. package/src/domain/fields.ts +223 -0
  38. package/src/features/builder/AddFieldDropdown.tsx +122 -0
  39. package/src/features/onboarding/OnboardingGuide.tsx +45 -0
  40. package/src/features/onboarding/OnboardingProgressStepper.tsx +71 -0
  41. package/src/features/onboarding/OnboardingProvider.tsx +82 -0
  42. package/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialog.tsx +46 -0
  43. package/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent.tsx +38 -0
  44. package/src/features/onboarding/OnboardingStepDialog/index.ts +1 -0
  45. package/src/features/onboarding/content.tsx +99 -0
  46. package/src/features/onboarding/types.ts +32 -0
  47. package/src/features/onboarding/useOnboardingExperiment.ts +6 -0
  48. package/src/hooks/usePersistedState.ts +20 -3
  49. package/src/legacy/components/Navigation/index.tsx +2 -0
  50. package/src/legacy/lib/builders/CustomTypeBuilder/TabZone/index.tsx +9 -9
  51. package/src/legacy/lib/builders/SliceBuilder/FieldZones/index.tsx +7 -9
  52. package/src/legacy/lib/builders/SliceBuilder/VariationsList/index.tsx +1 -0
  53. package/src/legacy/lib/builders/common/Zone/components/ZoneEmptyState/ZoneEmptyState.tsx +4 -13
  54. package/src/legacy/lib/builders/common/Zone/index.jsx +26 -31
  55. package/src/legacy/lib/models/common/widgets/Boolean/index.ts +2 -4
  56. package/src/legacy/lib/models/common/widgets/Color/index.ts +2 -4
  57. package/src/legacy/lib/models/common/widgets/ContentRelationship/index.ts +0 -2
  58. package/src/legacy/lib/models/common/widgets/Date/index.ts +0 -2
  59. package/src/legacy/lib/models/common/widgets/Embed/index.ts +2 -4
  60. package/src/legacy/lib/models/common/widgets/GeoPoint/index.ts +2 -4
  61. package/src/legacy/lib/models/common/widgets/Group/ListItem/index.jsx +25 -19
  62. package/src/legacy/lib/models/common/widgets/Group/createGroupWidget.ts +2 -4
  63. package/src/legacy/lib/models/common/widgets/Group/index.tsx +8 -8
  64. package/src/legacy/lib/models/common/widgets/Image/index.ts +2 -4
  65. package/src/legacy/lib/models/common/widgets/Link/index.ts +2 -4
  66. package/src/legacy/lib/models/common/widgets/LinkToMedia/index.ts +2 -4
  67. package/src/legacy/lib/models/common/widgets/NestedGroup/index.tsx +7 -7
  68. package/src/legacy/lib/models/common/widgets/Number/index.ts +2 -4
  69. package/src/legacy/lib/models/common/widgets/Select/index.ts +2 -4
  70. package/src/legacy/lib/models/common/widgets/StructuredText/index.ts +0 -2
  71. package/src/legacy/lib/models/common/widgets/Text/index.ts +0 -2
  72. package/src/legacy/lib/models/common/widgets/Timestamp/index.ts +2 -4
  73. package/src/legacy/lib/models/common/widgets/UID/index.ts +2 -4
  74. package/src/legacy/lib/models/common/widgets/Widget.ts +0 -2
  75. package/out/_next/static/8ioxR-A_Nfflu3QxG8paB/_buildManifest.js +0 -1
  76. package/out/_next/static/chunks/212-58a99ab312f8a03b.js +0 -1
  77. package/out/_next/static/chunks/448-7a6843206737d199.js +0 -1
  78. package/out/_next/static/chunks/465-695ac5fedef986eb.js +0 -1
  79. package/out/_next/static/chunks/574-dfeb0e6aa3079da1.js +0 -1
  80. package/out/_next/static/chunks/59b4e022-7a3b5554b806349f.js +0 -1
  81. package/out/_next/static/chunks/816-96fc9a79308019c7.js +0 -1
  82. package/out/_next/static/chunks/f36c6662-03bb5e9d9aa0b0aa.js +0 -1
  83. package/out/_next/static/chunks/pages/_app-0f7eec1bd89de90c.js +0 -536
  84. package/out/_next/static/chunks/pages/changelog-f0cd26a9ae095e9e.js +0 -1
  85. package/out/_next/static/chunks/pages/changes-1a57f90433a0b5e2.js +0 -1
  86. package/out/_next/static/chunks/pages/index-a0530bd55c7e1872.js +0 -1
  87. package/out/_next/static/chunks/pages/labs-7b0abeef69494aec.js +0 -1
  88. package/out/_next/static/chunks/pages/settings-f82286dde10f624c.js +0 -1
  89. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]/simulator-453f946e8879c829.js +0 -1
  90. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/[variation]-b530a5e0ac9d0391.js +0 -1
  91. package/out/_next/static/chunks/pages/slices-7411822d74f1f6c1.js +0 -1
  92. package/out/_next/static/css/ebf74ed654df9202.css +0 -1
  93. package/src/legacy/lib/builders/common/SelectFieldTypeModal/FieldTypeCard.jsx +0 -51
  94. package/src/legacy/lib/builders/common/SelectFieldTypeModal/index.jsx +0 -84
  95. /package/out/_next/static/{8ioxR-A_Nfflu3QxG8paB → yi8ItcVBmEtxCSvLChc0b}/_ssgManifest.js +0 -0
@@ -0,0 +1,223 @@
1
+ import { IconName } from "@prismicio/editor-ui";
2
+ import {
3
+ FieldType,
4
+ NestableFieldTypes,
5
+ } from "@prismicio/types-internal/lib/customtypes";
6
+
7
+ interface BaseField {
8
+ description: string;
9
+ icon: IconName;
10
+ name: string;
11
+ thumbnail: string;
12
+ type: FieldType;
13
+ variant?: FieldVariant;
14
+ }
15
+
16
+ export type NestableFieldVariant = "ContentRelationship" | "LinkToMedia";
17
+ export type GroupFieldVariant = "NestedGroup";
18
+ export type FieldVariant = NestableFieldVariant | GroupFieldVariant;
19
+
20
+ export interface NestableField extends BaseField {
21
+ type: NestableFieldTypes;
22
+ variant?: NestableFieldVariant;
23
+ }
24
+
25
+ export interface UIDField extends BaseField {
26
+ type: "UID";
27
+ variant?: never;
28
+ }
29
+
30
+ export interface GroupField extends BaseField {
31
+ type: "Group";
32
+ variant?: "NestedGroup";
33
+ }
34
+
35
+ export type Field = NestableField | UIDField | GroupField;
36
+
37
+ /**
38
+ * Nestable fields
39
+ */
40
+
41
+ export const booleanField: NestableField = {
42
+ name: "Boolean",
43
+ description: "A true or false toggle.",
44
+ icon: "toggleOff",
45
+ thumbnail:
46
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295526/DEV_TOOLS/SM_FIELDS/Type_Boolean_cbwg8k.png",
47
+ type: "Boolean",
48
+ };
49
+
50
+ export const colorField: NestableField = {
51
+ name: "Color",
52
+ description: "A color in hex format.",
53
+ icon: "colorLens",
54
+ thumbnail:
55
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295521/DEV_TOOLS/SM_FIELDS/Type_Color_nyoeit.png",
56
+ type: "Color",
57
+ };
58
+
59
+ export const dateField: NestableField = {
60
+ name: "Date",
61
+ description: "A date without time.",
62
+ icon: "dateRange",
63
+ thumbnail:
64
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295525/DEV_TOOLS/SM_FIELDS/Type_Date_dsuaeq.png",
65
+ type: "Date",
66
+ };
67
+
68
+ export const embedField: NestableField = {
69
+ name: "Embed",
70
+ description: "A video, song, or oEmbed link.",
71
+ icon: "code",
72
+ thumbnail:
73
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295523/DEV_TOOLS/SM_FIELDS/Type_Embed_wwssvd.png",
74
+ type: "Embed",
75
+ };
76
+
77
+ export const geoPointField: NestableField = {
78
+ name: "Geopoint",
79
+ description: "A geographical coordinate.",
80
+ icon: "place",
81
+ thumbnail:
82
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295522/DEV_TOOLS/SM_FIELDS/Type_GeoPoint_lpq0sm.png",
83
+ type: "GeoPoint",
84
+ };
85
+
86
+ export const imageField: NestableField = {
87
+ name: "Image",
88
+ description: "A responsive image.",
89
+ icon: "image",
90
+ thumbnail:
91
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295520/DEV_TOOLS/SM_FIELDS/Type_Image_zyatxy.png",
92
+ type: "Image",
93
+ };
94
+
95
+ export const LinkField: NestableField = {
96
+ name: "Link",
97
+ description: "A link to a website, asset, or document.",
98
+ icon: "link",
99
+ thumbnail:
100
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295528/DEV_TOOLS/SM_FIELDS/Type_Link_bgslgy.png",
101
+ type: "Link",
102
+ };
103
+
104
+ export const contentRelationshipField: NestableField = {
105
+ name: "Content Relationship",
106
+ description: "A reference to a Prismic document.",
107
+ icon: "settingsEthernet",
108
+ thumbnail:
109
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295527/DEV_TOOLS/SM_FIELDS/Type_Content_RelationShip_s4z4nh.png",
110
+ type: "Link",
111
+ variant: "ContentRelationship",
112
+ };
113
+
114
+ export const LinkToMediaField: NestableField = {
115
+ name: "Link To Media",
116
+ description: "A link to a media asset.",
117
+ icon: "attachFile",
118
+ thumbnail:
119
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295519/DEV_TOOLS/SM_FIELDS/Type_LinkToMedia_m2p0ce.png",
120
+ type: "Link",
121
+ variant: "LinkToMedia",
122
+ };
123
+
124
+ export const numberField: NestableField = {
125
+ name: "Number",
126
+ description: "An integer or float.",
127
+ icon: "pin",
128
+ thumbnail:
129
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295522/DEV_TOOLS/SM_FIELDS/Type_Number_o3olop.png",
130
+ type: "Number",
131
+ };
132
+
133
+ export const selectField: NestableField = {
134
+ name: "Select",
135
+ description: "A dropdown of options.",
136
+ icon: "arrowDropDownCircle",
137
+ thumbnail:
138
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295527/DEV_TOOLS/SM_FIELDS/Type_Select_bedjei.png",
139
+ type: "Select",
140
+ };
141
+
142
+ export const richTextField: NestableField = {
143
+ name: "Rich Text",
144
+ description: "Text with formatting options.",
145
+ icon: "textFields",
146
+ thumbnail:
147
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295530/DEV_TOOLS/SM_FIELDS/Type_Rich_Text_fxdyar.png",
148
+ type: "StructuredText",
149
+ };
150
+
151
+ export const textField: NestableField = {
152
+ name: "Text",
153
+ description: "A text string without formatting.",
154
+ icon: "title",
155
+ thumbnail:
156
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295529/DEV_TOOLS/SM_FIELDS/Type_Key_Text_xrqf45.png",
157
+ type: "Text",
158
+ };
159
+
160
+ export const timestampField: NestableField = {
161
+ name: "Timestamp",
162
+ description: "A date and time.",
163
+ icon: "schedule",
164
+ thumbnail:
165
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295524/DEV_TOOLS/SM_FIELDS/Type_Timestamp_fkr5iw.png",
166
+ type: "Timestamp",
167
+ };
168
+
169
+ export const nestableFields: NestableField[] = [
170
+ imageField,
171
+ textField,
172
+ richTextField,
173
+ booleanField,
174
+ numberField,
175
+ selectField,
176
+ LinkField,
177
+ LinkToMediaField,
178
+ colorField,
179
+ dateField,
180
+ timestampField,
181
+ embedField,
182
+ geoPointField,
183
+ contentRelationshipField,
184
+ ];
185
+
186
+ /**
187
+ * UID
188
+ */
189
+
190
+ export const UIDField: UIDField = {
191
+ name: "UID",
192
+ description: "Unique Identifier",
193
+ icon: "tag",
194
+ thumbnail:
195
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721295519/DEV_TOOLS/SM_FIELDS/Type_UID_hrwzug.png",
196
+ type: "UID",
197
+ };
198
+
199
+ /**
200
+ * Group
201
+ */
202
+
203
+ export const groupField: GroupField = {
204
+ name: "Repeatable Group",
205
+ description: "A set of fields that editors can repeat.",
206
+ icon: "createNewFolder",
207
+ thumbnail:
208
+ "https://res.cloudinary.com/dmtf1daqp/image/upload/v1721314577/DEV_TOOLS/SM_FIELDS/Type_Group_uwwco0.png",
209
+ type: "Group",
210
+ };
211
+
212
+ export const nestedGroupField: GroupField = {
213
+ ...groupField,
214
+ variant: "NestedGroup",
215
+ };
216
+
217
+ export const groupFields: GroupField[] = [groupField, nestedGroupField];
218
+
219
+ /**
220
+ * All fields
221
+ */
222
+
223
+ export const fields = [...nestableFields, UIDField, ...groupFields];
@@ -0,0 +1,122 @@
1
+ import {
2
+ Button,
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuLabel,
7
+ DropdownMenuTrigger,
8
+ Icon,
9
+ Image,
10
+ Text,
11
+ Tooltip,
12
+ } from "@prismicio/editor-ui";
13
+ import { FieldType } from "@prismicio/types-internal/lib/customtypes";
14
+
15
+ import {
16
+ Field,
17
+ FieldVariant,
18
+ groupField,
19
+ nestableFields,
20
+ UIDField,
21
+ } from "@/domain/fields";
22
+
23
+ interface AddFieldDropdownProps {
24
+ disabled: boolean;
25
+ onSelectField: (fieldType: FieldType | FieldVariant) => void;
26
+ fields: Field[];
27
+ triggerDataTestId?: string;
28
+ }
29
+
30
+ export function AddFieldDropdown(props: AddFieldDropdownProps) {
31
+ const {
32
+ disabled,
33
+ onSelectField,
34
+ fields,
35
+ triggerDataTestId = "add-field",
36
+ } = props;
37
+
38
+ const singleFieldsToRender = fields.filter(
39
+ (field) =>
40
+ nestableFields.some(
41
+ (nestableField) => nestableField.name === field.name,
42
+ ) || field.name === UIDField.name,
43
+ );
44
+ const groupFieldToRender = fields.find(
45
+ (field) => field.name === groupField.name,
46
+ );
47
+
48
+ return (
49
+ <DropdownMenu>
50
+ <DropdownMenuTrigger disabled={disabled}>
51
+ <Button startIcon="add" color="grey" data-testid={triggerDataTestId}>
52
+ Add a field
53
+ </Button>
54
+ </DropdownMenuTrigger>
55
+ <DropdownMenuContent align="end" maxHeight={400} collisionPadding={8}>
56
+ <DropdownMenuLabel>Single fields</DropdownMenuLabel>
57
+ {singleFieldsToRender.map((field) => (
58
+ <AddFieldDropdownItem field={field} onSelectField={onSelectField} />
59
+ ))}
60
+
61
+ {groupFieldToRender && (
62
+ <>
63
+ <DropdownMenuLabel>Set of fields</DropdownMenuLabel>
64
+ {
65
+ <AddFieldDropdownItem
66
+ field={groupFieldToRender}
67
+ onSelectField={onSelectField}
68
+ />
69
+ }
70
+ </>
71
+ )}
72
+ </DropdownMenuContent>
73
+ </DropdownMenu>
74
+ );
75
+ }
76
+
77
+ interface AddFieldDropdownItemProps {
78
+ onSelectField: AddFieldDropdownProps["onSelectField"];
79
+ field: Field;
80
+ }
81
+
82
+ function AddFieldDropdownItem(props: AddFieldDropdownItemProps) {
83
+ const { field, onSelectField } = props;
84
+ const { type, variant, name, icon, description, thumbnail } = field;
85
+
86
+ return (
87
+ <DropdownMenuItem
88
+ key={`${type}${variant ? `-${variant}` : ""}`}
89
+ startIcon={<Icon name={icon} size="large" />}
90
+ description={description}
91
+ onSelect={() => onSelectField(variant ?? type)}
92
+ renderTooltip={(trigger) => (
93
+ <Tooltip
94
+ visible
95
+ variant="custom"
96
+ content={
97
+ <>
98
+ <Image
99
+ src={thumbnail}
100
+ sx={{
101
+ height: 170,
102
+ width: 278,
103
+ borderRadius: 4,
104
+ }}
105
+ animateOnLoad={false}
106
+ />
107
+ <Text color="white">How it will look for content editors.</Text>
108
+ </>
109
+ }
110
+ align="start"
111
+ side="left"
112
+ sideOffset={4}
113
+ animation="none"
114
+ >
115
+ {trigger}
116
+ </Tooltip>
117
+ )}
118
+ >
119
+ {name}
120
+ </DropdownMenuItem>
121
+ );
122
+ }
@@ -0,0 +1,45 @@
1
+ import { Card, CardContent, Text } from "@prismicio/editor-ui";
2
+
3
+ import { OnboardingProgressStepper } from "@/features/onboarding/OnboardingProgressStepper";
4
+ import {
5
+ OnboardingProvider,
6
+ useOnboardingContext,
7
+ } from "@/features/onboarding/OnboardingProvider";
8
+ import { useOnboardingExperiment } from "@/features/onboarding/useOnboardingExperiment";
9
+
10
+ const OnboardingGuideContent = () => {
11
+ const { steps, completedStepCount, isComplete } = useOnboardingContext();
12
+
13
+ if (isComplete) return null;
14
+
15
+ return (
16
+ <Card color="grey2" variant="outlined" paddingBlock={16}>
17
+ <CardContent>
18
+ <div>
19
+ <Text variant="bold" color="grey12">
20
+ Welcome
21
+ </Text>
22
+ <Text color="grey11" variant="small">
23
+ Get started in {steps.length} steps
24
+ </Text>
25
+ </div>
26
+ <Text color="grey11" variant="small" align="end">
27
+ {completedStepCount}/{steps.length}
28
+ </Text>
29
+ <OnboardingProgressStepper />
30
+ </CardContent>
31
+ </Card>
32
+ );
33
+ };
34
+
35
+ export const OnboardingGuide = () => {
36
+ const { eligible } = useOnboardingExperiment();
37
+
38
+ if (!eligible) return null;
39
+
40
+ return (
41
+ <OnboardingProvider>
42
+ <OnboardingGuideContent />
43
+ </OnboardingProvider>
44
+ );
45
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ Button,
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuLabel,
7
+ DropdownMenuTrigger,
8
+ Icon,
9
+ } from "@prismicio/editor-ui";
10
+ import { useState } from "react";
11
+
12
+ import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider";
13
+ import { OnboardingStepDialog } from "@/features/onboarding/OnboardingStepDialog";
14
+ import type { OnboardingStep } from "@/features/onboarding/types";
15
+
16
+ const EndCtaIcon = () => <Icon name="playCircle" size="small" color="grey11" />;
17
+
18
+ export const OnboardingProgressStepper = () => {
19
+ const { completedStepCount, steps, isStepComplete } = useOnboardingContext();
20
+
21
+ const [isDialogOpen, setDialogOpen] = useState(false);
22
+ const [activeStep, setActiveStep] = useState(steps[0]);
23
+
24
+ const showStep = (step: OnboardingStep) => {
25
+ setActiveStep(step);
26
+ setDialogOpen(true);
27
+ };
28
+
29
+ return (
30
+ <>
31
+ <OnboardingStepDialog
32
+ step={activeStep}
33
+ isOpen={isDialogOpen}
34
+ onClose={() => setDialogOpen(false)}
35
+ />
36
+ <DropdownMenu>
37
+ <DropdownMenuTrigger>
38
+ <Button
39
+ color="grey"
40
+ sx={{ width: "100%" }}
41
+ renderEndIcon={EndCtaIcon}
42
+ >
43
+ {completedStepCount > 0 ? "Continue" : "Start"}
44
+ </Button>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent align="start">
47
+ <DropdownMenuLabel>Progress</DropdownMenuLabel>
48
+ {steps.map((step) => {
49
+ const isCompleted = isStepComplete(step.id);
50
+
51
+ return (
52
+ <DropdownMenuItem
53
+ key={step.id}
54
+ onSelect={() => showStep(step)}
55
+ startIcon={
56
+ <Icon
57
+ color={isCompleted ? "green10" : "currentColor"}
58
+ name={isCompleted ? "checkBox" : "checkBoxOutlinedBlank"}
59
+ />
60
+ }
61
+ description={step.description}
62
+ >
63
+ {step.title}
64
+ </DropdownMenuItem>
65
+ );
66
+ })}
67
+ </DropdownMenuContent>
68
+ </DropdownMenu>
69
+ </>
70
+ );
71
+ };
@@ -0,0 +1,82 @@
1
+ import { createContext, ReactNode, useContext } from "react";
2
+
3
+ import { onboardingSteps as steps } from "@/features/onboarding/content";
4
+ import {
5
+ type OnboardingStep,
6
+ type OnboardingStepStatuses,
7
+ onboardingStepStatusesSchema,
8
+ type OnboardingStepType,
9
+ } from "@/features/onboarding/types";
10
+ import { usePersistedState } from "@/hooks/usePersistedState";
11
+
12
+ type OnboardingContext = {
13
+ steps: OnboardingStep[];
14
+ completedStepCount: number;
15
+ toggleStepComplete: (step: OnboardingStepType) => void;
16
+ getStepIndex: (stepId: OnboardingStepType) => number;
17
+ isStepComplete: (stepId: OnboardingStepType) => boolean;
18
+ isComplete: boolean;
19
+ };
20
+
21
+ export const OnboardingContext = createContext<OnboardingContext | undefined>(
22
+ undefined,
23
+ );
24
+
25
+ const getInitialState = (): OnboardingStepStatuses => {
26
+ // if the old guide was dismissed, all steps start as complete
27
+ const wasOldGuideDismissed =
28
+ localStorage.getItem("slice-machine_isInAppGuideOpen") === "false";
29
+
30
+ return Object.fromEntries(
31
+ steps.map((step) => [step.id, wasOldGuideDismissed]),
32
+ ) as OnboardingStepStatuses;
33
+ };
34
+
35
+ export const OnboardingProvider = ({ children }: { children: ReactNode }) => {
36
+ const [stepStatus, setStepStatus] = usePersistedState(
37
+ "onboardingSteps",
38
+ getInitialState(),
39
+ { schema: onboardingStepStatusesSchema },
40
+ );
41
+
42
+ const toggleStepComplete = (step: OnboardingStepType) => {
43
+ setStepStatus((current) => ({ ...current, [step]: !current[step] }));
44
+ };
45
+
46
+ const getStepIndex = (stepId: OnboardingStepType) => {
47
+ return steps.findIndex(({ id }) => id === stepId);
48
+ };
49
+
50
+ const isStepComplete = (stepId: OnboardingStepType) => {
51
+ return stepStatus[stepId] ?? false;
52
+ };
53
+
54
+ const completedStepCount = Object.values(stepStatus).filter(Boolean).length;
55
+
56
+ return (
57
+ <OnboardingContext.Provider
58
+ value={{
59
+ steps,
60
+ toggleStepComplete,
61
+ isStepComplete,
62
+ getStepIndex,
63
+ completedStepCount,
64
+ isComplete: completedStepCount === steps.length,
65
+ }}
66
+ >
67
+ {children}
68
+ </OnboardingContext.Provider>
69
+ );
70
+ };
71
+
72
+ export const useOnboardingContext = () => {
73
+ const context = useContext(OnboardingContext);
74
+
75
+ if (context == null) {
76
+ throw new Error(
77
+ "useOnboardingContext must be used within an OnboardingProvider",
78
+ );
79
+ }
80
+
81
+ return context;
82
+ };
@@ -0,0 +1,46 @@
1
+ import {
2
+ Dialog,
3
+ DialogActions,
4
+ DialogContent,
5
+ DialogHeader,
6
+ } from "@prismicio/editor-ui";
7
+
8
+ import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider";
9
+ import { OnboardingStepDialogContent } from "@/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent";
10
+ import type { OnboardingStep } from "@/features/onboarding/types";
11
+
12
+ type OnboardingStepDialogProps = {
13
+ step: OnboardingStep;
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ };
17
+
18
+ export const OnboardingStepDialog = ({
19
+ step,
20
+ isOpen,
21
+ onClose,
22
+ }: OnboardingStepDialogProps) => {
23
+ const { toggleStepComplete, isStepComplete } = useOnboardingContext();
24
+
25
+ const markAsDone = () => {
26
+ if (!isOpen) return;
27
+ toggleStepComplete(step.id);
28
+ onClose();
29
+ };
30
+
31
+ return (
32
+ <Dialog open={isOpen} onOpenChange={onClose}>
33
+ <DialogHeader title="Learn" />
34
+ <DialogContent>
35
+ <OnboardingStepDialogContent step={step} />
36
+ <DialogActions
37
+ ok={{
38
+ text: isStepComplete(step.id) ? "Undo step" : "Mark as done",
39
+ onClick: markAsDone,
40
+ }}
41
+ cancel={{ text: "Close" }}
42
+ />
43
+ </DialogContent>
44
+ </Dialog>
45
+ );
46
+ };
@@ -0,0 +1,38 @@
1
+ import { Box, ScrollArea, Text, Video } from "@prismicio/editor-ui";
2
+
3
+ import { getOnboardingStepsContent } from "@/features/onboarding/content";
4
+ import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider";
5
+ import { OnboardingStep } from "@/features/onboarding/types";
6
+ import { useRepositoryInformation } from "@/hooks/useRepositoryInformation";
7
+
8
+ type OnboardingStepDialogContentProps = {
9
+ step: OnboardingStep;
10
+ };
11
+
12
+ export const OnboardingStepDialogContent = ({
13
+ step,
14
+ }: OnboardingStepDialogContentProps) => {
15
+ const { getStepIndex } = useOnboardingContext();
16
+ const { repositoryUrl } = useRepositoryInformation();
17
+
18
+ const {
19
+ content: Content,
20
+ videoUrl,
21
+ title = step.title,
22
+ } = getOnboardingStepsContent({ repositoryUrl })[step.id];
23
+
24
+ return (
25
+ <ScrollArea>
26
+ <Box as="article" flexDirection="column" padding={16} gap={16}>
27
+ <section>
28
+ <Text sx={{ marginBottom: 4 }} color="purple9" variant="bold">
29
+ Step {getStepIndex(step.id) + 1}
30
+ </Text>
31
+ <Text variant="h3">{title}</Text>
32
+ <Content />
33
+ </section>
34
+ <Video src={videoUrl} sizing="contain" autoPlay loop />
35
+ </Box>
36
+ </ScrollArea>
37
+ );
38
+ };
@@ -0,0 +1 @@
1
+ export { OnboardingStepDialog } from "./OnboardingStepDialog";