@the_disreputable_doggo/password-gate 1.0.1 → 1.1.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 CHANGED
@@ -10,26 +10,46 @@ npm install @the_disreputable_doggo/password-gate
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Simple Message (Recommended)
14
+
13
15
  ```tsx
14
16
  import PasswordGate from '@the_disreputable_doggo/password-gate';
15
17
 
16
18
  function App() {
17
19
  return (
18
20
  <PasswordGate
19
- title="Coming Soon"
21
+ title="Site Launch"
20
22
  subtitle="Enter your password"
21
- datePrefix="Expected"
22
- underConstruction="Building in progress"
23
+ message="Coming soon!"
23
24
  placeholder="Password"
24
25
  buttonText="Enter"
25
26
  enabled={true}
26
27
  password="secretpassword"
27
- releaseDate="2027-01-01"
28
28
  />
29
29
  );
30
30
  }
31
31
  ```
32
32
 
33
+ ### Automatic Fallback
34
+
35
+ When omitting both `message` and `releaseDate`, it automatically displays `"Coming soon!"` (or `"¡Próximamente!"` if Spanish is detected via `lang` prop or `document.documentElement.lang`).
36
+
37
+ ### Date-Based Launch (Legacy)
38
+
39
+ ```tsx
40
+ <PasswordGate
41
+ title="Coming Soon"
42
+ subtitle="Enter your password"
43
+ datePrefix="Expected"
44
+ releaseDate="2027-01-01"
45
+ underConstruction="Building in progress"
46
+ placeholder="Password"
47
+ buttonText="Enter"
48
+ enabled={true}
49
+ password="secretpassword"
50
+ />
51
+ ```
52
+
33
53
  ## Features
34
54
  - Fully customizable text props for internationalization.
35
55
  - Accepts configuration variables dynamically.
package/dist/index.d.mts CHANGED
@@ -3,18 +3,48 @@ import React from 'react';
3
3
  interface PasswordGateProps {
4
4
  title: string;
5
5
  subtitle: string;
6
- datePrefix: string;
7
- underConstruction: string;
8
6
  placeholder: string;
9
7
  buttonText: string;
8
+ message?: string;
9
+ datePrefix?: string;
10
+ underConstruction?: string;
11
+ releaseDate?: string;
10
12
  enabled?: boolean;
11
13
  password?: string;
12
- releaseDate?: string;
14
+ lang?: string;
13
15
  onUnlock?: () => void;
14
16
  showPasswordLabel?: string;
15
17
  hidePasswordLabel?: string;
16
18
  unlockBodyClass?: string;
17
19
  }
