Skip to content

Commit 8e7ab6d

Browse files
committed
chore: update version to 1.1.0 and enhance CSS build process
- Increment version in package.json to 1.1.0. - Add a new minified CSS output to the build process for improved performance. - Refactor the CSS build script to streamline the copying and minification of styles. - Remove unused constants from the codebase to clean up the project. These changes improve the overall efficiency and maintainability of the project.
1 parent 763c73c commit 8e7ab6d

9 files changed

Lines changed: 167 additions & 140 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vcui/popser",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"type": "module",
55
"description": "Toast notifications for React. Built on Base UI. Sonner-compatible API.",
66
"main": "./dist/index.cjs",
@@ -18,6 +18,7 @@
1818
}
1919
},
2020
"./styles": "./dist/styles/popser.css",
21+
"./styles/min": "./dist/styles/popser.min.css",
2122
"./tokens": "./dist/styles/tokens.css"
2223
},
2324
"sideEffects": [

scripts/build-css.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* CSS build script — zero dependencies.
3+
*
4+
* 1. Copies modular CSS files to dist/styles/ (for granular imports)
5+
* 2. Inlines @import directives in popser.css to produce a single flat file
6+
* 3. Minifies the result → dist/styles/popser.min.css
7+
*/
8+
9+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10+
import { join, resolve } from "node:path";
11+
12+
const SRC = resolve("src/styles");
13+
const DIST = resolve("dist/styles");
14+
15+
mkdirSync(DIST, { recursive: true });
16+
17+
// ── 1. Copy individual CSS files ────────────────────────────────────
18+
const files = [
19+
"tokens.css",
20+
"viewport.css",
21+
"toast.css",
22+
"transitions.css",
23+
"content.css",
24+
"controls.css",
25+
"popser.css",
26+
];
27+
28+
for (const file of files) {
29+
copyFileSync(join(SRC, file), join(DIST, file));
30+
}
31+
32+
// ── 2. Inline @imports and build flat bundle ────────────────────────
33+
const entryCSS = readFileSync(join(SRC, "popser.css"), "utf8");
34+
35+
const inlined = entryCSS.replace(
36+
/@import\s+["']\.\/([^"']+)["']\s*;/g,
37+
(_, filename) => {
38+
return readFileSync(join(SRC, filename), "utf8");
39+
}
40+
);
41+
42+
// ── 3. Minify (strip comments, collapse whitespace) ─────────────────
43+
function minifyCSS(css) {
44+
return (
45+
css
46+
// Remove block comments (non-greedy)
47+
.replace(/\/\*[\s\S]*?\*\//g, "")
48+
// Collapse whitespace around braces, colons, semicolons, commas
49+
.replace(/\s*([{}:;,>~+])\s*/g, "$1")
50+
// Collapse remaining runs of whitespace
51+
.replace(/\s{2,}/g, " ")
52+
// Remove trailing semicolons before closing braces
53+
.replace(/;}/g, "}")
54+
// Remove leading/trailing whitespace
55+
.trim()
56+
);
57+
}
58+
59+
const minified = minifyCSS(inlined);
60+
61+
writeFileSync(join(DIST, "popser.min.css"), minified);
62+
63+
const rawKB = (Buffer.byteLength(inlined) / 1024).toFixed(1);
64+
const minKB = (Buffer.byteLength(minified) / 1024).toFixed(1);
65+
console.log(
66+
`CSS: ${rawKB} KB → ${minKB} KB minified (dist/styles/popser.min.css)`
67+
);

src/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ export const DEFAULT_SWIPE_DIRECTION: PopserSwipeDirection[] = [
88
"right",
99
];
1010
export const DEFAULT_CLOSE_BUTTON = "hover" as const;
11-
export const DEFAULT_EXPAND = false;
12-
export const DEFAULT_RICH_COLORS = false;
13-
export const DEFAULT_THEME = "system" as const;
1411
export const DEFAULT_OFFSET = 16;
1512
export const DEFAULT_GAP = 8;
1613
export const DEFAULT_MOBILE_BREAKPOINT = 600;

src/toast-icon.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,13 @@ const icons: Record<string, ReactNode> = {
7272
),
7373
};
7474

