@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.
- package/LICENSE +21 -0
- package/README.md +380 -2
- package/dist/adapter-Y9yB3-q3.d.ts +14 -0
- package/dist/adapter-fFZPsINL.d.ts +14 -0
- package/dist/chunk-7B66QEMY.js +536 -0
- package/dist/chunk-7B66QEMY.js.map +1 -0
- package/dist/chunk-FM64UDKJ.js +893 -0
- package/dist/chunk-FM64UDKJ.js.map +1 -0
- package/dist/chunk-VC6XLWM3.js +176 -0
- package/dist/chunk-VC6XLWM3.js.map +1 -0
- package/dist/chunk-ZBVQGMR4.js +583 -0
- package/dist/chunk-ZBVQGMR4.js.map +1 -0
- package/dist/cli/index.js +1335 -0
- package/dist/core/index.d.ts +273 -0
- package/dist/core/index.js +71 -0
- package/dist/core/index.js.map +1 -0
- package/dist/expo/index.d.ts +189 -0
- package/dist/expo/index.js +39 -0
- package/dist/expo/index.js.map +1 -0
- package/dist/expo-router/index.d.ts +18 -0
- package/dist/expo-router/index.js +46 -0
- package/dist/expo-router/index.js.map +1 -0
- package/dist/next/index.d.ts +18 -0
- package/dist/next/index.js +49 -0
- package/dist/next/index.js.map +1 -0
- package/dist/presentation-CClsPRT6.d.ts +12 -0
- package/dist/react/index.d.ts +35 -0
- package/dist/react/index.js +29 -0
- package/dist/react/index.js.map +1 -0
- package/dist/rtk/index.d.ts +751 -0
- package/dist/rtk/index.js +114 -0
- package/dist/rtk/index.js.map +1 -0
- package/dist/stepCount-CTYYFWiV.d.ts +57 -0
- package/dist/types-CHD2pB5X.d.ts +192 -0
- package/dist/web/index.d.ts +112 -0
- package/dist/web/index.js +34 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +147 -3
- package/templates/expo/AnnouncementModal.tsx +102 -0
- package/templates/expo/TourCard.tsx +112 -0
- package/templates/expo/WhatsNewButton.tsx +87 -0
- package/templates/expo/WhatsNewPanel.tsx +167 -0
- package/templates/mantine/AnnouncementModal.tsx +66 -0
- package/templates/mantine/TourCard.tsx +84 -0
- package/templates/mantine/WhatsNewButton.tsx +62 -0
- package/templates/mantine/WhatsNewPanel.tsx +129 -0
- package/templates/tailwind/AnnouncementModal.tsx +100 -0
- package/templates/tailwind/TourCard.tsx +69 -0
- package/templates/tailwind/WhatsNewButton.tsx +56 -0
- package/templates/tailwind/WhatsNewPanel.tsx +174 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// After copying, change these imports to '@thelol3882/sapar/...'
|
|
4
|
+
import { useEffect, useRef, type KeyboardEvent } from 'react';
|
|
5
|
+
import { useWhatsNewPanel, type WhatsNewPanelItem } from '../../src/react';
|
|
6
|
+
import { BodyPortal } from '../../src/web';
|
|
7
|
+
|
|
8
|
+
export interface WhatsNewPanelLabels {
|
|
9
|
+
title: string;
|
|
10
|
+
empty: string;
|
|
11
|
+
close: string;
|
|
12
|
+
takeTour: string;
|
|
13
|
+
new: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LABELS: WhatsNewPanelLabels = {
|
|
17
|
+
title: "What's new",
|
|
18
|
+
empty: 'Nothing new yet.',
|
|
19
|
+
close: 'Close',
|
|
20
|
+
takeTour: 'Take the tour',
|
|
21
|
+
new: 'New',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Replace these with your design tokens.
|
|
25
|
+
const styles = {
|
|
26
|
+
overlay: 'fixed inset-0 z-[900] flex justify-end',
|
|
27
|
+
backdrop: 'absolute inset-0 bg-black/40',
|
|
28
|
+
panel: 'relative flex h-full w-full max-w-md flex-col bg-white text-neutral-900 shadow-xl focus:outline-none dark:bg-neutral-900 dark:text-neutral-100',
|
|
29
|
+
header: 'flex items-center justify-between border-b border-neutral-200 px-5 py-4 dark:border-neutral-800',
|
|
30
|
+
heading: 'text-base font-semibold',
|
|
31
|
+
close: 'rounded-md px-2 text-xl leading-none text-neutral-500 hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
32
|
+
content: 'flex-1 overflow-y-auto px-5 py-4',
|
|
33
|
+
empty: 'text-sm text-neutral-500 dark:text-neutral-400',
|
|
34
|
+
list: 'space-y-6',
|
|
35
|
+
entry: 'space-y-2 border-b border-neutral-200 pb-6 last:border-0 dark:border-neutral-800',
|
|
36
|
+
entryHeader: 'flex items-center gap-2',
|
|
37
|
+
entryTitle: 'text-sm font-semibold',
|
|
38
|
+
newBadge: 'rounded-full bg-indigo-600 px-2 text-[10px] font-semibold uppercase leading-4 text-white',
|
|
39
|
+
date: 'block text-xs text-neutral-500 dark:text-neutral-400',
|
|
40
|
+
image: 'w-full rounded-lg',
|
|
41
|
+
body: 'text-sm text-neutral-600 dark:text-neutral-300',
|
|
42
|
+
actions: 'flex flex-wrap items-center gap-3',
|
|
43
|
+
tourButton: 'rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500',
|
|
44
|
+
link: 'text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface WhatsNewPanelProps {
|
|
48
|
+
open: boolean;
|
|
49
|
+
onClose: () => void;
|
|
50
|
+
labels?: Partial<WhatsNewPanelLabels>;
|
|
51
|
+
/** BCP 47 tag for the default date format. Defaults to the browser locale. */
|
|
52
|
+
locale?: string;
|
|
53
|
+
/** Replaces the default date format; the template then never uses `Intl`. */
|
|
54
|
+
formatDate?: (isoDate: string) => string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function useFocusWhileOpen(open: boolean) {
|
|
58
|
+
const panelRef = useRef<HTMLElement>(null);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) return;
|
|
61
|
+
const previouslyFocused = document.activeElement;
|
|
62
|
+
panelRef.current?.focus();
|
|
63
|
+
return () => {
|
|
64
|
+
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus();
|
|
65
|
+
};
|
|
66
|
+
}, [open]);
|
|
67
|
+
return panelRef;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatDateWithIntl(isoDate: string, locale: string | undefined): string {
|
|
71
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(new Date(isoDate));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Entry({
|
|
75
|
+
item,
|
|
76
|
+
labels,
|
|
77
|
+
formatDate,
|
|
78
|
+
onCtaClick,
|
|
79
|
+
onTakeTour,
|
|
80
|
+
}: {
|
|
81
|
+
item: WhatsNewPanelItem;
|
|
82
|
+
labels: WhatsNewPanelLabels;
|
|
83
|
+
formatDate: (isoDate: string) => string;
|
|
84
|
+
onCtaClick: (key: string) => void;
|
|
85
|
+
onTakeTour: (tourKey: string) => void;
|
|
86
|
+
}) {
|
|
87
|
+
const { tourKey } = item;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<li className={styles.entry}>
|
|
91
|
+
<div className={styles.entryHeader}>
|
|
92
|
+
<h3 className={styles.entryTitle}>{item.title}</h3>
|
|
93
|
+
{item.isNew && <span className={styles.newBadge}>{labels.new}</span>}
|
|
94
|
+
</div>
|
|
95
|
+
{item.publishedAt && (
|
|
96
|
+
<time dateTime={item.publishedAt} className={styles.date}>
|
|
97
|
+
{formatDate(item.publishedAt)}
|
|
98
|
+
</time>
|
|
99
|
+
)}
|
|
100
|
+
{item.imageUrl && <img src={item.imageUrl} alt="" loading="lazy" className={styles.image} />}
|
|
101
|
+
<p className={styles.body}>{item.body}</p>
|
|
102
|
+
<div className={styles.actions}>
|
|
103
|
+
{tourKey && (
|
|
104
|
+
<button type="button" onClick={() => onTakeTour(tourKey)} className={styles.tourButton}>
|
|
105
|
+
{labels.takeTour}
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
{item.cta && (
|
|
109
|
+
<a
|
|
110
|
+
href={item.cta.url}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noreferrer"
|
|
113
|
+
onClick={() => onCtaClick(item.key)}
|
|
114
|
+
className={styles.link}
|
|
115
|
+
>
|
|
116
|
+
{item.cta.label}
|
|
117
|
+
</a>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</li>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function WhatsNewPanel({ open, onClose, labels, locale, formatDate }: WhatsNewPanelProps) {
|
|
125
|
+
const text = { ...DEFAULT_LABELS, ...labels };
|
|
126
|
+
const { items, recordClick, startTour } = useWhatsNewPanel(open);
|
|
127
|
+
const panelRef = useFocusWhileOpen(open);
|
|
128
|
+
const format = formatDate ?? ((isoDate: string) => formatDateWithIntl(isoDate, locale));
|
|
129
|
+
|
|
130
|
+
if (!open) return null;
|
|
131
|
+
|
|
132
|
+
function handleKeyDown(event: KeyboardEvent): void {
|
|
133
|
+
if (event.key === 'Escape') onClose();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function takeTour(tourKey: string): void {
|
|
137
|
+
onClose();
|
|
138
|
+
void startTour(tourKey);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<BodyPortal>
|
|
143
|
+
<div className={styles.overlay} onKeyDown={handleKeyDown}>
|
|
144
|
+
<div className={styles.backdrop} aria-hidden="true" onClick={onClose} />
|
|
145
|
+
<aside ref={panelRef} role="dialog" aria-modal="true" aria-label={text.title} tabIndex={-1} className={styles.panel}>
|
|
146
|
+
<header className={styles.header}>
|
|
147
|
+
<h2 className={styles.heading}>{text.title}</h2>
|
|
148
|
+
<button type="button" aria-label={text.close} onClick={onClose} className={styles.close}>
|
|
149
|
+
×
|
|
150
|
+
</button>
|
|
151
|
+
</header>
|
|
152
|
+
<div className={styles.content}>
|
|
153
|
+
{items.length === 0 ? (
|
|
154
|
+
<p className={styles.empty}>{text.empty}</p>
|
|
155
|
+
) : (
|
|
156
|
+
<ul className={styles.list}>
|
|
157
|
+
{items.map((item) => (
|
|
158
|
+
<Entry
|
|
159
|
+
key={item.key}
|
|
160
|
+
item={item}
|
|
161
|
+
labels={text}
|
|
162
|
+
formatDate={format}
|
|
163
|
+
onCtaClick={recordClick}
|
|
164
|
+
onTakeTour={takeTour}
|
|
165
|
+
/>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</aside>
|
|
171
|
+
</div>
|
|
172
|
+
</BodyPortal>
|
|
173
|
+
);
|
|
174
|
+
}
|