Skip to content

Commit 4453573

Browse files
committed
[release] 0.1.4 - Bump version, update README to reflect component behavior changes, and refactor BreakPointer for client-only rendering with customizable breakpoints
1 parent 2adcae0 commit 4453573

7 files changed

Lines changed: 134 additions & 77 deletions

File tree

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ A development utility React component that displays the current Tailwind CSS bre
1515
- **Viewport Dimensions** - Displays real-time width and height
1616
- **Color-Coded Indicators** - Each breakpoint has a unique color for quick identification
1717
- **Keyboard Toggle** - Visible by default; press 't' to turn on and 'Shift+T' to turn off (configurable)
18-
- **Production Safe** - Automatically hidden in production builds
18+
- **Client-only** - Import from a Client Component in App Router; resolves to a no-op on the server
19+
- **No Tailwind Dependency** - Ships its own lightweight CSS; no safelist or node_modules scan needed
1920
- **Zero Dependencies** - Only requires React as a peer dependency
2021
- **Highly Configurable** - Customize position, visibility, font, and more
2122
- **TypeScript Support** - Full type definitions included
@@ -151,8 +152,9 @@ export default function RootLayout({ children }: { children: ReactNode }) {
151152
```
152153

153154
Behavior:
154-
- In development, the component renders on the client.
155-
- In production and on the server, it resolves to a no-op.
155+
- The component renders on the client whenever used from a Client Component.
156+
- On the server, it resolves to a no-op (to avoid RSC hook errors).
157+
- Uses an injected, namespaced stylesheet (`rtwbp-*`), independent of your Tailwind config.
156158

157159
### Next.js Pages Router
158160

@@ -199,7 +201,8 @@ function App() {
199201
| `toggleOffKey` | `string` | `'T'` | Keyboard key to turn the overlay off |
200202
| `position` | `Position` | `'bottom-center'` | Position of the overlay on screen |
201203
| `zIndex` | `number` | `9999` | z-index of the overlay |
202-
| `hideInProduction` | `boolean` | `true` | Automatically hide in production builds |
204+
| `hideInProduction` | `boolean` | `removed` | Removed; component no longer auto-hides in prod |
205+
| `screens` | `{ sm?, md?, lg?, xl?, '2xl'? }` | Tailwind defaults | Override breakpoint thresholds (px) |
203206
| `showDimensions` | `boolean` | `true` | Show viewport width and height |
204207
| `className` | `string` | `undefined` | Additional CSS classes |
205208
| `style` | `React.CSSProperties` | `undefined` | Additional inline styles |
@@ -269,9 +272,9 @@ const config: BreakPointerProps = {
269272
### Why isn't the component showing?
270273

271274
1. Ensure it's rendered from a Client Component (App Router server files resolve to a no-op)
272-
2. Verify you're in development mode (`next dev`); production resolves to a no-op by default
273-
3. Try pressing the default keys: 't' turns on, 'Shift+T' turns off
274-
4. Ensure Tailwind CSS is properly configured and no z-index conflicts
275+
2. Try pressing the default keys: 't' turns on, 'Shift+T' turns off
276+
3. Check for overlapping overlays or z-index conflicts
277+
4. If you customized `screens`, verify the values match your expected breakpoints
275278

276279
### Can I use this without Tailwind CSS?
277280

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-tw-breakpointer",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "A React component that displays the current Tailwind CSS breakpoint and viewport dimensions",
55
"type": "module",
66
"main": "dist/client.cjs",
@@ -10,8 +10,6 @@
1010
".": {
1111
"types": "./dist/client.d.ts",
1212
"react-server": "./dist/noop.js",
13-
"development": "./dist/client.js",
14-
"production": "./dist/noop.js",
1513
"import": "./dist/client.js",
1614
"require": "./dist/client.cjs",
1715
"default": "./dist/client.js"

src/BreakPointer.test.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
/// <reference path="./global.d.ts" />
12
import { render } from '@testing-library/react';
3+
import React from 'react';
4+
import '@testing-library/jest-dom/vitest';
25
import { beforeEach, describe, expect, it, vi } from 'vitest';
36
import { BreakPointer } from './BreakPointer';
47

@@ -13,17 +16,7 @@ describe('BreakPointer - Core functionality', () => {
1316
expect(container).toBeInTheDocument();
1417
});
1518

16-
it('should not render in production by default', () => {
17-
mockEnvironment('production');
18-
const { container } = render(<BreakPointer />);
19-
expect(container.firstChild).toBeNull();
20-
});
21-
22-
it('should render in production when hideInProduction=false', () => {
23-
mockEnvironment('production');
24-
const { container } = render(<BreakPointer hideInProduction={false} />);
25-
expect(container.firstChild).not.toBeNull();
26-
});
19+
// Production gating removed; component renders regardless of NODE_ENV
2720

2821
it('should apply custom className', () => {
2922
const { container } = render(<BreakPointer className="custom-test-class" />);
@@ -70,12 +63,10 @@ describe('BreakPointer - Core functionality', () => {
7063
it('should apply different position classes', () => {
7164
const { container: container1 } = render(<BreakPointer position="top-left" />);
7265
const element1 = container1.firstChild as HTMLElement;
73-
expect(element1?.className).toContain('top-2');
74-
expect(element1?.className).toContain('left-2');
66+
expect(element1?.className).toContain('rtwbp-top-left');
7567

7668
const { container: container2 } = render(<BreakPointer position="bottom-right" />);
7769
const element2 = container2.firstChild as HTMLElement;
78-
expect(element2?.className).toContain('bottom-2');
79-
expect(element2?.className).toContain('right-2');
70+
expect(element2?.className).toContain('rtwbp-bottom-right');
8071
});
8172
});

src/client.tsx

Lines changed: 111 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import type React from 'react';
4-
import { useEffect, useState } from 'react';
4+
import { useEffect, useMemo, useState } from 'react';
55

66
export type Position =
77
| 'bottom-center'
@@ -21,24 +21,73 @@ export interface BreakPointerProps {
2121
toggleOffKey?: string;
2222
position?: Position;
2323
zIndex?: number;
24-
hideInProduction?: boolean;
2524
showDimensions?: boolean;
2625
className?: string;
2726
style?: React.CSSProperties;
2827
fontFamily?: string;
28+
/** Optional override for Tailwind-like breakpoints (in px). */
29+
screens?: {
30+
sm?: number;
31+
md?: number;
32+
lg?: number;
33+
xl?: number;
34+
'2xl'?: number;
35+
};
2936
}
3037

3138
const DEFAULT_FONT_FAMILY =
3239
'JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace';
3340

34-
const positionClasses: Record<Position, string> = {
35-
'bottom-center': 'fixed bottom-2 left-1/2 -translate-x-1/2',
36-
'top-center': 'fixed top-2 left-1/2 -translate-x-1/2',
37-
'top-left': 'fixed top-2 left-2',
38-
'top-right': 'fixed top-2 right-2',
39-
'bottom-left': 'fixed bottom-2 left-2',
40-
'bottom-right': 'fixed bottom-2 right-2',
41-
};
41+
function ensureStylesInjected(cssText: string) {
42+
if (typeof document === 'undefined') {
43+
return;
44+
}
45+
const id = 'rtwbp-styles';
46+
const existing = document.getElementById(id) as HTMLStyleElement | null;
47+
if (existing) {
48+
if (existing.textContent !== cssText) {
49+
existing.textContent = cssText;
50+
}
51+
return;
52+
}
53+
const style = document.createElement('style');
54+
style.id = id;
55+
style.textContent = cssText;
56+
document.head.appendChild(style);
57+
}
58+
59+
function generateCss(params: {
60+
sm: number;
61+
md: number;
62+
lg: number;
63+
xl: number;
64+
x2l: number;
65+
}) {
66+
const { sm, md, lg, xl, x2l } = params;
67+
const toMax = (n: number) => `${Math.max(0, n - 0.02)}px`;
68+
return `
69+
.rtwbp-container{position:fixed;border:2px solid #000;border-radius:4px;font-size:12px;line-height:1.1;background:transparent}
70+
.rtwbp-bottom-center{bottom:8px;left:50%;transform:translateX(-50%)}
71+
.rtwbp-top-center{top:8px;left:50%;transform:translateX(-50%)}
72+
.rtwbp-top-left{top:8px;left:8px}
73+
.rtwbp-top-right{top:8px;right:8px}
74+
.rtwbp-bottom-left{bottom:8px;left:8px}
75+
.rtwbp-bottom-right{bottom:8px;right:8px}
76+
.rtwbp-badge{display:none;padding:4px 8px;font-family:inherit;font-weight:600;letter-spacing:-0.01em}
77+
.rtwbp-xs{background:#ec2427;color:#fff}
78+
.rtwbp-sm{background:#f36525;color:#fff}
79+
.rtwbp-md{background:#edb41f;color:#fff}
80+
.rtwbp-lg{background:#f7ee49;color:#000}
81+
.rtwbp-xl{background:#4686c5;color:#fff}
82+
.rtwbp-2xl{background:#45b64a;color:#fff}
83+
@media (max-width:${toMax(sm)}){.rtwbp-xs{display:block}}
84+
@media (min-width:${sm}px) and (max-width:${toMax(md)}){.rtwbp-sm{display:block}}
85+
@media (min-width:${md}px) and (max-width:${toMax(lg)}){.rtwbp-md{display:block}}
86+
@media (min-width:${lg}px) and (max-width:${toMax(xl)}){.rtwbp-lg{display:block}}
87+
@media (min-width:${xl}px) and (max-width:${toMax(x2l)}){.rtwbp-xl{display:block}}
88+
@media (min-width:${x2l}px){.rtwbp-2xl{display:block}}
89+
`;
90+
}
4291

4392
export const BreakPointer: React.FC<BreakPointerProps> = ({
4493
initiallyVisible = true,
@@ -47,11 +96,11 @@ export const BreakPointer: React.FC<BreakPointerProps> = ({
4796
toggleOffKey,
4897
position = 'bottom-center',
4998
zIndex = 9999,
50-
hideInProduction = true,
5199
showDimensions = true,
52100
className = '',
53101
style = {},
54102
fontFamily = DEFAULT_FONT_FAMILY,
103+
screens,
55104
}) => {
56105
const [isVisible, setIsVisible] = useState(initiallyVisible);
57106
const [viewport, setViewport] = useState({
@@ -110,14 +159,20 @@ export const BreakPointer: React.FC<BreakPointerProps> = ({
110159
};
111160
}, [resolvedToggleOnKey, resolvedToggleOffKey]);
112161

113-
// Production auto-hide
114-
if (
115-
hideInProduction &&
116-
typeof process !== 'undefined' &&
117-
process.env?.NODE_ENV === 'production'
118-
) {
119-
return null;
120-
}
162+
const resolvedScreens = useMemo(() => {
163+
return {
164+
sm: screens?.sm ?? 640,
165+
md: screens?.md ?? 768,
166+
lg: screens?.lg ?? 1024,
167+
xl: screens?.xl ?? 1280,
168+
x2l: screens?.['2xl'] ?? 1536,
169+
} as const;
170+
}, [screens?.sm, screens?.md, screens?.lg, screens?.xl, screens?.['2xl']]);
171+
172+
useEffect(() => {
173+
const css = generateCss(resolvedScreens);
174+
ensureStylesInjected(css);
175+
}, [resolvedScreens]);
121176

122177
if (!isMounted) {
123178
return null;
@@ -126,7 +181,18 @@ export const BreakPointer: React.FC<BreakPointerProps> = ({
126181
return null;
127182
}
128183

129-
const positionClass = positionClasses[position] || positionClasses['bottom-center'];
184+
const positionClass =
185+
position === 'top-center'
186+
? 'rtwbp-top-center'
187+
: position === 'top-left'
188+
? 'rtwbp-top-left'
189+
: position === 'top-right'
190+
? 'rtwbp-top-right'
191+
: position === 'bottom-left'
192+
? 'rtwbp-bottom-left'
193+
: position === 'bottom-right'
194+
? 'rtwbp-bottom-right'
195+
: 'rtwbp-bottom-center';
130196

131197
const containerStyles: React.CSSProperties = {
132198
zIndex,
@@ -136,60 +202,60 @@ export const BreakPointer: React.FC<BreakPointerProps> = ({
136202

137203
return (
138204
<div
139-
className={`${positionClass} rounded border-2 border-black text-xs ${className}`}
205+
className={`rtwbp-container ${positionClass} ${className}`}
140206
style={containerStyles}
141207
aria-hidden="true"
142208
>
143-
<span className="block items-center bg-[#ec2427] px-2 py-1 font-mono font-semibold tracking-tighter text-white sm:hidden md:hidden lg:hidden xl:hidden 2xl:hidden">
144-
1/6 <span className="text-black"></span> xs <span className="text-black"></span>{' '}
209+
<span className="rtwbp-badge rtwbp-xs">
210+
1/6 <span></span> xs <span></span>{' '}
145211
{showDimensions && (
146-
<span className="text-gray-100">
147-
{viewport.width}px <span className="text-black">&lt;</span> 640px
212+
<span>
213+
{viewport.width}px <span>&lt;</span> {resolvedScreens.sm}px
148214
</span>
149215
)}
150216
</span>
151217

152-
<span className="hidden items-center bg-[#f36525] px-2 py-1 font-mono font-semibold tracking-tighter text-white sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
153-
2/6 <span className="text-black"></span> sm <span className="text-black"></span>{' '}
218+
<span className="rtwbp-badge rtwbp-sm">
219+
2/6 <span></span> sm <span></span>{' '}
154220
{showDimensions && (
155-
<span className="text-gray-100">
156-
{viewport.width}px <span className="text-black">&lt;</span> 768px
221+
<span>
222+
{viewport.width}px <span>&lt;</span> {resolvedScreens.md}px
157223
</span>
158224
)}
159225
</span>
160226

161-
<span className="hidden items-center bg-[#edb41f] px-2 py-1 font-mono font-semibold tracking-tighter text-white md:block lg:hidden xl:hidden 2xl:hidden">
162-
3/6 <span className="text-black"></span> md <span className="text-black"></span>{' '}
227+
<span className="rtwbp-badge rtwbp-md">
228+
3/6 <span></span> md <span></span>{' '}
163229
{showDimensions && (
164-
<span className="text-gray-100">
165-
{viewport.width}px <span className="text-black">&lt;</span> 1024px
230+
<span>
231+
{viewport.width}px <span>&lt;</span> {resolvedScreens.lg}px
166232
</span>
167233
)}
168234
</span>
169235

170-
<span className="hidden items-center bg-[#f7ee49] px-2 py-1 font-mono font-semibold tracking-tighter text-black lg:block xl:hidden 2xl:hidden">
171-
4/6 <span className="text-black"></span> lg <span className="text-black"></span>{' '}
236+
<span className="rtwbp-badge rtwbp-lg">
237+
4/6 <span></span> lg <span></span>{' '}
172238
{showDimensions && (
173-
<span className="text-black">
174-
{viewport.width}px <span className="text-black">&lt;</span> 1280px
239+
<span>
240+
{viewport.width}px <span>&lt;</span> {resolvedScreens.xl}px
175241
</span>
176242
)}
177243
</span>
178244

179-
<span className="hidden items-center bg-[#4686c5] px-2 py-1 font-mono font-semibold tracking-tighter text-white xl:block 2xl:hidden">
180-
5/6 <span className="text-black"></span> xl <span className="text-black"></span>{' '}
245+
<span className="rtwbp-badge rtwbp-xl">
246+
5/6 <span></span> xl <span></span>{' '}
181247
{showDimensions && (
182-
<span className="text-gray-100">
183-
{viewport.width}px <span className="text-black">&lt;</span> 1536px
248+
<span>
249+
{viewport.width}px <span>&lt;</span> {resolvedScreens.x2l}px
184250
</span>
185251
)}
186252
</span>
187253

188-
<span className="hidden items-center bg-[#45b64a] px-2 py-1 font-mono font-semibold tracking-tighter text-white 2xl:block">
189-
6/6 <span className="text-black"></span> 2xl <span className="text-black"></span>{' '}
254+
<span className="rtwbp-badge rtwbp-2xl">
255+
6/6 <span></span> 2xl <span></span>{' '}
190256
{showDimensions && (
191-
<span className="text-gray-100">
192-
{viewport.width}px <span className="text-black"></span> 1536px
257+
<span>
258+
{viewport.width}px <span></span> {resolvedScreens.x2l}px
193259
</span>
194260
)}
195261
</span>

src/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ declare global {
44
NODE_ENV?: 'development' | 'production' | 'test';
55
}
66
}
7+
// Provided in test-setup.ts and configured via vitest.setupFiles
8+
// Declared here so editors/tsc know it's available in tests
9+
// eslint-disable-next-line @typescript-eslint/naming-convention
10+
const mockEnvironment: (env: 'development' | 'production' | 'test') => void;
711
}
812

913
export {};

src/noop.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export interface BreakPointerProps {
1515
toggleOffKey?: string;
1616
position?: Position;
1717
zIndex?: number;
18-
hideInProduction?: boolean;
1918
showDimensions?: boolean;
2019
className?: string;
2120
style?: React.CSSProperties;

src/test-setup.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import '@testing-library/jest-dom';
1+
import '@testing-library/jest-dom/vitest';
22
import { afterEach, beforeEach } from 'vitest';
33

44
// Mock process.env for tests
@@ -11,12 +11,8 @@ beforeEach(() => {
1111
afterEach(() => {
1212
process.env = originalEnv;
1313
});
14-
1514
// Global test utilities
16-
declare global {
17-
const mockEnvironment: (env: 'development' | 'production' | 'test') => void;
18-
}
19-
15+
// Define on globalThis without redeclaring types here (types live in global.d.ts)
2016
(globalThis as any).mockEnvironment = (env: 'development' | 'production' | 'test') => {
2117
process.env.NODE_ENV = env;
2218
};

0 commit comments

Comments
 (0)