popup-button-plugin 0.0.0-stage → 0.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
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # popup-button-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A tiny, **framework-free** web component: register content by name,
4
+ and `<popup-button name="...">` automatically shows it in a popup when
5
+ clicked. No React/Angular/Vue/Stencil required to use it — works in
6
+ any HTML page as-is, and inside React/Angular/Vue too since it's a
7
+ standard custom element.
8
+
9
+ See the full design write-up and integration steps in
10
+ [popup-button-prd.md](./popup-button-prd.md) and
11
+ [popup-button-integration-guide.md](./popup-button-integration-guide.md).
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ npm install
17
+ npm run build
18
+ ```
19
+
20
+ ```ts
21
+ import { registerButtonContent } from 'popup-button-plugin';
22
+
23
+ registerButtonContent('save-info', {
24
+ title: 'Save changes?',
25
+ body: 'This will overwrite your previous draft.',
26
+ });
27
+ ```
28
+
29
+ ```html
30
+ <popup-button name="save-info" label="Save"></popup-button>
31
+ ```
32
+
33
+ Open `examples/index.html` (via a local static server, e.g. `npx serve .`)
34
+ to see it working.
35
+
36
+ ## Why plain text only, no HTML content?
37
+
38
+ Registered `body` content is inserted as `textContent`/escaped, never
39
+ as raw HTML. This means it can never execute a script, even if the
40
+ content came from somewhere less trusted — safety by default.
@@ -0,0 +1,3 @@
1
+ export { registerButtonContent, getButtonContent, unregisterButtonContent } from './popup-registry';
2
+ export type { PopupContent } from './popup-registry';
3
+ export { PopupButton } from './popup-button';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // This is the single file an app imports. Importing it:
2
+ // 1. Registers the <popup-button> custom element (side effect of
3
+ // the import below — customElements.define runs once).
4
+ // 2. Gives the app the registry functions it needs to call.
5
+ export { registerButtonContent, getButtonContent, unregisterButtonContent } from './popup-registry';
6
+ export { PopupButton } from './popup-button';
@@ -0,0 +1,25 @@
1
+ export declare class PopupButton extends HTMLElement {
2
+ private isOpen;
3
+ private content?;
4
+ constructor();
5
+ /**
6
+ * Runs once, the first time this tag is attached to the page.
7
+ * (Equivalent to a component's "mount"/"componentDidMount".)
8
+ */
9
+ connectedCallback(): void;
10
+ /** Reads the `name` HTML attribute, e.g. <popup-button name="save-info">. */
11
+ private get buttonName();
12
+ /** Reads the `label` HTML attribute (button text), with a default. */
13
+ private get label();
14
+ /** Runs when the visible <button> inside our shadow DOM is clicked. */
15
+ private handleButtonClick;
16
+ /** Runs when the popup's close button or backdrop is clicked. */
17
+ private handleClose;
18
+ /**
19
+ * Rebuilds this element's shadow DOM to match current state
20
+ * (isOpen / content). There's no automatic re-render like Stencil —
21
+ * we call this ourselves every time state changes.
22
+ */
23
+ private render;
24
+ private renderPopupMarkup;
25
+ }
@@ -0,0 +1,155 @@
1
+ // =====================================================================
2
+ // <popup-button> — a native Web Component (no framework, no build
3
+ // tool like Stencil needed to USE it — just plain JS/HTML).
4
+ // ---------------------------------------------------------------------
5
+ // WHY THIS FILE EXISTS / WHAT IT DOES:
6
+ // This is the ONE tag the app puts in HTML. On click, it:
7
+ // 1. Looks up content for its `name` attribute in the registry.
8
+ // 2. If found -> shows a popup (modal) with that content.
9
+ // 3. If NOT found -> fires a "popup-content-missing" event (so the
10
+ // app/dev can catch a typo instead of nothing happening silently).
11
+ //
12
+ // HOW CUSTOM ELEMENTS WORK (since there's no Stencil here to hide it):
13
+ // - A custom element is just a JS class that `extends HTMLElement`.
14
+ // - `customElements.define('popup-button', PopupButton)` registers the
15
+ // tag name <popup-button> to use this class, globally, once.
16
+ // - `connectedCallback()` runs automatically when the browser inserts
17
+ // the tag into the page (like a component "mount").
18
+ // - `attachShadow({ mode: 'open' })` gives the element its own isolated
19
+ // DOM + CSS, so our button/modal styles can't leak out, and page
20
+ // styles can't accidentally break our popup.
21
+ // =====================================================================
22
+ import { getButtonContent } from './popup-registry';
23
+ export class PopupButton extends HTMLElement {
24
+ constructor() {
25
+ super();
26
+ // Tracks whether the popup is currently visible.
27
+ this.isOpen = false;
28
+ /** Runs when the visible <button> inside our shadow DOM is clicked. */
29
+ this.handleButtonClick = () => {
30
+ const found = getButtonContent(this.buttonName);
31
+ if (!found) {
32
+ // Nothing registered for this name — tell the outside world via
33
+ // a CustomEvent, instead of failing silently. `bubbles: true` so
34
+ // the app can listen on a parent element too, not just this tag.
35
+ this.dispatchEvent(new CustomEvent('popup-content-missing', {
36
+ detail: { name: this.buttonName },
37
+ bubbles: true,
38
+ }));
39
+ return;
40
+ }
41
+ this.content = found;
42
+ this.isOpen = true;
43
+ this.render();
44
+ };
45
+ /** Runs when the popup's close button or backdrop is clicked. */
46
+ this.handleClose = () => {
47
+ this.isOpen = false;
48
+ this.render();
49
+ };
50
+ // `mode: 'open'` means outside JS *can* still read this.shadowRoot
51
+ // if it really needs to (useful for testing); 'closed' would hide it.
52
+ this.attachShadow({ mode: 'open' });
53
+ }
54
+ /**
55
+ * Runs once, the first time this tag is attached to the page.
56
+ * (Equivalent to a component's "mount"/"componentDidMount".)
57
+ */
58
+ connectedCallback() {
59
+ this.render();
60
+ }
61
+ /** Reads the `name` HTML attribute, e.g. <popup-button name="save-info">. */
62
+ get buttonName() {
63
+ var _a;
64
+ return (_a = this.getAttribute('name')) !== null && _a !== void 0 ? _a : '';
65
+ }
66
+ /** Reads the `label` HTML attribute (button text), with a default. */
67
+ get label() {
68
+ var _a;
69
+ return (_a = this.getAttribute('label')) !== null && _a !== void 0 ? _a : 'Click me';
70
+ }
71
+ /**
72
+ * Rebuilds this element's shadow DOM to match current state
73
+ * (isOpen / content). There's no automatic re-render like Stencil —
74
+ * we call this ourselves every time state changes.
75
+ */
76
+ render() {
77
+ const shadow = this.shadowRoot;
78
+ shadow.innerHTML = `
79
+ <style>${STYLES}</style>
80
+ <button class="btn">${escapeHtml(this.label)}</button>
81
+ ${this.isOpen ? this.renderPopupMarkup() : ''}
82
+ `;
83
+ // Re-attach listeners every render, since innerHTML = '...' above
84
+ // destroys and recreates the actual DOM nodes (old listeners are
85
+ // gone with them).
86
+ shadow.querySelector('.btn').addEventListener('click', this.handleButtonClick);
87
+ if (this.isOpen) {
88
+ shadow.querySelector('.backdrop').addEventListener('click', this.handleClose);
89
+ shadow.querySelector('.close').addEventListener('click', this.handleClose);
90
+ // Stop clicks INSIDE the popup box from bubbling to the backdrop
91
+ // (otherwise clicking the popup content would also close it).
92
+ shadow.querySelector('.popup').addEventListener('click', (e) => e.stopPropagation());
93
+ }
94
+ }
95
+ renderPopupMarkup() {
96
+ var _a, _b, _c, _d;
97
+ const title = (_b = (_a = this.content) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : '';
98
+ const body = (_d = (_c = this.content) === null || _c === void 0 ? void 0 : _c.body) !== null && _d !== void 0 ? _d : '';
99
+ return `
100
+ <div class="backdrop">
101
+ <div class="popup">
102
+ <div class="popup__header">
103
+ <span class="popup__title">${escapeHtml(title)}</span>
104
+ <button class="close" aria-label="Close">&times;</button>
105
+ </div>
106
+ <div class="popup__body">${escapeHtml(body)}</div>
107
+ </div>
108
+ </div>
109
+ `;
110
+ }
111
+ }
112
+ // Plain-text content only (see PRD): we escape it before inserting
113
+ // into innerHTML, so registered content is always treated as TEXT,
114
+ // never as HTML/script — this is what keeps this component safe from
115
+ // XSS even though we build markup with innerHTML.
116
+ function escapeHtml(value) {
117
+ const div = document.createElement('div');
118
+ div.textContent = value;
119
+ return div.innerHTML;
120
+ }
121
+ const STYLES = `
122
+ :host { display: inline-block; font-family: inherit; }
123
+ .btn {
124
+ padding: 8px 16px;
125
+ border-radius: 6px;
126
+ border: none;
127
+ background: #2563eb;
128
+ color: #fff;
129
+ font-size: 14px;
130
+ cursor: pointer;
131
+ }
132
+ .btn:hover { background: #1d4ed8; }
133
+ .backdrop {
134
+ position: fixed; inset: 0;
135
+ background: rgba(0,0,0,0.5);
136
+ display: flex; align-items: center; justify-content: center;
137
+ z-index: 1000;
138
+ }
139
+ .popup {
140
+ background: #fff; border-radius: 10px;
141
+ min-width: 320px; max-width: 90vw;
142
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
143
+ }
144
+ .popup__header {
145
+ display: flex; align-items: center; justify-content: space-between;
146
+ padding: 16px; border-bottom: 1px solid #e5e7eb;
147
+ }
148
+ .popup__title { font-size: 16px; font-weight: 700; color: #111827; }
149
+ .close { background: none; border: none; font-size: 20px; cursor: pointer; color: #6b7280; }
150
+ .popup__body { padding: 16px; color: #374151; }
151
+ `;
152
+ // Registers the <popup-button> tag globally. This file only needs to
153
+ // be imported ONCE anywhere in the app for the tag to start working
154
+ // everywhere on the page.
155
+ customElements.define('popup-button', PopupButton);
@@ -0,0 +1,20 @@
1
+ /** The shape of content a popup can display. */
2
+ export interface PopupContent {
3
+ title?: string;
4
+ body: string;
5
+ }
6
+ /**
7
+ * Call this once per button `name` (usually right when your app starts)
8
+ * to say what should appear in the popup for that name.
9
+ *
10
+ * Example: registerButtonContent('save-info', { title: 'Save?', body: 'Sure?' })
11
+ */
12
+ export declare function registerButtonContent(name: string, content: PopupContent): void;
13
+ /**
14
+ * Used internally by <popup-button> on click, to find what to show.
15
+ * Returns undefined if nothing was registered for this name — almost
16
+ * always means a typo somewhere.
17
+ */
18
+ export declare function getButtonContent(name: string): PopupContent | undefined;
19
+ /** Optional cleanup — e.g. remove a mapping when a page/feature unmounts. */
20
+ export declare function unregisterButtonContent(name: string): void;
@@ -0,0 +1,34 @@
1
+ // =====================================================================
2
+ // POPUP REGISTRY
3
+ // ---------------------------------------------------------------------
4
+ // WHY THIS FILE EXISTS:
5
+ // The app using this plugin should only have to say, ONCE:
6
+ // "when a button named X is clicked, show THIS content"
7
+ // We store that (name -> content) mapping here, in memory, so ANY
8
+ // <popup-button> on the page can look it up by its `name` attribute
9
+ // at click time. One shared "address book" for the whole page.
10
+ // =====================================================================
11
+ // Map (not a plain {} object) so a button literally named "toString"
12
+ // or "constructor" can never collide with built-in object properties.
13
+ const registry = new Map();
14
+ /**
15
+ * Call this once per button `name` (usually right when your app starts)
16
+ * to say what should appear in the popup for that name.
17
+ *
18
+ * Example: registerButtonContent('save-info', { title: 'Save?', body: 'Sure?' })
19
+ */
20
+ export function registerButtonContent(name, content) {
21
+ registry.set(name, content);
22
+ }
23
+ /**
24
+ * Used internally by <popup-button> on click, to find what to show.
25
+ * Returns undefined if nothing was registered for this name — almost
26
+ * always means a typo somewhere.
27
+ */
28
+ export function getButtonContent(name) {
29
+ return registry.get(name);
30
+ }
31
+ /** Optional cleanup — e.g. remove a mapping when a page/feature unmounts. */
32
+ export function unregisterButtonContent(name) {
33
+ registry.delete(name);
34
+ }
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "popup-button-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A tiny, standalone, framework-free button-with-popup web component: register content by name, and <popup-button name=\"...\"> automatically shows it when clicked.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist/"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "start": "tsc -p tsconfig.json --watch",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": ["web-components", "custom-elements", "popup", "modal", "button"],
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "typescript": "^5.6.0"
20
+ }
21
+ }