18
- declare function PasswordGate({ title, subtitle, datePrefix, underConstruction, placeholder, buttonText, enabled, password, releaseDate, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
20
+ /**
21
+ * PasswordGate
22
+ *
23
+ * Why:
24
+ * Full-screen modal overlay that restricts site access during pre-launch or staging phases.
25
+ * Validates user input against a configured password, persists unlock state in localStorage, and
26
+ * removes the locking CSS class from the document root.
27
+ *
28
+ * Inputs:
29
+ * - props: PasswordGateProps including localized copy strings (title, subtitle, message, placeholder, buttonText),
30
+ * legacy date props (datePrefix, releaseDate, underConstruction), configuration (enabled, password, unlockBodyClass, lang),
31
+ * and callback (onUnlock).
32
+ *
33
+ * Output:
34
+ * - JSX.Element | null (renders null if disabled or already unlocked).
35
+ *
36
+ * Edge cases & handling:
37
+ * - Fallback copy: If `message` is passed, it takes precedence. If `releaseDate` is passed, it preserves
38
+ * legacy `{datePrefix} {releaseDate} {underConstruction}` display. If neither is passed, it detects
39
+ * locale (via `lang` prop or document.documentElement.lang) and renders "¡Próximamente!" for Spanish
40
+ * or "Coming soon!" for English.
41
+ * - Accessibility landmark: Wraps the gate in <main id="main-content" role="main"> so automated audits
42
+ * and screen readers detect a primary landmark when the site's normal content is locked out.
43
+ * - Mobile touch targets: The eye toggle button is explicitly sized to 44x44px (w-11 h-11)
44
+ * to satisfy WCAG 2.5.5 minimum tap target requirements.
45
+ * - LocalStorage sync: Checks localStorage on initial load to avoid flash of locked state if previously unlocked.
46
+ * - Graceful exit: Triggers an opacity fade-out before removing itself from the DOM tree.
47
+ */
48
+ declare function PasswordGate({ title, subtitle, message, datePrefix, underConstruction, releaseDate, placeholder, buttonText, enabled, password, lang, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
19
49
 
20
50
  export { type PasswordGateProps, PasswordGate as default };
package/dist/index.d.ts CHANGED
@@ -3,18 +3,48 @@ import React from 'react';
3
3
  interface PasswordGateProps {
4
4
  title: string;
5
5
  subtitle: string;
6
- datePrefix: string;
7
- underConstruction: string;
8
6
  placeholder: string;
9
7
  buttonText: string;
8
+ message?: string;
9
+ datePrefix?: string;
10
+ underConstruction?: string;
11
+ releaseDate?: string;
10
12
  enabled?: boolean;
11
13
  password?: string;
12
- releaseDate?: string;
14
+ lang?: string;
13
15
  onUnlock?: () => void;
14
16
  showPasswordLabel?: string;
15
17
  hidePasswordLabel?: string;
16
18
  unlockBodyClass?: string;
17
19
  }
18
- declare function PasswordGate({ title, subtitle, datePrefix, underConstruction, placeholder, buttonText, enabled, password, releaseDate, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
20
+ /**
21
+ * PasswordGate
22
+ *
23
+ * Why:
24
+ * Full-screen modal overlay that restricts site access during pre-launch or staging phases.
25
+ * Validates user input against a configured password, persists unlock state in localStorage, and
26
+ * removes the locking CSS class from the document root.
27
+ *
28
+ * Inputs:
29
+ * - props: PasswordGateProps including localized copy strings (title, subtitle, message, placeholder, buttonText),
30
+ * legacy date props (datePrefix, releaseDate, underConstruction), configuration (enabled, password, unlockBodyClass, lang),
31
+ * and callback (onUnlock).
32
+ *
33
+ * Output:
34
+ * - JSX.Element | null (renders null if disabled or already unlocked).
35
+ *
36
+ * Edge cases & handling:
37
+ * - Fallback copy: If `message` is passed, it takes precedence. If `releaseDate` is passed, it preserves
38
+ * legacy `{datePrefix} {releaseDate} {underConstruction}` display. If neither is passed, it detects
39
+ * locale (via `lang` prop or document.documentElement.lang) and renders "¡Próximamente!" for Spanish
40
+ * or "Coming soon!" for English.
41
+ * - Accessibility landmark: Wraps the gate in <main id="main-content" role="main"> so automated audits
42
+ * and screen readers detect a primary landmark when the site's normal content is locked out.
43
+ * - Mobile touch targets: The eye toggle button is explicitly sized to 44x44px (w-11 h-11)
44
+ * to satisfy WCAG 2.5.5 minimum tap target requirements.
45
+ * - LocalStorage sync: Checks localStorage on initial load to avoid flash of locked state if previously unlocked.
46
+ * - Graceful exit: Triggers an opacity fade-out before removing itself from the DOM tree.
47
+ */
48
+ declare function PasswordGate({ title, subtitle, message, datePrefix, underConstruction, releaseDate, placeholder, buttonText, enabled, password, lang, onUnlock, showPasswordLabel, hidePasswordLabel, unlockBodyClass }: PasswordGateProps): React.JSX.Element;
19
49
 
20
50
  export { type PasswordGateProps, PasswordGate as default };
package/dist/index.js CHANGED
@@ -36,13 +36,15 @@ var import_react = __toESM(require("react"));
36
36
  function PasswordGate({
37
37
  title,
38
38
  subtitle,
39
- datePrefix,
40
- underConstruction,
39
+ message,
40
+ datePrefix = "",
41
+ underConstruction = "",
42
+ releaseDate = "",
41
43
  placeholder,
42
44
  buttonText,
43
45
  enabled = true,
44
46
  password = "",
45
- releaseDate = "",
47
+ lang,
46
48
  onUnlock,
47
49
  showPasswordLabel = "Show password",
48
50
  hidePasswordLabel = "Hide password",
@@ -82,26 +84,38 @@ function PasswordGate({
82
84
  setTimeout(() => setError(false), 500);
83
85
  }
84
86
  };
87
+ const getDisplayMessage = () => {
88
+ if (message) {
89
+ return message;
90
+ }
91
+ if (releaseDate && releaseDate.trim()) {
92
+ return `${datePrefix} ${releaseDate} ${underConstruction}`.trim();
93
+ }
94
+ const isSpanish = lang?.toLowerCase().startsWith("es") || typeof document !== "undefined" && document.documentElement.lang?.toLowerCase().startsWith("es");
95
+ return isSpanish ? "\xA1Pr\xF3ximamente!" : "Coming soon!";
96
+ };
85
97
  return /* @__PURE__ */ import_react.default.createElement(
86
- "div",
98
+ "main",
87
99
  {
100
+ id: "main-content",
101
+ role: "main",
88
102
  className: `fixed inset-0 z-50 flex items-center justify-center bg-base transition-opacity duration-500 ${unlocked ? "opacity-0 pointer-events-none" : "opacity-100"}`
89
103
  },
90
- /* @__PURE__ */ import_react.default.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ import_react.default.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ import_react.default.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ import_react.default.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ import_react.default.createElement("p", { className: "text-accent-muted font-bold mb-4" }, datePrefix, " ", releaseDate, " ", underConstruction), /* @__PURE__ */ import_react.default.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ import_react.default.createElement(
104
+ /* @__PURE__ */ import_react.default.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ import_react.default.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ import_react.default.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ import_react.default.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ import_react.default.createElement("p", { className: "text-accent-muted font-bold mb-4" }, getDisplayMessage()), /* @__PURE__ */ import_react.default.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ import_react.default.createElement(
91
105
  "input",
92
106
  {
93
107
  type: showPassword ? "text" : "password",
94
108
  placeholder,
95
109
  value: passwordInput,
96
110
  onChange: (e) => setPasswordInput(e.target.value),
97
- className: `w-full px-4 py-2 pr-10 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
111
+ className: `w-full px-4 py-2 pr-12 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
98
112
  }
99
113
  ), /* @__PURE__ */ import_react.default.createElement(
100
114
  "button",
101
115
  {
102
116
  type: "button",
103
117
  onClick: () => setShowPassword(!showPassword),
104
- className: "absolute inset-y-0 right-0 pr-3 flex items-center text-body hover:text-heading",
118
+ className: "pnp-pg-toggle-btn absolute inset-y-0 right-0 text-body hover:text-heading",
105
119
  "aria-label": showPassword ? hidePasswordLabel : showPasswordLabel
106
120
  },
107
121
  showPassword ? /* @__PURE__ */ import_react.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" })) : /* @__PURE__ */ import_react.default.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }), /* @__PURE__ */ import_react.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }))
@@ -114,6 +128,15 @@ function PasswordGate({
114
128
  buttonText
115
129
  )))),
116
130
  /* @__PURE__ */ import_react.default.createElement("style", null, `
131
+ .pnp-pg-toggle-btn {
132
+ width: 44px !important;
133
+ height: 44px !important;
134
+ min-width: 44px !important;
135
+ min-height: 44px !important;
136
+ display: flex !important;
137
+ align-items: center !important;
138
+ justify-content: center !important;
139
+ }
117
140
  @keyframes shake {
118
141
  0%, 100% { transform: translateX(0); }
119
142
  25% { transform: translateX(-5px); }
package/dist/index.mjs CHANGED
@@ -3,13 +3,15 @@ import React, { useState } from "react";
3
3
  function PasswordGate({
4
4
  title,
5
5
  subtitle,
6
- datePrefix,
7
- underConstruction,
6
+ message,
7
+ datePrefix = "",
8
+ underConstruction = "",
9
+ releaseDate = "",
8
10
  placeholder,
9
11
  buttonText,
10
12
  enabled = true,
11
13
  password = "",
12
- releaseDate = "",
14
+ lang,
13
15
  onUnlock,
14
16
  showPasswordLabel = "Show password",
15
17
  hidePasswordLabel = "Hide password",
@@ -49,26 +51,38 @@ function PasswordGate({
49
51
  setTimeout(() => setError(false), 500);
50
52
  }
51
53
  };
54
+ const getDisplayMessage = () => {
55
+ if (message) {
56
+ return message;
57
+ }
58
+ if (releaseDate && releaseDate.trim()) {
59
+ return `${datePrefix} ${releaseDate} ${underConstruction}`.trim();
60
+ }
61
+ const isSpanish = lang?.toLowerCase().startsWith("es") || typeof document !== "undefined" && document.documentElement.lang?.toLowerCase().startsWith("es");
62
+ return isSpanish ? "\xA1Pr\xF3ximamente!" : "Coming soon!";
63
+ };
52
64
  return /* @__PURE__ */ React.createElement(
53
- "div",
65
+ "main",
54
66
  {
67
+ id: "main-content",
68
+ role: "main",
55
69
  className: `fixed inset-0 z-50 flex items-center justify-center bg-base transition-opacity duration-500 ${unlocked ? "opacity-0 pointer-events-none" : "opacity-100"}`
56
70
  },
57
- /* @__PURE__ */ React.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ React.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ React.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ React.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ React.createElement("p", { className: "text-accent-muted font-bold mb-4" }, datePrefix, " ", releaseDate, " ", underConstruction), /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React.createElement(
71
+ /* @__PURE__ */ React.createElement("div", { className: "max-w-2xl px-6 text-center" }, /* @__PURE__ */ React.createElement("h1", { className: "text-4xl md:text-5xl font-bold text-heading mb-6" }, title), /* @__PURE__ */ React.createElement("p", { className: "text-lg text-body mb-8" }, subtitle), /* @__PURE__ */ React.createElement("div", { className: "bg-surface p-8 rounded-xl shadow-lg inline-block border border-accent-muted" }, /* @__PURE__ */ React.createElement("p", { className: "text-accent-muted font-bold mb-4" }, getDisplayMessage()), /* @__PURE__ */ React.createElement("form", { onSubmit: handleSubmit, className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React.createElement(
58
72
  "input",
59
73
  {
60
74
  type: showPassword ? "text" : "password",
61
75
  placeholder,
62
76
  value: passwordInput,
63
77
  onChange: (e) => setPasswordInput(e.target.value),
64
- className: `w-full px-4 py-2 pr-10 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
78
+ className: `w-full px-4 py-2 pr-12 bg-base border rounded text-body focus:outline-none focus:ring-2 focus:ring-accent transition-transform ${error ? "border-red-500 animate-[shake_0.5s_ease-in-out]" : "border-accent-muted"}`
65
79
  }
66
80
  ), /* @__PURE__ */ React.createElement(
67
81
  "button",
68
82
  {
69
83
  type: "button",
70
84
  onClick: () => setShowPassword(!showPassword),
71
- className: "absolute inset-y-0 right-0 pr-3 flex items-center text-body hover:text-heading",
85
+ className: "pnp-pg-toggle-btn absolute inset-y-0 right-0 text-body hover:text-heading",
72
86
  "aria-label": showPassword ? hidePasswordLabel : showPasswordLabel
73
87
  },
74
88
  showPassword ? /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" })) : /* @__PURE__ */ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", className: "w-5 h-5" }, /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" }), /* @__PURE__ */ React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }))
@@ -81,6 +95,15 @@ function PasswordGate({
81
95
  buttonText
82
96
  )))),
83
97
  /* @__PURE__ */ React.createElement("style", null, `
98
+ .pnp-pg-toggle-btn {
99
+ width: 44px !important;
100
+ height: 44px !important;
101
+ min-width: 44px !important;
102
+ min-height: 44px !important;
103
+ display: flex !important;
104
+ align-items: center !important;
105
+ justify-content: center !important;
106
+ }
84
107
  @keyframes shake {
85
108
  0%, 100% { transform: translateX(0); }
86
109
  25% { transform: translateX(-5px); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the_disreputable_doggo/password-gate",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Password Gate component for Pier & Pixel templates",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",