@versini/ui-badge 1.0.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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @versini/ui-badge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@versini/ui-badge?style=flat-square)](https://www.npmjs.com/package/@versini/ui-badge)
4
+ ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-badge?style=flat-square&label=size%20(gzip)>)
5
+
6
+ > An inline, superscript-style React badge for adding a small red dot or a count to any piece of text.
7
+
8
+ The Badge component renders a `<sup>` element that sits next to text — like a native `<sup>` tag — and displays either a small red dot (notification indicator) or a count (with a `99+` ceiling for large numbers). Its size automatically scales with the surrounding font, so it works inside any text element (`<h1>`, `<p>`, `<button>`, etc.) without manual tuning.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @versini/ui-badge
14
+ ```
15
+
16
+ > **Note**: This component requires TailwindCSS and the `@versini/ui-styles` plugin for proper styling. See the [installation documentation](https://versini-org.github.io/ui-components/?path=/docs/getting-started-installation--docs) for complete setup instructions.
17
+
18
+ ## Usage
19
+
20
+ ### Number badge
21
+
22
+ ```tsx
23
+ import { Badge } from "@versini/ui-badge";
24
+
25
+ function App() {
26
+ return (
27
+ <p>
28
+ Notifications <Badge>42</Badge>
29
+ </p>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ### Dot badge
35
+
36
+ ```tsx
37
+ import { Badge } from "@versini/ui-badge";
38
+
39
+ function App() {
40
+ return (
41
+ <p>
42
+ Profile <Badge />
43
+ </p>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ### Counts above 99
49
+
50
+ ```tsx
51
+ <p>
52
+ Inbox <Badge>250</Badge>
53
+ </p>
54
+ // renders "99+"
55
+ ```
56
+
57
+ ### Auto-scaling with the parent font-size
58
+
59
+ ```tsx
60
+ <h1>Messages <Badge>3</Badge></h1>
61
+ <p>Messages <Badge>3</Badge></p>
62
+ ```
63
+
64
+ The badge's font-size and dimensions are expressed in `em` units, so the badge naturally scales with the text it's attached to.
65
+
66
+ ### Fine-tuning position
67
+
68
+ Inside flex containers (menu items, buttons with icons, list rows), the default `vertical-align: super` may pull the badge higher than ideal. Use `offsetX` and `offsetY` to nudge it back into place.
69
+
70
+ ```tsx
71
+ // inside a flex menu item — bring the badge down to roughly cap-height
72
+ <MenuItem label={<>Inbox <Badge offsetY="0.4em">12</Badge></>} />
73
+
74
+ // nudge a dot slightly right
75
+ <Badge offsetX="0.2em" />
76
+
77
+ // numbers default to em units (negative shifts up/left)
78
+ <Badge offsetX={0.1} offsetY={-0.1}>42</Badge>
79
+ ```
80
+
81
+ ## API
82
+
83
+ ### Badge Props
84
+
85
+ | Prop | Type | Default | Description |
86
+ | --------- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | children | `number \| string` | - | The count to display. If omitted, a small red dot is rendered. `0` (number or string) renders nothing. Non-numeric strings also render nothing. |
88
+ | className | `string` | - | CSS class(es) to add to the badge element. |
89
+ | offsetX | `number \| string` | - | Horizontal nudge via CSS `translateX`. Numbers are em units; strings pass through as-is (`"2px"`, `"-0.1em"`). Negative shifts left. |
90
+ | offsetY | `number \| string` | - | Vertical nudge via CSS `translateY`. Same unit rules as `offsetX`. Negative shifts up (toward the superscript position), positive shifts toward the baseline. |
@@ -0,0 +1,46 @@
1
+ import { JSX } from 'react/jsx-runtime';
2
+
3
+ export declare const Badge: ({ children, className, offsetX, offsetY, }: BadgeProps) => JSX.Element | null;
4
+
5
+ export declare const BADGE_CLASSNAME = "av-badge";
6
+
7
+ export declare const BADGE_DOT_SR_LABEL = "new";
8
+
9
+ export declare const BADGE_NUMBER_CEILING = 99;
10
+
11
+ declare type BadgeProps = {
12
+ /**
13
+ * The count to display inside the badge. Accepts a number or a numeric
14
+ * string so that the natural JSX form `<Badge>42</Badge>` works.
15
+ * - Omit (or pass `undefined`) to render a small red dot.
16
+ * - A positive number is displayed as-is, with values greater than 99
17
+ * shown as "99+".
18
+ * - `0` renders nothing — matches the ButtonIcon `badge` convention.
19
+ * - Non-numeric values render nothing.
20
+ */
21
+ children?: number | string;
22
+ /**
23
+ * CSS class(es) to add to the badge element.
24
+ */
25
+ className?: string;
26
+ /**
27
+ * Horizontal nudge applied via CSS `translateX`. Useful when the badge's
28
+ * default position doesn't quite line up next to surrounding content
29
+ * (e.g. inside a flex menu item, alongside an icon).
30
+ * - Numbers are interpreted as `em` units so the offset scales with the
31
+ * surrounding font (matching the badge's em-based sizing).
32
+ * - Strings are passed through as-is, so you can supply any CSS length
33
+ * (`"2px"`, `"0.25rem"`, `"-0.1em"`).
34
+ * - Negative values shift left.
35
+ */
36
+ offsetX?: number | string;
37
+ /**
38
+ * Vertical nudge applied via CSS `translateY`. Same unit rules as
39
+ * `offsetX`. Negative values shift up (toward where the badge is
40
+ * pulled by `vertical-align: super`), positive values shift down
41
+ * toward the text baseline.
42
+ */
43
+ offsetY?: number | string;
44
+ };
45
+
46
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ /*!
2
+ @versini/ui-badge v1.0.0
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+ import { jsx } from "react/jsx-runtime";
7
+ import clsx from "clsx";
8
+
9
+ const BADGE_CLASSNAME = "av-badge";
10
+ const BADGE_NUMBER_CEILING = 99;
11
+ const BADGE_DOT_SR_LABEL = "new";
12
+
13
+
14
+
15
+
16
+
17
+ const getBadgeDisplayValue = (value)=>value > (/* inlined export .BADGE_NUMBER_CEILING */99) ? `${(/* inlined export .BADGE_NUMBER_CEILING */99)}+` : String(value);
18
+ const toCssLength = (value)=>typeof value === "number" ? `${value}em` : value;
19
+ const getBadgeOffsetStyle = ({ offsetX, offsetY })=>{
20
+ if (offsetX === undefined && offsetY === undefined) {
21
+ return undefined;
22
+ }
23
+ const x = offsetX === undefined ? "0" : toCssLength(offsetX);
24
+ const y = offsetY === undefined ? "0" : toCssLength(offsetY);
25
+ return {
26
+ transform: `translate(${x}, ${y})`
27
+ };
28
+ };
29
+ const getBadgeClasses = ({ isDot, className })=>clsx(BADGE_CLASSNAME, "inline-flex items-center justify-center", "align-super leading-none", "rounded-full", "border border-border-white", "bg-copy-error-dark text-copy-lighter", "pointer-events-none select-none", {
30
+ "size-[0.6em]": isDot,
31
+ "text-[0.65em] font-semibold min-w-[1.5em] h-[1.5em] px-[0.4em] whitespace-nowrap": !isDot
32
+ }, className);
33
+
34
+
35
+
36
+
37
+ const Badge = ({ children, className, offsetX, offsetY })=>{
38
+ const style = getBadgeOffsetStyle({
39
+ offsetX,
40
+ offsetY
41
+ });
42
+ if (children === undefined) {
43
+ return /*#__PURE__*/ jsx("sup", {
44
+ className: getBadgeClasses({
45
+ isDot: true,
46
+ className
47
+ }),
48
+ style: style,
49
+ children: /*#__PURE__*/ jsx("span", {
50
+ className: "sr-only",
51
+ children: (/* inlined export .BADGE_DOT_SR_LABEL */"new")
52
+ })
53
+ });
54
+ }
55
+ const numericValue = typeof children === "number" ? children : Number(children);
56
+ if (!Number.isFinite(numericValue) || numericValue === 0) {
57
+ return null;
58
+ }
59
+ return /*#__PURE__*/ jsx("sup", {
60
+ className: getBadgeClasses({
61
+ isDot: false,
62
+ className
63
+ }),
64
+ style: style,
65
+ children: getBadgeDisplayValue(numericValue)
66
+ });
67
+ };
68
+
69
+
70
+
71
+
72
+ var components_BADGE_DOT_SR_LABEL = (/* inlined export .BADGE_DOT_SR_LABEL */"new");
73
+ var components_BADGE_NUMBER_CEILING = (/* inlined export .BADGE_NUMBER_CEILING */99);
74
+ export { BADGE_CLASSNAME, Badge, components_BADGE_DOT_SR_LABEL as BADGE_DOT_SR_LABEL, components_BADGE_NUMBER_CEILING as BADGE_NUMBER_CEILING };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@versini/ui-badge",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "author": "Arno Versini",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "homepage": "https://www.npmjs.com/package/@versini/ui-badge",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git@github.com:aversini/ui-components.git"
13
+ },
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build:check": "tsc",
23
+ "build:js": "rslib build",
24
+ "build:types": "echo 'Types now built with rslib'",
25
+ "build": "npm-run-all --serial clean build:check build:js",
26
+ "clean": "rimraf dist tmp coverage",
27
+ "dev:js": "rslib build --watch",
28
+ "dev:types": "echo 'Types now watched with rslib'",
29
+ "dev": "rslib build --watch",
30
+ "lint": "biome lint src",
31
+ "lint:fix": "biome check src --write --no-errors-on-unmatched",
32
+ "prettier": "biome check --write --no-errors-on-unmatched",
33
+ "start": "static-server dist --port 5173",
34
+ "test:coverage:ui": "vitest --coverage --ui",
35
+ "test:coverage": "vitest run --coverage",
36
+ "test:watch": "vitest",
37
+ "test": "vitest run",
38
+ "test:visual": "playwright test -c playwright-ct.config.ts",
39
+ "test:visual:report": "playwright show-report playwright-report",
40
+ "test:visual:update": "playwright test -c playwright-ct.config.ts --update-snapshots",
41
+ "test:visual:ui": "playwright test -c playwright-ct.config.ts --ui"
42
+ },
43
+ "devDependencies": {
44
+ "@versini/ui-types": "workspace:*"
45
+ },
46
+ "dependencies": {
47
+ "clsx": "2.1.1",
48
+ "tailwindcss": "4.3.0"
49
+ },
50
+ "sideEffects": [
51
+ "**/*.css"
52
+ ]
53
+ }