claude-code-popup 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.
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const cardsEl = document.getElementById('cn-cards');
4
+ const countEl = document.getElementById('cn-count');
5
+ const settingsBtn = document.getElementById('cn-settings');
6
+ const clearBtn = document.getElementById('cn-clear');
7
+ const body = document.body;
8
+
9
+ let t = {};
10
+ let i18nReady = false;
11
+ let pendingState = null;
12
+ const MAX_VISIBLE_CARDS = 3;
13
+ const PROGRESS_DURATION = 30; // seconds — fixed timer bar, independent of config.duration
14
+ const dismissHandles = new Map();
15
+
16
+ (async function bootstrapI18n() {
17
+ const i18n = await window.cn.i18n();
18
+ t = i18n.strings;
19
+ document.documentElement.lang = i18n.locale;
20
+ settingsBtn.title = t.notify_settings;
21
+ settingsBtn.setAttribute('aria-label', t.notify_settings);
22
+ clearBtn.title = t.notify_clear_all;
23
+ clearBtn.setAttribute('aria-label', t.notify_clear_all);
24
+ i18nReady = true;
25
+ if (pendingState) {
26
+ const queued = pendingState;
27
+ pendingState = null;
28
+ renderState(queued);
29
+ }
30
+ })();
31
+
32
+ settingsBtn.addEventListener('click', (e) => {
33
+ e.stopPropagation();
34
+ window.cn.openSetup();
35
+ });
36
+ clearBtn.addEventListener('click', (e) => {
37
+ e.stopPropagation();
38
+ window.cn.clearAll();
39
+ });
40
+
41
+ function shortenDir(p) {
42
+ if (!p) return '';
43
+ const parts = p.replace(/\\/g, '/').split('/').filter(Boolean);
44
+ if (parts.length <= 2) return p;
45
+ return `…/${parts.slice(-2).join('/')}`;
46
+ }
47
+
48
+ function cssEscape(value) {
49
+ return String(value).replace(/(["\\\]])/g, '\\$1');
50
+ }
51
+
52
+ function applyDesign(config) {
53
+ body.dataset.design = String(config.design || 1);
54
+ }
55
+
56
+ function buildCard(card, config) {
57
+ const li = document.createElement('li');
58
+ li.className = 'cn-card';
59
+ li.dataset.id = card.id;
60
+ li.dataset.dir = card.dir;
61
+ li.title = t.notify_card_tooltip || '';
62
+ li.addEventListener('click', (e) => {
63
+ if (e.target.closest('.cn-card__close')) return;
64
+ window.cn.focusOrigin(card.dir);
65
+ });
66
+
67
+ const dir = document.createElement('div');
68
+ dir.className = 'cn-card__dir';
69
+ dir.textContent = `📁 ${shortenDir(card.dir)}`;
70
+ dir.title = card.dir;
71
+ li.appendChild(dir);
72
+
73
+ const title = document.createElement('div');
74
+ title.className = 'cn-card__title';
75
+ title.textContent = (config.message?.title?.trim()) || t.notify_default_title || 'Claude needs your input';
76
+ li.appendChild(title);
77
+
78
+ const close = document.createElement('button');
79
+ close.className = 'cn-card__close';
80
+ close.type = 'button';
81
+ close.textContent = '×';
82
+ close.title = t.notify_dismiss || '';
83
+ close.addEventListener('click', (e) => {
84
+ e.stopPropagation();
85
+ dismiss(card.id);
86
+ });
87
+ li.appendChild(close);
88
+
89
+ // Fixed 30-second "newness" timer bar. Independent of config.duration.
90
+ // Newer cards visibly retain more bar; after 30s the bar is removed entirely.
91
+ const progress = document.createElement('div');
92
+ progress.className = 'cn-card__progress';
93
+ const bar = document.createElement('span');
94
+ bar.className = 'cn-card__progress-bar';
95
+ bar.style.animationDuration = `${PROGRESS_DURATION}s`;
96
+ progress.appendChild(bar);
97
+ li.appendChild(progress);
98
+ setTimeout(() => progress.remove(), PROGRESS_DURATION * 1000);
99
+
100
+ return li;
101
+ }
102
+
103
+ async function dismiss(id) {
104
+ const li = cardsEl.querySelector(`[data-id="${cssEscape(id)}"]`);
105
+ if (li) li.classList.add('cn-card--leaving');
106
+ await window.cn.dismiss(id);
107
+ }
108
+
109
+ function scheduleAutoDismiss(card, durationSec) {
110
+ if (!durationSec || durationSec <= 0) return;
111
+ const handle = setTimeout(() => dismiss(card.id), durationSec * 1000);
112
+ dismissHandles.set(card.id, handle);
113
+ }
114
+
115
+ function reportHeight() {
116
+ const shell = document.querySelector('.cn-shell');
117
+ const header = document.querySelector('.cn-header');
118
+ if (!shell || !header) return;
119
+
120
+ const allCards = Array.from(cardsEl.children).filter(
121
+ (el) => !el.classList.contains('cn-card--leaving'),
122
+ );
123
+
124
+ if (allCards.length <= MAX_VISIBLE_CARDS) {
125
+ cardsEl.style.maxHeight = '';
126
+ const h = Math.ceil(shell.getBoundingClientRect().height) + 4;
127
+ window.cn.resize(h);
128
+ return;
129
+ }
130
+
131
+ const cardsRect = cardsEl.getBoundingClientRect();
132
+ const lastVisible = allCards[MAX_VISIBLE_CARDS - 1].getBoundingClientRect();
133
+ const cardsStyles = getComputedStyle(cardsEl);
134
+ const padBottom = parseFloat(cardsStyles.paddingBottom) || 0;
135
+ const cappedListHeight = Math.ceil(lastVisible.bottom - cardsRect.top + padBottom);
136
+ cardsEl.style.maxHeight = `${cappedListHeight}px`;
137
+
138
+ const headerH = header.getBoundingClientRect().height;
139
+ window.cn.resize(Math.ceil(headerH + cappedListHeight) + 4);
140
+ }
141
+
142
+ function renderState(state) {
143
+ applyDesign(state.config);
144
+ countEl.textContent = String(state.cards.length);
145
+
146
+ const seen = new Set(state.cards.map((c) => c.id));
147
+ for (const child of Array.from(cardsEl.children)) {
148
+ if (!seen.has(child.dataset.id)) {
149
+ const id = child.dataset.id;
150
+ child.classList.add('cn-card--leaving');
151
+ setTimeout(() => child.remove(), 180);
152
+ const handle = dismissHandles.get(id);
153
+ if (handle) {
154
+ clearTimeout(handle);
155
+ dismissHandles.delete(id);
156
+ }
157
+ }
158
+ }
159
+
160
+ for (const card of state.cards) {
161
+ if (cardsEl.querySelector(`[data-id="${cssEscape(card.id)}"]`)) continue;
162
+ const node = buildCard(card, state.config);
163
+ cardsEl.appendChild(node);
164
+ scheduleAutoDismiss(card, state.config.duration);
165
+ }
166
+
167
+ requestAnimationFrame(reportHeight);
168
+ }
169
+
170
+ window.cn.onState((state) => {
171
+ if (!i18nReady) {
172
+ pendingState = state;
173
+ return;
174
+ }
175
+ renderState(state);
176
+ });
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { contextBridge, ipcRenderer } = require('electron');
4
+
5
+ contextBridge.exposeInMainWorld('cn', {
6
+ getConfig: () => ipcRenderer.invoke('cn:getConfig'),
7
+ saveConfig: (next) => ipcRenderer.invoke('cn:saveConfig', next),
8
+ installHooks: () => ipcRenderer.invoke('cn:installHooks'),
9
+ uninstallHooks: () => ipcRenderer.invoke('cn:uninstallHooks'),
10
+ platform: () => ipcRenderer.invoke('cn:platform'),
11
+ openExternal: (url) => ipcRenderer.invoke('cn:openExternal', url),
12
+ dismiss: (id) => ipcRenderer.invoke('cn:dismiss', id),
13
+ clearAll: () => ipcRenderer.invoke('cn:clearAll'),
14
+ openSetup: () => ipcRenderer.invoke('cn:openSetup'),
15
+ focusOrigin: (dir) => ipcRenderer.invoke('cn:focusOrigin', dir),
16
+ resize: (height) => ipcRenderer.invoke('cn:resize', height),
17
+ openTerminal: (dir) => ipcRenderer.invoke('cn:openTerminal', dir),
18
+ i18n: () => ipcRenderer.invoke('cn:i18n'),
19
+ onState: (cb) => {
20
+ const handler = (_e, state) => cb(state);
21
+ ipcRenderer.on('state', handler);
22
+ return () => ipcRenderer.removeListener('state', handler);
23
+ },
24
+ });
@@ -0,0 +1,185 @@
1
+ :root {
2
+ --setup-bg: #fafbfc;
3
+ --setup-fg: #1a1d21;
4
+ --setup-muted: #606770;
5
+ --setup-line: #e5e7eb;
6
+ --setup-card: #ffffff;
7
+ --setup-input-bg: #ffffff;
8
+ --setup-accent: #d97757;
9
+ --setup-accent-fg: #ffffff;
10
+ --setup-preview-bg: #f5f6f8;
11
+ --setup-shadow: 0 6px 18px rgba(15,17,21,0.08);
12
+ }
13
+
14
+ * { box-sizing: border-box; }
15
+ html, body {
16
+ margin: 0;
17
+ background: var(--setup-bg);
18
+ color: var(--setup-fg);
19
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Kaku Gothic ProN",
20
+ "Noto Sans JP", Roboto, Helvetica, Arial, sans-serif;
21
+ font-size: 13px;
22
+ -webkit-user-select: none;
23
+ }
24
+
25
+ .setup {
26
+ max-width: 660px;
27
+ margin: 0 auto;
28
+ padding: 28px 28px 96px;
29
+ }
30
+ .setup__head h1 { margin: 0 0 6px; font-size: 22px; }
31
+ .setup__head p { margin: 0 0 24px; color: var(--setup-muted); }
32
+
33
+ .setup__section {
34
+ background: var(--setup-card);
35
+ border: 1px solid var(--setup-line);
36
+ border-radius: 10px;
37
+ padding: 16px 18px;
38
+ margin-bottom: 14px;
39
+ }
40
+ .setup__section h2 { margin: 0 0 10px; font-size: 13px; font-weight: 600; }
41
+
42
+ .setup__row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
43
+ .setup__row label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
44
+ .setup__field { display: grid; grid-template-columns: 100px 1fr; align-items: center; gap: 10px; margin-top: 8px; }
45
+ .setup__field span { color: var(--setup-muted); }
46
+ .setup__field input,
47
+ .setup__field select {
48
+ border: 1px solid var(--setup-line);
49
+ border-radius: 6px;
50
+ padding: 6px 8px;
51
+ font: inherit;
52
+ color: var(--setup-fg);
53
+ background: var(--setup-input-bg);
54
+ }
55
+ .setup__save { color: #2f8f3a; font-size: 12px; }
56
+
57
+ .btn {
58
+ border: 1px solid var(--setup-line);
59
+ background: var(--setup-input-bg);
60
+ color: var(--setup-fg);
61
+ border-radius: 8px;
62
+ padding: 8px 14px;
63
+ font: inherit;
64
+ cursor: pointer;
65
+ }
66
+ .btn:hover { filter: brightness(0.96); }
67
+ .btn--primary { background: var(--setup-accent); color: var(--setup-accent-fg); border-color: transparent; }
68
+
69
+ .setup__footer {
70
+ position: fixed;
71
+ bottom: 0;
72
+ left: 0;
73
+ right: 0;
74
+ padding: 12px 28px;
75
+ background: var(--setup-bg);
76
+ border-top: 1px solid var(--setup-line);
77
+ display: flex;
78
+ justify-content: flex-end;
79
+ align-items: center;
80
+ gap: 12px;
81
+ }
82
+
83
+ /* --- design grid (always visible) --- */
84
+ .design__grid {
85
+ display: grid;
86
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
87
+ gap: 8px;
88
+ margin-bottom: 14px;
89
+ }
90
+ .design__option {
91
+ text-align: left;
92
+ padding: 10px;
93
+ border: 1px solid var(--setup-line);
94
+ border-radius: 8px;
95
+ background: var(--setup-input-bg);
96
+ cursor: pointer;
97
+ font: inherit;
98
+ color: var(--setup-fg);
99
+ }
100
+ .design__option:hover { filter: brightness(0.96); }
101
+ .design__option--active {
102
+ border-color: var(--setup-accent);
103
+ box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.18);
104
+ }
105
+ .design__optionTitle { font-weight: 600; margin-bottom: 2px; }
106
+ .design__optionDesc { color: var(--setup-muted); font-size: 11px; }
107
+
108
+ /* --- design preview --- */
109
+ .design__preview {
110
+ padding: 14px;
111
+ background: var(--setup-preview-bg);
112
+ border-radius: 8px;
113
+ }
114
+ .design__previewCard {
115
+ width: 320px;
116
+ margin: 0 auto;
117
+ border-radius: 10px;
118
+ background: #fff;
119
+ color: #1a1d21;
120
+ overflow: hidden;
121
+ box-shadow: var(--setup-shadow);
122
+ }
123
+ .design__previewHead {
124
+ display: flex;
125
+ justify-content: space-between;
126
+ align-items: center;
127
+ padding: 8px 12px;
128
+ background: #f5f6f8;
129
+ font-size: 12px;
130
+ font-weight: 600;
131
+ border-bottom: 1px solid rgba(0,0,0,0.06);
132
+ }
133
+ .design__previewHead span {
134
+ background: rgba(0,0,0,0.06);
135
+ padding: 1px 8px;
136
+ border-radius: 999px;
137
+ font-size: 11px;
138
+ color: #606770;
139
+ }
140
+ .design__previewBody { padding: 12px 14px; }
141
+ .design__previewDir { font-size: 11px; color: #606770; }
142
+ .design__previewTitle { font-size: 13px; font-weight: 600; margin-top: 4px; }
143
+ .design__previewBar {
144
+ height: 3px;
145
+ border-radius: 2px;
146
+ background: rgba(0, 0, 0, 0.08);
147
+ margin-top: 10px;
148
+ overflow: hidden;
149
+ }
150
+ .design__previewBar span {
151
+ display: block;
152
+ height: 100%;
153
+ width: 60%;
154
+ background: #d97757;
155
+ }
156
+
157
+ /* preview design variants */
158
+ .design__preview[data-design="2"] .design__previewCard { background: #23272d; color: #e6e8eb; border-left: 3px solid #5fa8ff; }
159
+ .design__preview[data-design="2"] .design__previewHead { background: #15171b; color: #f4f5f7; border-bottom-color: rgba(255,255,255,0.06); }
160
+ .design__preview[data-design="2"] .design__previewHead span { background: rgba(255,255,255,0.08); color: #c2c6cc; }
161
+ .design__preview[data-design="2"] .design__previewDir { color: #9aa0a8; }
162
+ .design__preview[data-design="2"] .design__previewTitle { color: #f4f5f7; }
163
+ .design__preview[data-design="2"] .design__previewBar { background: rgba(255,255,255,0.10); }
164
+
165
+ .design__preview[data-design="3"] .design__previewCard { background: #fffaf5; }
166
+ .design__preview[data-design="3"] .design__previewHead { background: linear-gradient(90deg, #d97757, #c95f3f); color: #fff; }
167
+ .design__preview[data-design="3"] .design__previewHead span { background: rgba(255,255,255,0.22); color: #fff; }
168
+
169
+ .design__preview[data-design="4"] .design__previewCard { background: #fff; border-left: 3px solid #d97757; }
170
+ .design__preview[data-design="4"] .design__previewHead { background: #fff8f3; }
171
+
172
+ .design__preview[data-design="5"] .design__previewCard { background: #23272d; color: #e6e8eb; }
173
+ .design__preview[data-design="5"] .design__previewHead { background: #15171b; color: #f4f5f7; }
174
+ .design__preview[data-design="5"] .design__previewHead span { background: rgba(255,255,255,0.08); color: #c2c6cc; }
175
+ .design__preview[data-design="5"] .design__previewDir { color: #9aa0a8; }
176
+ .design__preview[data-design="5"] .design__previewTitle { color: #f4f5f7; }
177
+ .design__preview[data-design="5"] .design__previewBar { background: rgba(255,255,255,0.10); }
178
+
179
+ /* duration */
180
+ .duration { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
181
+ .duration input[type="range"] { flex: 1; min-width: 200px; }
182
+ .duration small { width: 100%; color: var(--setup-muted); font-size: 11px; }
183
+
184
+ /* switch */
185
+ .switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
@@ -0,0 +1,76 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';" />
6
+ <title>claude-code-popup Settings</title>
7
+ <link rel="stylesheet" href="setup.css" />
8
+ </head>
9
+ <body>
10
+ <main class="setup">
11
+ <header class="setup__head">
12
+ <h1>claude-code-popup</h1>
13
+ <p id="subtitle"></p>
14
+ </header>
15
+
16
+ <section class="setup__section">
17
+ <h2 id="h-design"></h2>
18
+ <div id="designGrid" class="design__grid"></div>
19
+ <div id="designPreview" class="design__preview" data-design="1">
20
+ <div class="design__previewCard">
21
+ <div class="design__previewHead">🤖 Claude Code <span>1</span></div>
22
+ <div class="design__previewBody">
23
+ <div class="design__previewDir">📁 …/projects/web-app</div>
24
+ <div class="design__previewTitle" id="previewTitle"></div>
25
+ <div class="design__previewBar"><span></span></div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </section>
30
+
31
+ <section class="setup__section">
32
+ <h2 id="h-message"></h2>
33
+ <label class="setup__field">
34
+ <span id="l-title"></span>
35
+ <input id="msgTitle" type="text" maxlength="80" />
36
+ </label>
37
+ </section>
38
+
39
+ <section class="setup__section">
40
+ <h2 id="h-position"></h2>
41
+ <label class="setup__field">
42
+ <span id="l-position"></span>
43
+ <select id="position">
44
+ <option value="bottom-right" data-i18n="position_br"></option>
45
+ <option value="bottom-left" data-i18n="position_bl"></option>
46
+ <option value="top-right" data-i18n="position_tr"></option>
47
+ <option value="top-left" data-i18n="position_tl"></option>
48
+ </select>
49
+ </label>
50
+ </section>
51
+
52
+ <section class="setup__section">
53
+ <h2 id="h-duration"></h2>
54
+ <div class="duration">
55
+ <input id="duration" type="range" min="0" max="60" step="1" />
56
+ <output id="durationOut"></output>
57
+ <small id="duration-hint"></small>
58
+ </div>
59
+ </section>
60
+
61
+ <section class="setup__section">
62
+ <h2 id="h-sound"></h2>
63
+ <label class="switch">
64
+ <input id="sound" type="checkbox" />
65
+ <span id="l-sound"></span>
66
+ </label>
67
+ </section>
68
+
69
+ <footer class="setup__footer">
70
+ <span id="saveMsg" class="setup__save"></span>
71
+ <button id="save" type="button" class="btn btn--primary"></button>
72
+ </footer>
73
+ </main>
74
+ <script src="setup.js"></script>
75
+ </body>
76
+ </html>
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ let t = null; // i18n strings dict
4
+ let state = null; // current config in-memory
5
+
6
+ const els = {
7
+ subtitle: document.getElementById('subtitle'),
8
+ hDesign: document.getElementById('h-design'),
9
+ hMessage: document.getElementById('h-message'),
10
+ hPosition: document.getElementById('h-position'),
11
+ hDuration: document.getElementById('h-duration'),
12
+ hSound: document.getElementById('h-sound'),
13
+ lTitle: document.getElementById('l-title'),
14
+ lPosition: document.getElementById('l-position'),
15
+ lSound: document.getElementById('l-sound'),
16
+ durationHint: document.getElementById('duration-hint'),
17
+ designGrid: document.getElementById('designGrid'),
18
+ designPreview: document.getElementById('designPreview'),
19
+ previewTitle: document.getElementById('previewTitle'),
20
+ msgTitle: document.getElementById('msgTitle'),
21
+ position: document.getElementById('position'),
22
+ duration: document.getElementById('duration'),
23
+ durationOut: document.getElementById('durationOut'),
24
+ sound: document.getElementById('sound'),
25
+ save: document.getElementById('save'),
26
+ saveMsg: document.getElementById('saveMsg'),
27
+ };
28
+
29
+ function fmt(template, vars) {
30
+ return String(template).replace(/\{(\w+)\}/g, (_, k) => vars[k]);
31
+ }
32
+
33
+ function applyStaticTexts() {
34
+ els.subtitle.textContent = t.subtitle;
35
+ els.hDesign.textContent = t.section_design;
36
+ els.hMessage.textContent = t.section_message;
37
+ els.hPosition.textContent = t.section_position;
38
+ els.hDuration.textContent = t.section_duration;
39
+ els.hSound.textContent = t.section_sound;
40
+ els.lTitle.textContent = t.field_title;
41
+ els.lPosition.textContent = t.field_position;
42
+ els.lSound.textContent = t.sound_enable;
43
+ els.durationHint.textContent = t.duration_hint;
44
+ els.save.textContent = t.save;
45
+ els.msgTitle.placeholder = t.notify_default_title;
46
+
47
+ for (const opt of els.position.querySelectorAll('option[data-i18n]')) {
48
+ opt.textContent = t[opt.dataset.i18n] || opt.textContent;
49
+ }
50
+ }
51
+
52
+ function designCatalog() {
53
+ return [
54
+ { id: 1, title: t.design_1_title, desc: t.design_1_desc },
55
+ { id: 2, title: t.design_2_title, desc: t.design_2_desc },
56
+ { id: 3, title: t.design_3_title, desc: t.design_3_desc },
57
+ { id: 4, title: t.design_4_title, desc: t.design_4_desc },
58
+ { id: 5, title: t.design_5_title, desc: t.design_5_desc },
59
+ ];
60
+ }
61
+
62
+ function buildDesignGrid() {
63
+ els.designGrid.innerHTML = '';
64
+ for (const d of designCatalog()) {
65
+ const btn = document.createElement('button');
66
+ btn.type = 'button';
67
+ btn.className = 'design__option';
68
+ if (state.design === d.id) btn.classList.add('design__option--active');
69
+ btn.dataset.id = String(d.id);
70
+ btn.innerHTML = `
71
+ <div class="design__optionTitle">${d.id}. ${d.title}</div>
72
+ <div class="design__optionDesc">${d.desc}</div>
73
+ `;
74
+ btn.addEventListener('click', () => {
75
+ state.design = d.id;
76
+ buildDesignGrid();
77
+ applyDesignDisplay();
78
+ });
79
+ els.designGrid.appendChild(btn);
80
+ }
81
+ }
82
+
83
+ function applyDesignDisplay() {
84
+ els.designPreview.dataset.design = String(state.design || 1);
85
+ els.previewTitle.textContent = state.message?.title?.trim() || t.notify_default_title;
86
+ }
87
+
88
+ function applyDurationLabel() {
89
+ els.durationOut.textContent = state.duration === 0
90
+ ? t.duration_never
91
+ : fmt(t.duration_seconds, { n: state.duration });
92
+ }
93
+
94
+ function applyForm() {
95
+ els.msgTitle.value = state.message?.title || '';
96
+ els.position.value = state.position;
97
+ els.duration.value = String(state.duration);
98
+ els.sound.checked = !!state.sound;
99
+ applyDurationLabel();
100
+ applyDesignDisplay();
101
+ buildDesignGrid();
102
+ }
103
+
104
+ function bindEvents() {
105
+ els.msgTitle.addEventListener('input', () => {
106
+ state.message = state.message || {};
107
+ state.message.title = els.msgTitle.value;
108
+ applyDesignDisplay();
109
+ });
110
+ els.position.addEventListener('change', () => {
111
+ state.position = els.position.value;
112
+ });
113
+ els.duration.addEventListener('input', () => {
114
+ state.duration = Number(els.duration.value);
115
+ applyDurationLabel();
116
+ });
117
+ els.sound.addEventListener('change', () => {
118
+ state.sound = els.sound.checked;
119
+ });
120
+ els.save.addEventListener('click', async () => {
121
+ await window.cn.saveConfig(state);
122
+ els.saveMsg.textContent = t.saved;
123
+ setTimeout(() => (els.saveMsg.textContent = ''), 2000);
124
+ });
125
+ }
126
+
127
+ (async function init() {
128
+ const i18n = await window.cn.i18n();
129
+ t = i18n.strings;
130
+ document.documentElement.lang = i18n.locale;
131
+ applyStaticTexts();
132
+
133
+ state = await window.cn.getConfig();
134
+ state.message = state.message || { title: '' };
135
+ applyForm();
136
+ bindEvents();
137
+ })();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "claude-code-popup",
3
+ "version": "0.1.0",
4
+ "description": "Desktop popup notifications for Claude Code (CLI) Notification hooks. Stackable cards, 5 designs, cross-platform via Electron.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "notification",
9
+ "popup",
10
+ "electron",
11
+ "cli",
12
+ "desktop"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Tom-Canon-Rock",
16
+ "bin": {
17
+ "claude-code-popup": "bin/cli.js"
18
+ },
19
+ "main": "electron/main.js",
20
+ "files": [
21
+ "bin",
22
+ "src",
23
+ "electron",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "start": "electron .",
28
+ "setup": "node bin/cli.js setup",
29
+ "postinstall": "node bin/postinstall.js",
30
+ "preuninstall": "node bin/preuninstall.js"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "electron": "^31.0.0"
37
+ }
38
+ }
package/src/config.js ADDED
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.claude-code-popup');
8
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
9
+ const RUNTIME_PATH = path.join(CONFIG_DIR, 'runtime.json');
10
+
11
+ const DEFAULT_CONFIG = {
12
+ design: 1,
13
+ message: {
14
+ // Empty by default; the renderer falls back to the localised
15
+ // i18n.notify_default_title at display time.
16
+ title: '',
17
+ },
18
+ position: 'bottom-right',
19
+ duration: 5,
20
+ sound: false,
21
+ };
22
+
23
+ function ensureDir() {
24
+ if (!fs.existsSync(CONFIG_DIR)) {
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
+ }
27
+ }
28
+
29
+ function readConfig() {
30
+ ensureDir();
31
+ if (!fs.existsSync(CONFIG_PATH)) {
32
+ return { ...DEFAULT_CONFIG };
33
+ }
34
+ try {
35
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
36
+ const parsed = JSON.parse(raw);
37
+ return { ...DEFAULT_CONFIG, ...parsed, message: { ...DEFAULT_CONFIG.message, ...(parsed.message || {}) } };
38
+ } catch {
39
+ return { ...DEFAULT_CONFIG };
40
+ }
41
+ }
42
+
43
+ function writeConfig(next) {
44
+ ensureDir();
45
+ const merged = { ...readConfig(), ...next };
46
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
47
+ return merged;
48
+ }
49
+
50
+ module.exports = {
51
+ CONFIG_DIR,
52
+ CONFIG_PATH,
53
+ RUNTIME_PATH,
54
+ DEFAULT_CONFIG,
55
+ readConfig,
56
+ writeConfig,
57
+ ensureDir,
58
+ };