75+
const SPINNER_BARS = Array.from({ length: 12 }, (_, i) => (
76+
// biome-ignore lint/suspicious/noArrayIndexKey: static spinner bars never reorder
77+
<div data-popser-spinner-bar key={i} />
78+
));
79+
7580
function Spinner() {
76-
return (
77-
<div data-popser-spinner>
78-
{Array.from({ length: 12 }, (_, i) => (
79-
// biome-ignore lint/suspicious/noArrayIndexKey: static spinner bars never reorder
80-
<div data-popser-spinner-bar key={i} />
81-
))}
82-
</div>
83-
);
81+
return <div data-popser-spinner>{SPINNER_BARS}</div>;
8482
}
8583

8684
export interface ToastIconProps {

src/toast.ts

Lines changed: 75 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,69 @@ function isReactElement(value: unknown): boolean {
111111
return typeof value === "object" && value !== null && "$$typeof" in value;
112112
}
113113

114+
/**
115+
* Builds a promise state handler (success or error) for `toast.promise`.
116+
* Eliminates duplication between success/error branches.
117+
*/
118+
function buildPromiseHandler<TType extends "success" | "error", TInput>(
119+
type: TType,
120+
handler:
121+
| ReactNode
122+
| ((input: TInput) => ReactNode | PopserPromiseExtendedResult | undefined),
123+
toastId: { current: string },
124+
descriptionOption: PopserPromiseOptions<never>["description"]
125+
) {
126+
if (typeof handler !== "function") {
127+
return {
128+
title: handler,
129+
type,
130+
...(typeof descriptionOption !== "function" &&
131+
descriptionOption !== undefined && {
132+
description: descriptionOption,
133+
}),
134+
};
135+
}
136+
137+
return (input: TInput) => {
138+
const result = (
139+
handler as (
140+
input: TInput
141+
) => ReactNode | PopserPromiseExtendedResult | undefined
142+
)(input);
143+
if (result === undefined) {
144+
queueMicrotask(() => getManager().close(toastId.current));
145+
return { title: "" as ReactNode, type, timeout: 1 };
146+
}
147+
if (isExtendedResult(result)) {
148+
const { title, timeout, icon, action, cancel, description, ...rest } =
149+
result;
150+
return {
151+
title,
152+
type,
153+
...(timeout !== undefined && { timeout }),
154+
...(description !== undefined && { description }),
155+
data: {
156+
__popser: {
157+
...(icon !== undefined && { icon }),
158+
...(action !== undefined && { action }),
159+
...(cancel !== undefined && { cancel }),
160+
...rest,
161+
},
162+
},
163+
};
164+
}
165+
const desc =
166+
typeof descriptionOption === "function"
167+
? (descriptionOption as (data: TInput) => ReactNode)(input)
168+
: undefined;
169+
return {
170+
title: result,
171+
type,
172+
...(desc !== undefined && { description: desc }),
173+
};
174+
};
175+
}
176+
114177
// ---------------------------------------------------------------------------
115178
// Public API
116179
// ---------------------------------------------------------------------------
@@ -220,113 +283,18 @@ toast.promise = <T>(
220283

221284
const toastId = { current: "" };
222285

223-
const successHandler =
224-
typeof success === "function"
225-
? (result: T) => {
226-
const handlerResult = success(result);
227-
if (handlerResult === undefined) {
228-
// Close the loading toast instead of showing a ghost (B1 fix)
229-
queueMicrotask(() => getManager().close(toastId.current));
230-
return { title: "", type: "success" as const, timeout: 1 };
231-
}
232-
if (isExtendedResult(handlerResult)) {
233-
const {
234-
title,
235-
timeout,
236-
icon,
237-
action,
238-
cancel,
239-
description,
240-
...rest
241-
} = handlerResult;
242-
return {
243-
title,
244-
type: "success" as const,
245-
...(timeout !== undefined && { timeout }),
246-
...(description !== undefined && { description }),
247-
data: {
248-
__popser: {
249-
...(icon !== undefined && { icon }),
250-
...(action !== undefined && { action }),
251-
...(cancel !== undefined && { cancel }),
252-
...rest,
253-
},
254-
},
255-
};
256-
}
257-
// Per-state description support
258-
const desc =
259-
typeof options.description === "function"
260-
? (options.description as (data: T) => ReactNode)(result)
261-
: undefined;
262-
return {
263-
title: handlerResult,
264-
type: "success" as const,
265-
...(desc !== undefined && { description: desc }),
266-
};
267-
}
268-
: {
269-
title: success,
270-
type: "success" as const,
271-
...(typeof options.description !== "function" &&
272-
options.description !== undefined && {
273-
description: options.description,
274-
}),
275-
};
276-
277-
const errorHandler =
278-
typeof error === "function"
279-
? (err: unknown) => {
280-
const handlerResult = error(err);
281-
if (handlerResult === undefined) {
282-
// Close the loading toast instead of showing a ghost (B1 fix)
283-
queueMicrotask(() => getManager().close(toastId.current));
284-
return { title: "", type: "error" as const, timeout: 1 };
285-
}
286-
if (isExtendedResult(handlerResult)) {
287-
const {
288-
title,
289-
timeout,
290-
icon,
291-
action,
292-
cancel,
293-
description,
294-
...rest
295-
} = handlerResult;
296-
return {
297-
title,
298-
type: "error" as const,
299-
...(timeout !== undefined && { timeout }),
300-
...(description !== undefined && { description }),
301-
data: {
302-
__popser: {
303-
...(icon !== undefined && { icon }),
304-
...(action !== undefined && { action }),
305-
...(cancel !== undefined && { cancel }),
306-
...rest,
307-
},
308-
},
309-
};
310-
}
311-
// Per-state description support
312-
const desc =
313-
typeof options.description === "function"
314-
? (options.description as (error: unknown) => ReactNode)(err)
315-
: undefined;
316-
return {
317-
title: handlerResult,
318-
type: "error" as const,
319-
...(desc !== undefined && { description: desc }),
320-
};
321-
}
322-
: {
323-
title: error,
324-
type: "error" as const,
325-
...(typeof options.description !== "function" &&
326-
options.description !== undefined && {
327-
description: options.description,
328-
}),
329-
};
286+
const successHandler = buildPromiseHandler<"success", T>(
287+
"success",
288+
success,
289+
toastId,
290+
options.description
291+
);
292+
const errorHandler = buildPromiseHandler<"error", unknown>(
293+
"error",
294+
error,
295+
toastId,
296+
options.description
297+
);
330298

331299
const result = getManager().promise(promise, {
332300
...(options.id !== undefined && { id: options.id }),

tsup.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ export default defineConfig({
1010
sourcemap: false,
1111
minify: true,
1212
external: ["react", "react-dom", "@base-ui/react"],
13-
onSuccess: "mkdir -p dist/styles && cp src/styles/*.css dist/styles/",
13+
onSuccess: "node scripts/build-css.mjs",
1414
});

web/app/(home)/page.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ const highlights = [
3131
},
3232
{
3333
label: "E2E ready",
34-
detail: "data-popser-id on every toast. Your tests won't be flaky (for once).",
34+
detail:
35+
"data-popser-id on every toast. Your tests won't be flaky (for once).",
3536
},
3637
{
3738
label: "Zero icon deps",
@@ -103,17 +104,15 @@ export default function HomePage() {
103104
<section className="mx-auto w-full max-w-2xl px-4 py-16">
104105
<div className="divide-y divide-fd-border rounded-xl border border-fd-border">
105106
{highlights.map((item) => (
106-
<div
107-
className="flex items-start gap-3 px-5 py-4"
108-
key={item.label}
109-
>
107+
<div className="flex items-start gap-3 px-5 py-4" key={item.label}>
110108
<span className="mt-0.5 text-fd-muted-foreground">
111109
{"badge" in item ? (
112-
<span className="inline-block rounded-full bg-purple-500/10 px-2 py-0.5 font-medium text-purple-500 text-[10px] leading-tight">
110+
<span className="inline-block rounded-full bg-purple-500/10 px-2 py-0.5 font-medium text-[10px] text-purple-500 leading-tight">
113111
{item.badge}
114112
</span>
115113
) : (
116114
<svg
115+
aria-hidden="true"
117116
className="size-4"
118117
fill="none"
119118
stroke="currentColor"

web/components/copy-command.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallback, useState } from "react";
55
function CopyIcon() {
66
return (
77
<svg
8+
aria-hidden="true"
89
className="size-3.5"
910
fill="none"
1011
stroke="currentColor"
@@ -22,6 +23,7 @@ function CopyIcon() {
2223
function CheckIcon() {
2324
return (
2425
<svg
26+
aria-hidden="true"
2527
className="size-3.5"
2628
fill="none"
2729
stroke="currentColor"

0 commit comments

Comments
 (0)