@thelol3882/sapar 0.0.0-stage → 0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -2
  3. package/dist/adapter-Y9yB3-q3.d.ts +14 -0
  4. package/dist/adapter-fFZPsINL.d.ts +14 -0
  5. package/dist/chunk-7B66QEMY.js +536 -0
  6. package/dist/chunk-7B66QEMY.js.map +1 -0
  7. package/dist/chunk-FM64UDKJ.js +893 -0
  8. package/dist/chunk-FM64UDKJ.js.map +1 -0
  9. package/dist/chunk-VC6XLWM3.js +176 -0
  10. package/dist/chunk-VC6XLWM3.js.map +1 -0
  11. package/dist/chunk-ZBVQGMR4.js +583 -0
  12. package/dist/chunk-ZBVQGMR4.js.map +1 -0
  13. package/dist/cli/index.js +1335 -0
  14. package/dist/core/index.d.ts +273 -0
  15. package/dist/core/index.js +71 -0
  16. package/dist/core/index.js.map +1 -0
  17. package/dist/expo/index.d.ts +189 -0
  18. package/dist/expo/index.js +39 -0
  19. package/dist/expo/index.js.map +1 -0
  20. package/dist/expo-router/index.d.ts +18 -0
  21. package/dist/expo-router/index.js +46 -0
  22. package/dist/expo-router/index.js.map +1 -0
  23. package/dist/next/index.d.ts +18 -0
  24. package/dist/next/index.js +49 -0
  25. package/dist/next/index.js.map +1 -0
  26. package/dist/presentation-CClsPRT6.d.ts +12 -0
  27. package/dist/react/index.d.ts +35 -0
  28. package/dist/react/index.js +29 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/rtk/index.d.ts +751 -0
  31. package/dist/rtk/index.js +114 -0
  32. package/dist/rtk/index.js.map +1 -0
  33. package/dist/stepCount-CTYYFWiV.d.ts +57 -0
  34. package/dist/types-CHD2pB5X.d.ts +192 -0
  35. package/dist/web/index.d.ts +112 -0
  36. package/dist/web/index.js +34 -0
  37. package/dist/web/index.js.map +1 -0
  38. package/package.json +147 -3
  39. package/templates/expo/AnnouncementModal.tsx +102 -0
  40. package/templates/expo/TourCard.tsx +112 -0
  41. package/templates/expo/WhatsNewButton.tsx +87 -0
  42. package/templates/expo/WhatsNewPanel.tsx +167 -0
  43. package/templates/mantine/AnnouncementModal.tsx +66 -0
  44. package/templates/mantine/TourCard.tsx +84 -0
  45. package/templates/mantine/WhatsNewButton.tsx +62 -0
  46. package/templates/mantine/WhatsNewPanel.tsx +129 -0
  47. package/templates/tailwind/AnnouncementModal.tsx +100 -0
  48. package/templates/tailwind/TourCard.tsx +69 -0
  49. package/templates/tailwind/WhatsNewButton.tsx +56 -0
  50. package/templates/tailwind/WhatsNewPanel.tsx +174 -0
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { Anchor, Button, Group, Image, Modal, Stack, Text } from '@mantine/core';
5
+ import { useAnnouncementInterruption, useWhatsNew } from '../../src/react';
6
+
7
+ export interface AnnouncementModalLabels {
8
+ acknowledge: string;
9
+ takeTour: string;
10
+ }
11
+
12
+ const DEFAULT_LABELS: AnnouncementModalLabels = {
13
+ acknowledge: 'Got it',
14
+ takeTour: 'Take the tour',
15
+ };
16
+
17
+ export function AnnouncementModal({ labels }: { labels?: Partial<AnnouncementModalLabels> }) {
18
+ const text = { ...DEFAULT_LABELS, ...labels };
19
+ const { announcement, dismiss, acknowledge } = useAnnouncementInterruption();
20
+ const { recordClick, startTour } = useWhatsNew();
21
+
22
+ if (!announcement) return null;
23
+ const { tourKey } = announcement;
24
+
25
+ async function takeTour(tourKey: string): Promise<void> {
26
+ await acknowledge();
27
+ await startTour(tourKey);
28
+ }
29
+
30
+ return (
31
+ <Modal
32
+ opened
33
+ centered
34
+ withCloseButton={false}
35
+ title={announcement.title}
36
+ onClose={() => void dismiss()}
37
+ >
38
+ <Stack gap="sm">
39
+ {announcement.imageUrl && <Image src={announcement.imageUrl} alt="" radius="md" />}
40
+ <Text size="sm" c="dimmed">
41
+ {announcement.body}
42
+ </Text>
43
+ {announcement.cta && (
44
+ <Anchor
45
+ href={announcement.cta.url}
46
+ target="_blank"
47
+ rel="noreferrer"
48
+ size="sm"
49
+ fw={500}
50
+ onClick={() => recordClick(announcement.key)}
51
+ >
52
+ {announcement.cta.label}
53
+ </Anchor>
54
+ )}
55
+ <Group justify="flex-end" mt="xs">
56
+ {tourKey && (
57
+ <Button variant="subtle" color="gray" onClick={() => void takeTour(tourKey)}>
58
+ {text.takeTour}
59
+ </Button>
60
+ )}
61
+ <Button onClick={() => void acknowledge()}>{text.acknowledge}</Button>
62
+ </Group>
63
+ </Stack>
64
+ </Modal>
65
+ );
66
+ }
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { Button, CloseButton, Group, Paper, Text, Title } from '@mantine/core';
5
+ import type { CSSProperties } from 'react';
6
+ import { TourOverlay, TourStep, type TourOverlayProps } from '../../src/web';
7
+ import type { StepAnnouncement, StepCount } from '../../src/react';
8
+
9
+ export interface TourCardLabels {
10
+ next: string;
11
+ back: string;
12
+ skip: string;
13
+ done: string;
14
+ close: string;
15
+ progress: ({ position, total }: StepCount) => string;
16
+ announceStep: ({ position, total, title }: StepAnnouncement) => string;
17
+ }
18
+
19
+ const DEFAULT_LABELS: TourCardLabels = {
20
+ next: 'Next',
21
+ back: 'Back',
22
+ skip: 'Skip tour',
23
+ done: 'Done',
24
+ close: 'Close tour',
25
+ progress: ({ position, total }) => `${position} / ${total}`,
26
+ announceStep: ({ position, total, title }) => `Step ${position} of ${total}: ${title}`,
27
+ };
28
+
29
+ const SPOTLIGHT_STYLE: CSSProperties = {
30
+ zIndex: 'var(--mantine-z-index-overlay)',
31
+ color: 'color-mix(in srgb, var(--mantine-color-black) 50%, transparent)',
32
+ };
33
+
34
+ const POPOVER_STYLE: CSSProperties = { zIndex: 'calc(var(--mantine-z-index-overlay) + 1)' };
35
+
36
+ export interface TourCardProps extends Pick<TourOverlayProps, 'presentation'> {
37
+ labels?: Partial<TourCardLabels>;
38
+ }
39
+
40
+ export function TourCard({ labels, presentation }: TourCardProps) {
41
+ const text = { ...DEFAULT_LABELS, ...labels };
42
+
43
+ return (
44
+ <TourOverlay spotlightStyle={SPOTLIGHT_STYLE} popoverStyle={POPOVER_STYLE} presentation={presentation}>
45
+ <Paper
46
+ withBorder
47
+ shadow="md"
48
+ radius="md"
49
+ p="md"
50
+ w={320}
51
+ maw="calc(100vw - 2rem)"
52
+ className="mantine-focus-auto"
53
+ renderRoot={(props) => <TourStep.Root {...props} announceStep={text.announceStep} />}
54
+ >
55
+ <Group justify="space-between" mb="xs">
56
+ <Text
57
+ size="xs"
58
+ fw={500}
59
+ c="dimmed"
60
+ renderRoot={(props) => <TourStep.Progress {...props}>{text.progress}</TourStep.Progress>}
61
+ />
62
+ <TourStep.Close aria-label={text.close} render={({ children: _icon, ...props }) => <CloseButton {...props} />} />
63
+ </Group>
64
+ <Title order={5} renderRoot={(props) => <TourStep.Title {...props} />} />
65
+ <Text size="sm" c="dimmed" mt={4} renderRoot={(props) => <TourStep.Body {...props} />} />
66
+ <Group justify="space-between" mt="md">
67
+ <TourStep.Skip render={(props) => <Button variant="subtle" color="gray" size="xs" {...props} />}>
68
+ {text.skip}
69
+ </TourStep.Skip>
70
+ <Group gap="xs">
71
+ <TourStep.Back render={(props) => <Button variant="subtle" color="gray" size="xs" {...props} />}>
72
+ {text.back}
73
+ </TourStep.Back>
74
+ <TourStep.Next
75
+ nextLabel={text.next}
76
+ doneLabel={text.done}
77
+ render={(props) => <Button size="xs" {...props} />}
78
+ />
79
+ </Group>
80
+ </Group>
81
+ </Paper>
82
+ </TourOverlay>
83
+ );
84
+ }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { ActionIcon, Indicator } from '@mantine/core';
5
+ import { useState } from 'react';
6
+ import { useWhatsNew } from '../../src/react';
7
+ import { WhatsNewPanel, type WhatsNewPanelProps } from './WhatsNewPanel';
8
+
9
+ const MAX_DISPLAYED_COUNT = 9;
10
+
11
+ export interface WhatsNewButtonProps extends Pick<WhatsNewPanelProps, 'labels' | 'locale' | 'formatDate'> {
12
+ className?: string;
13
+ /** Accessible name; the unread count is appended. */
14
+ label?: string;
15
+ }
16
+
17
+ function SparkleIcon() {
18
+ return (
19
+ <svg
20
+ viewBox="0 0 24 24"
21
+ fill="none"
22
+ stroke="currentColor"
23
+ strokeWidth="2"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ width="20"
27
+ height="20"
28
+ aria-hidden="true"
29
+ >
30
+ <path d="M9.9 15.5a2 2 0 0 0-1.4-1.4l-6.1-1.6a.5.5 0 0 1 0-1l6.1-1.6a2 2 0 0 0 1.4-1.4l1.6-6.1a.5.5 0 0 1 1 0l1.6 6.1a2 2 0 0 0 1.4 1.4l6.1 1.6a.5.5 0 0 1 0 1l-6.1 1.6a2 2 0 0 0-1.4 1.4l-1.6 6.1a.5.5 0 0 1-1 0zM20 3v4M22 5h-4M4 17v2M5 18H3" />
31
+ </svg>
32
+ );
33
+ }
34
+
35
+ export function WhatsNewButton({ className, label = "What's new", labels, locale, formatDate }: WhatsNewButtonProps) {
36
+ const [open, setOpen] = useState(false);
37
+ const { unreadCount } = useWhatsNew();
38
+
39
+ return (
40
+ <>
41
+ <Indicator
42
+ label={unreadCount > MAX_DISPLAYED_COUNT ? `${MAX_DISPLAYED_COUNT}+` : unreadCount}
43
+ disabled={unreadCount === 0}
44
+ size={16}
45
+ color="red"
46
+ >
47
+ <ActionIcon
48
+ variant="subtle"
49
+ color="gray"
50
+ size="lg"
51
+ radius="xl"
52
+ className={className}
53
+ aria-label={unreadCount > 0 ? `${label} (${unreadCount})` : label}
54
+ onClick={() => setOpen(true)}
55
+ >
56
+ <SparkleIcon />
57
+ </ActionIcon>
58
+ </Indicator>
59
+ <WhatsNewPanel open={open} onClose={() => setOpen(false)} labels={labels} locale={locale} formatDate={formatDate} />
60
+ </>
61
+ );
62
+ }
@@ -0,0 +1,129 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { Anchor, Badge, Button, Drawer, Group, Image, ScrollArea, Stack, Text, Title } from '@mantine/core';
5
+ import { useWhatsNewPanel, type WhatsNewPanelItem } from '../../src/react';
6
+
7
+ export interface WhatsNewPanelLabels {
8
+ title: string;
9
+ empty: string;
10
+ close: string;
11
+ takeTour: string;
12
+ new: string;
13
+ }
14
+
15
+ const DEFAULT_LABELS: WhatsNewPanelLabels = {
16
+ title: "What's new",
17
+ empty: 'Nothing new yet.',
18
+ close: 'Close',
19
+ takeTour: 'Take the tour',
20
+ new: 'New',
21
+ };
22
+
23
+ export interface WhatsNewPanelProps {
24
+ open: boolean;
25
+ onClose: () => void;
26
+ labels?: Partial<WhatsNewPanelLabels>;
27
+ /** BCP 47 tag for the default date format. Defaults to the browser locale. */
28
+ locale?: string;
29
+ /** Replaces the default date format; the template then never uses `Intl`. */
30
+ formatDate?: (isoDate: string) => string;
31
+ }
32
+
33
+ function formatDateWithIntl(isoDate: string, locale: string | undefined): string {
34
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(isoDate));
35
+ }
36
+
37
+ function Entry({
38
+ item,
39
+ labels,
40
+ formatDate,
41
+ onCtaClick,
42
+ onTakeTour,
43
+ }: {
44
+ item: WhatsNewPanelItem;
45
+ labels: WhatsNewPanelLabels;
46
+ formatDate: (isoDate: string) => string;
47
+ onCtaClick: (key: string) => void;
48
+ onTakeTour: (tourKey: string) => void;
49
+ }) {
50
+ const { tourKey } = item;
51
+
52
+ return (
53
+ <Stack component="li" gap="xs">
54
+ <Group gap="xs">
55
+ <Title order={4}>{item.title}</Title>
56
+ {item.isNew && <Badge size="sm">{labels.new}</Badge>}
57
+ </Group>
58
+ {item.publishedAt && (
59
+ <Text component="time" dateTime={item.publishedAt} size="xs" c="dimmed">
60
+ {formatDate(item.publishedAt)}
61
+ </Text>
62
+ )}
63
+ {item.imageUrl && <Image src={item.imageUrl} alt="" loading="lazy" radius="md" />}
64
+ <Text size="sm" c="dimmed">
65
+ {item.body}
66
+ </Text>
67
+ <Group gap="md">
68
+ {tourKey && (
69
+ <Button size="xs" onClick={() => onTakeTour(tourKey)}>
70
+ {labels.takeTour}
71
+ </Button>
72
+ )}
73
+ {item.cta && (
74
+ <Anchor
75
+ href={item.cta.url}
76
+ target="_blank"
77
+ rel="noreferrer"
78
+ size="sm"
79
+ fw={500}
80
+ onClick={() => onCtaClick(item.key)}
81
+ >
82
+ {item.cta.label}
83
+ </Anchor>
84
+ )}
85
+ </Group>
86
+ </Stack>
87
+ );
88
+ }
89
+
90
+ export function WhatsNewPanel({ open, onClose, labels, locale, formatDate }: WhatsNewPanelProps) {
91
+ const text = { ...DEFAULT_LABELS, ...labels };
92
+ const { items, recordClick, startTour } = useWhatsNewPanel(open);
93
+ const format = formatDate ?? ((isoDate: string) => formatDateWithIntl(isoDate, locale));
94
+
95
+ function takeTour(tourKey: string): void {
96
+ onClose();
97
+ void startTour(tourKey);
98
+ }
99
+
100
+ return (
101
+ <Drawer
102
+ opened={open}
103
+ onClose={onClose}
104
+ position="right"
105
+ title={text.title}
106
+ closeButtonProps={{ 'aria-label': text.close }}
107
+ scrollAreaComponent={ScrollArea.Autosize}
108
+ >
109
+ {items.length === 0 ? (
110
+ <Text size="sm" c="dimmed">
111
+ {text.empty}
112
+ </Text>
113
+ ) : (
114
+ <Stack component="ul" gap="xl" p={0} m={0} style={{ listStyle: 'none' }}>
115
+ {items.map((item) => (
116
+ <Entry
117
+ key={item.key}
118
+ item={item}
119
+ labels={text}
120
+ formatDate={format}
121
+ onCtaClick={recordClick}
122
+ onTakeTour={takeTour}
123
+ />
124
+ ))}
125
+ </Stack>
126
+ )}
127
+ </Drawer>
128
+ );
129
+ }
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { useEffect, useRef, type KeyboardEvent } from 'react';
5
+ import { useAnnouncementInterruption, useWhatsNew } from '../../src/react';
6
+ import { BodyPortal } from '../../src/web';
7
+
8
+ export interface AnnouncementModalLabels {
9
+ acknowledge: string;
10
+ takeTour: string;
11
+ }
12
+
13
+ const DEFAULT_LABELS: AnnouncementModalLabels = {
14
+ acknowledge: 'Got it',
15
+ takeTour: 'Take the tour',
16
+ };
17
+
18
+ // Replace these with your design tokens.
19
+ const styles = {
20
+ overlay: 'fixed inset-0 z-[900] flex items-center justify-center p-4',
21
+ backdrop: 'absolute inset-0 bg-black/50',
22
+ dialog: 'relative w-full max-w-md space-y-3 rounded-xl bg-white p-6 text-neutral-900 shadow-xl focus:outline-none dark:bg-neutral-900 dark:text-neutral-100',
23
+ image: 'w-full rounded-lg',
24
+ title: 'text-lg font-semibold',
25
+ body: 'text-sm text-neutral-600 dark:text-neutral-300',
26
+ link: 'inline-block text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400',
27
+ actions: 'flex justify-end gap-2 pt-2',
28
+ secondaryButton: 'rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:text-neutral-200 dark:hover:bg-neutral-800',
29
+ primaryButton: 'rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500',
30
+ };
31
+
32
+ export function AnnouncementModal({ labels }: { labels?: Partial<AnnouncementModalLabels> }) {
33
+ const text = { ...DEFAULT_LABELS, ...labels };
34
+ const { announcement, dismiss, acknowledge } = useAnnouncementInterruption();
35
+ const { recordClick, startTour } = useWhatsNew();
36
+ const dialogRef = useRef<HTMLDivElement>(null);
37
+ const announcementKey = announcement?.key;
38
+
39
+ useEffect(() => {
40
+ if (announcementKey) dialogRef.current?.focus();
41
+ }, [announcementKey]);
42
+
43
+ if (!announcement) return null;
44
+ const { tourKey } = announcement;
45
+
46
+ function handleKeyDown(event: KeyboardEvent): void {
47
+ if (event.key === 'Escape') void dismiss();
48
+ }
49
+
50
+ async function takeTour(tourKey: string): Promise<void> {
51
+ await acknowledge();
52
+ await startTour(tourKey);
53
+ }
54
+
55
+ return (
56
+ <BodyPortal>
57
+ <div className={styles.overlay} onKeyDown={handleKeyDown}>
58
+ <div className={styles.backdrop} aria-hidden="true" onClick={() => void dismiss()} />
59
+ <div
60
+ ref={dialogRef}
61
+ role="dialog"
62
+ aria-modal="true"
63
+ aria-labelledby="announcement-title"
64
+ aria-describedby="announcement-body"
65
+ tabIndex={-1}
66
+ className={styles.dialog}
67
+ >
68
+ {announcement.imageUrl && <img src={announcement.imageUrl} alt="" className={styles.image} />}
69
+ <h2 id="announcement-title" className={styles.title}>
70
+ {announcement.title}
71
+ </h2>
72
+ <p id="announcement-body" className={styles.body}>
73
+ {announcement.body}
74
+ </p>
75
+ {announcement.cta && (
76
+ <a
77
+ href={announcement.cta.url}
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ onClick={() => recordClick(announcement.key)}
81
+ className={styles.link}
82
+ >
83
+ {announcement.cta.label}
84
+ </a>
85
+ )}
86
+ <div className={styles.actions}>
87
+ {tourKey && (
88
+ <button type="button" onClick={() => void takeTour(tourKey)} className={styles.secondaryButton}>
89
+ {text.takeTour}
90
+ </button>
91
+ )}
92
+ <button type="button" onClick={() => void acknowledge()} className={styles.primaryButton}>
93
+ {text.acknowledge}
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </BodyPortal>
99
+ );
100
+ }
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { TourOverlay, TourStep, type TourOverlayProps } from '../../src/web';
5
+ import type { StepAnnouncement, StepCount } from '../../src/react';
6
+
7
+ export interface TourCardLabels {
8
+ next: string;
9
+ back: string;
10
+ skip: string;
11
+ done: string;
12
+ close: string;
13
+ progress: ({ position, total }: StepCount) => string;
14
+ announceStep: ({ position, total, title }: StepAnnouncement) => string;
15
+ }
16
+
17
+ const DEFAULT_LABELS: TourCardLabels = {
18
+ next: 'Next',
19
+ back: 'Back',
20
+ skip: 'Skip tour',
21
+ done: 'Done',
22
+ close: 'Close tour',
23
+ progress: ({ position, total }) => `${position} / ${total}`,
24
+ announceStep: ({ position, total, title }) => `Step ${position} of ${total}: ${title}`,
25
+ };
26
+
27
+ // Replace these with your design tokens.
28
+ const styles = {
29
+ spotlight: 'z-[1000] text-black/50',
30
+ popover: 'z-[1001]',
31
+ card: 'w-80 max-w-[calc(100vw-2rem)] rounded-xl border border-neutral-200 bg-white p-4 text-neutral-900 shadow-xl outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100 dark:focus-visible:ring-offset-neutral-900',
32
+ header: 'flex items-center justify-between',
33
+ progress: 'text-xs font-medium text-neutral-500 dark:text-neutral-400',
34
+ close: '-mr-1 rounded-md px-2 text-lg leading-none text-neutral-500 hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:hover:bg-neutral-800',
35
+ title: 'mt-2 text-base font-semibold',
36
+ body: 'mt-1 text-sm text-neutral-600 dark:text-neutral-300',
37
+ footer: 'mt-4 flex items-center justify-between gap-2',
38
+ actions: 'flex items-center gap-2',
39
+ secondaryButton: 'rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:text-neutral-300 dark:hover:bg-neutral-800',
40
+ primaryButton: 'rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500',
41
+ };
42
+
43
+ export interface TourCardProps extends Pick<TourOverlayProps, 'presentation'> {
44
+ labels?: Partial<TourCardLabels>;
45
+ }
46
+
47
+ export function TourCard({ labels, presentation }: TourCardProps) {
48
+ const text = { ...DEFAULT_LABELS, ...labels };
49
+
50
+ return (
51
+ <TourOverlay spotlightClassName={styles.spotlight} popoverClassName={styles.popover} presentation={presentation}>
52
+ <TourStep.Root announceStep={text.announceStep} className={styles.card}>
53
+ <div className={styles.header}>
54
+ <TourStep.Progress className={styles.progress}>{text.progress}</TourStep.Progress>
55
+ <TourStep.Close aria-label={text.close} className={styles.close} />
56
+ </div>
57
+ <TourStep.Title className={styles.title} />
58
+ <TourStep.Body className={styles.body} />
59
+ <div className={styles.footer}>
60
+ <TourStep.Skip className={styles.secondaryButton}>{text.skip}</TourStep.Skip>
61
+ <div className={styles.actions}>
62
+ <TourStep.Back className={styles.secondaryButton}>{text.back}</TourStep.Back>
63
+ <TourStep.Next nextLabel={text.next} doneLabel={text.done} className={styles.primaryButton} />
64
+ </div>
65
+ </div>
66
+ </TourStep.Root>
67
+ </TourOverlay>
68
+ );
69
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ // After copying, change these imports to '@thelol3882/sapar/...'
4
+ import { useState } from 'react';
5
+ import { useWhatsNew } from '../../src/react';
6
+ import { WhatsNewPanel, type WhatsNewPanelProps } from './WhatsNewPanel';
7
+
8
+ // Replace these with your design tokens.
9
+ const styles = {
10
+ button: 'relative rounded-full p-2 text-neutral-600 hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:text-neutral-300 dark:hover:bg-neutral-800',
11
+ icon: 'size-5',
12
+ badge: 'absolute -right-0.5 -top-0.5 flex min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-semibold leading-4 text-white',
13
+ };
14
+
15
+ export interface WhatsNewButtonProps extends Pick<WhatsNewPanelProps, 'labels' | 'locale' | 'formatDate'> {
16
+ className?: string;
17
+ /** Accessible name; the unread count is appended. */
18
+ label?: string;
19
+ }
20
+
21
+ function SparkleIcon() {
22
+ return (
23
+ <svg
24
+ viewBox="0 0 24 24"
25
+ fill="none"
26
+ stroke="currentColor"
27
+ strokeWidth="2"
28
+ strokeLinecap="round"
29
+ strokeLinejoin="round"
30
+ className={styles.icon}
31
+ aria-hidden="true"
32
+ >
33
+ <path d="M9.9 15.5a2 2 0 0 0-1.4-1.4l-6.1-1.6a.5.5 0 0 1 0-1l6.1-1.6a2 2 0 0 0 1.4-1.4l1.6-6.1a.5.5 0 0 1 1 0l1.6 6.1a2 2 0 0 0 1.4 1.4l6.1 1.6a.5.5 0 0 1 0 1l-6.1 1.6a2 2 0 0 0-1.4 1.4l-1.6 6.1a.5.5 0 0 1-1 0zM20 3v4M22 5h-4M4 17v2M5 18H3" />
34
+ </svg>
35
+ );
36
+ }
37
+
38
+ export function WhatsNewButton({ className, label = "What's new", labels, locale, formatDate }: WhatsNewButtonProps) {
39
+ const [open, setOpen] = useState(false);
40
+ const { unreadCount } = useWhatsNew();
41
+
42
+ return (
43
+ <>
44
+ <button
45
+ type="button"
46
+ aria-label={unreadCount > 0 ? `${label} (${unreadCount})` : label}
47
+ onClick={() => setOpen(true)}
48
+ className={className ?? styles.button}
49
+ >
50
+ <SparkleIcon />
51
+ {unreadCount > 0 && <span className={styles.badge}>{unreadCount}</span>}
52
+ </button>
53
+ <WhatsNewPanel open={open} onClose={() => setOpen(false)} labels={labels} locale={locale} formatDate={formatDate} />
54
+ </>
55
+ );
56
+ }