From c246088d244d0a370b5b66b11afced137b981bae Mon Sep 17 00:00:00 2001 From: DontxSleep Date: Sat, 26 Sep 2026 05:09:14 +0200 Subject: [PATCH 1/2] Add manual RepostExchange companion extension --- repostexchange-companion/README.md | 25 +++ .../dist/repost-companion-1.0.0.zip | Bin 0 -> 14279 bytes repostexchange-companion/icons/icon-128.png | Bin 0 -> 3477 bytes repostexchange-companion/icons/icon-16.png | Bin 0 -> 528 bytes repostexchange-companion/icons/icon-48.png | Bin 0 -> 1331 bytes .../icons/icon-master.png | Bin 0 -> 3212 bytes repostexchange-companion/manifest.json | 21 ++ repostexchange-companion/popup.css | 114 ++++++++++ repostexchange-companion/popup.html | 108 +++++++++ repostexchange-companion/scripts/package.sh | 13 ++ .../scripts/render_icons.py | 58 +++++ repostexchange-companion/src/core.js | 127 +++++++++++ repostexchange-companion/src/popup.js | 207 ++++++++++++++++++ .../store-assets/README.md | 5 + .../store-assets/privacy-policy.html | 42 ++++ repostexchange-companion/store-listing.md | 41 ++++ repostexchange-companion/tests/core.test.cjs | 60 +++++ 17 files changed, 821 insertions(+) create mode 100644 repostexchange-companion/README.md create mode 100644 repostexchange-companion/dist/repost-companion-1.0.0.zip create mode 100644 repostexchange-companion/icons/icon-128.png create mode 100644 repostexchange-companion/icons/icon-16.png create mode 100644 repostexchange-companion/icons/icon-48.png create mode 100644 repostexchange-companion/icons/icon-master.png create mode 100644 repostexchange-companion/manifest.json create mode 100644 repostexchange-companion/popup.css create mode 100644 repostexchange-companion/popup.html create mode 100755 repostexchange-companion/scripts/package.sh create mode 100755 repostexchange-companion/scripts/render_icons.py create mode 100644 repostexchange-companion/src/core.js create mode 100644 repostexchange-companion/src/popup.js create mode 100644 repostexchange-companion/store-assets/README.md create mode 100644 repostexchange-companion/store-assets/privacy-policy.html create mode 100644 repostexchange-companion/store-listing.md create mode 100644 repostexchange-companion/tests/core.test.cjs diff --git a/repostexchange-companion/README.md b/repostexchange-companion/README.md new file mode 100644 index 0000000..6a3df29 --- /dev/null +++ b/repostexchange-companion/README.md @@ -0,0 +1,25 @@ +# Repost Companion — Manual Tracker + +A small Chrome Manifest V3 extension for keeping a local campaign log and drafting comments from your own listening notes. + +## What it does + +- Tracks completed reposts by your local calendar day and in a rolling 12-hour window. +- Helps you work toward a local target of 20 per day while warning at 10 completed reposts in the last 12 hours. +- Builds varied, editable comment drafts from the listening notes you provide. +- Stores tracks, notes, drafts, and completion times in `chrome.storage.local` on this device. + +The extension does not access or modify RepostExchange or SoundCloud pages. It does not play tracks, submit comments, repost, schedule work, or run in the background. Add a track to the log only after you have completed the repost manually. The 10-per-12-hour warning reflects the current RepostExchange terms; check the terms for updates. + +## Install locally + +1. Open `chrome://extensions` in Chrome. +2. Turn on **Developer mode**. +3. Choose **Load unpacked** and select this `repostexchange-companion` folder. +4. Pin **Repost Companion** to the toolbar for easy access. + +## Test and package + +Run the local logic tests with `node --test tests/core.test.cjs`. Create the store upload ZIP with `./scripts/package.sh`. + +See `store-listing.md` for the proposed listing and privacy disclosures. diff --git a/repostexchange-companion/dist/repost-companion-1.0.0.zip b/repostexchange-companion/dist/repost-companion-1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..6baaf781222290e02c73bae58769d0d794680394 GIT binary patch literal 14279 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 b_z9ixSH14~oIaz!D-lZHRIgIUA>#i4$=i4C literal 0 HcmV?d00001 diff --git a/repostexchange-companion/icons/icon-16.png b/repostexchange-companion/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8925d8f617f4906fe6a2a806286c1f2f7bcae0 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^{p97-Atf zH+Z8TlcT`go3@UMUd)^(+sYEM6B9cPOQy|=klcSFN>|sXT7=SQf0IG?)>|EZr_YtrmvkB#;v#b^fXQ1XVs?PuDYBGJYZW~TwimI z@stEtwDjvYANRF%&rshmO?ubfa~Ct4yREM_Z9J8}*6Z92mYBe+T`Bp|+R-291Ww&Q zVIhNJn81>*Nfx%g*Q_kp%Lw#U%xZjX_?acA#c+1emQ8cGE5pyKzv9=)OzD=`5tm}e zwYhj!R(567iMm5aYr0;E_g~>x%UPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NGok>JNRA>d=T1{wFR}?}qby2WL!HvNZ=%PVHu!}Auwi{QvaiP$Ki$c4suDWSOmn~QjTiQh-ZAq!9 zrCC%FZHiSAFhvs5$t0PXd;PvUy_1*tCT8Aylf3C+Uf#{U_niBE=bZbKxe@7dQS0DW zyN@3|Xgob4TJ|etN@8Ta{_B`(vq_0DM$L#Pzboy1_vG@mUtqyRS&0NxI)3;q@%-~* z^sw@Mn4@~6ew590>J*^iW9oId%^MS4KRY@+32f4`D&!NZ*X>D;{FW>sTl#} z`;d702lOp$db&6?FM1=b@L3Y=y^VP>&hDPo86FOd)IUbrWEVVvG7`XOu>v!zEP>qv z+YgB64P_)>>%$=VP!LyES?2~3RQketH4tkpa`%8Jb{l~mKovSt-U8`w%ZBJ3& zNBh*wviv$RC*?&gX-tbm5vbN0DMK%FlG3wGkpO=Ktrga>jeXdcI!r0rNFuQb)B)ts zn>%FmXrC1F4L}Y^6r^w+!BdC2v_V^OB~}U%z_z?6AH3TygRk_+-N~|jHVUGOlzM%j zS5CgyFAx5%$n87xHaa*Sv@NkC;MR4fMGh0RU7?O!k({k|ZLoiA0#tT^XGGc+rxQbU z)li8Dtg^I4za%2y5+_dXIvr;tF8|mB+-AfLsv2W=r4Dr|&Q=@}u~s2^ZxL(w=U1m> zPk)cx!77vwHtvfY`cmh!FQ??;RK?ozWA19P2+&g;@aNy=Fa6f!&&J5!v%S1SHU&g2(j)9v`^zK#J)+^{-$9RQXxQt1oZv*Q=D;(O)n45 zbt?_a+_Xqgq0&v>Fp#+A)Cdqj16bowS6--FPqUt7lHgoPTT&(9z%d#;PJeiD(pIsY zR?4_B+`A(LXq;dkfTZt^5g@Qm0(ZpIPfBNblivRi0$BO}X|8#jk1f90B+1X#iQ*W$jtv$!oAR-PZ&Z=YPxEqpU=0G$`z0RPRZi*C;6 p=cKFu)gZ*CEB@Idgtt43&j1QQ&Q=uVgRcMp002ovPDHLkV1m07V#@#k literal 0 HcmV?d00001 diff --git a/repostexchange-companion/icons/icon-master.png b/repostexchange-companion/icons/icon-master.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8837172c05318eef1f5c6ebf600ec3ffebbae9 GIT binary patch literal 3212 zcmcgudrVVj6#spr=fc}z&Q#5ADfa*rXkZvC0SfDr%sFmU%ft} zs6<`1Y;h})$P!&3sM{jWFu6p7nI@i5J)10Bot9K+| zV#vpV?2wO_$wIT&pa{+mlTAu>$EeckrUb-3>gY!)+%ZVhJO)*^`k@GA89k8Kpi09) zl#%Ad>^^1_Ee^i#l94S*$iG#2 z!l>2UixiML{kULnejqZ88~--5x;JeF_U@~}i2%ozJb&~KJd2|gC=Z@p*9uSd?qFFK zEGrXvd$+f8!nBrT!A9IYPKD}O7PUljb=jT%YJIk~hA7hvPEVl@R1B@^{gHD^vhM70 z24e?PhpW!Haqc&ts~5DQ_GE0A1XNg`ASKs(z_WZTL%ew#wGp>@f^YO}XvQi9uXJv& zG&i#QwmYbmmIilW|DlJD?RsK~#w_4I)`zionSV z8iv0$t1ry#`7;lZpG5Ady0fUpdYq?b@+QL( zX!`t+2O}~N<0#$&XquZgaA8t7f`X0QnGT7yT)(_6OyR$bPG?M`TS$YW1!SjhT#g=E z@SQKHZO!j?esij!!N5;~mdnVa`5VTEljyqc_-ahsI?Ve!&B$!ig&TpLIW1}K;AW2V zoo`(=wYWJlBidgx2`*Pb?dEqb(1w?ImXBjF1=ieIHHw?#{42<;6@2Z#kl0dk=-5m5 zC3vpBW?2+CIYFWmC(6OO^5V4EHeC&JkwF{7t%Ip?B3WsLGAWkUYd|%eE1>6sU<#d4 z&j22~5o&g;g@@;rR4SpHknb;_Q>pqzS?jlf_pf-jL0uw#5@^|-*FIT->)N}mj>XH) z=@;MFX$`fho%+pJ_Nn8Dy^W*3l6v2^oT97bto&2GyD46z*MMG2{q7F;qUQG_zv}3< z=G2E)$um^JUL+RRJz6b4p0y_vkx{HYDfpNee2kZnGO{q%RlbFi%ez=tgs6g_Ns_>y zB6vzFPaJ4?`X6_{KRJO6oSV*7CyOBTKgIX{BdgF)kBF86*Y-AWbpYlo$0HHg>P~sI zC9Q%!vLKIhP=o;9jdrOIa#&O)s4j1`ZseufB6^Y1hayGVl8{ZkpDiHq#a@wzOks)u zCDl(A4u&fd*Mpp^GNzYyuSm(#A59kY9SpVlyKm7NcgORiB8C=6EYMUSa)`lBpBckJ z0xX*+3$XfgMe;9~=%Z!EM5rAfQek%&rEIzy$zX1XW0YyK2&*qwg5BHWNRkZA6Aa0s zm%Hl>Fe?;dfMruKM26i{Zh>}fMkbfyjA&|4+XB0*Jj^fuwkt2MoxN~u%ZkGFG<3|( L$<40KvTywtqdpFz literal 0 HcmV?d00001 diff --git a/repostexchange-companion/manifest.json b/repostexchange-companion/manifest.json new file mode 100644 index 0000000..d87e6b5 --- /dev/null +++ b/repostexchange-companion/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 3, + "name": "Repost Companion — Manual Tracker", + "version": "1.0.0", + "description": "Track a manual RepostExchange goal and draft comments from your own listening notes.", + "permissions": ["storage", "clipboardWrite"], + "action": { + "default_title": "Repost Companion", + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } +} diff --git a/repostexchange-companion/popup.css b/repostexchange-companion/popup.css new file mode 100644 index 0000000..bc81bf8 --- /dev/null +++ b/repostexchange-companion/popup.css @@ -0,0 +1,114 @@ +:root { + color-scheme: light; + --ink: #172724; + --muted: #667772; + --soft: #f4f6f3; + --line: #e5eae5; + --green: #155f4b; + --green-dark: #104c3c; + --mint: #dff4e9; + --lime: #c6f279; + --amber-bg: #fff3e8; + --white: #fff; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; +} + +* { box-sizing: border-box; } +button, input, textarea { font: inherit; } +button { cursor: pointer; } +button:disabled { cursor: not-allowed; } + +body { + width: 410px; + min-height: 580px; + max-height: 640px; + margin: 0; + overflow-y: auto; + background: var(--soft); + color: var(--ink); +} + +.app-shell { padding: 19px 20px 16px; } +.topbar { display: flex; align-items: center; gap: 11px; padding-bottom: 18px; } +.brand-mark { width: 39px; height: 39px; border-radius: 13px; display: flex; align-items: center; justify-content: center; gap: 2px; background: var(--green); } +.brand-mark span { width: 3px; border-radius: 4px; background: var(--lime); } +.brand-mark span:nth-child(1), .brand-mark span:nth-child(5) { height: 8px; } +.brand-mark span:nth-child(2), .brand-mark span:nth-child(4) { height: 15px; } +.brand-mark span:nth-child(3) { height: 21px; } +.brand-copy { flex: 1; min-width: 0; } +.eyebrow { margin: 0 0 4px; color: var(--muted); font-size: 9px; font-weight: 750; letter-spacing: .12em; } +h1, h2, h3, p { margin-top: 0; } +h1 { margin: 0; font-size: 16px; letter-spacing: -.025em; } +.local-badge { display: inline-flex; align-items: center; gap: 5px; border: 1px solid #dce5dd; border-radius: 99px; padding: 5px 8px; color: var(--muted); font-size: 9px; font-weight: 750; letter-spacing: .08em; } +.local-dot, .status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #4c9a69; } + +.progress-section { border: 1px solid var(--line); border-radius: 15px; padding: 14px; background: var(--white); box-shadow: 0 3px 10px rgba(27, 53, 43, .025); } +.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; } +.section-heading h2 { margin: 0; font-size: 17px; letter-spacing: -.035em; } +.date-label { padding-bottom: 1px; color: var(--muted); font-size: 11px; } +.progress-card { margin-top: 13px; } +.progress-topline { display: flex; justify-content: space-between; align-items: baseline; } +.progress-topline strong { font-size: 25px; letter-spacing: -.06em; } +.count-total { color: #87958f; font-size: 13px; } +.progress-caption { color: var(--muted); font-size: 11px; } +.progress-track { height: 7px; margin-top: 8px; overflow: hidden; border-radius: 9px; background: #edf1ed; } +.progress-track span { display: block; height: 100%; width: 0; border-radius: inherit; background: var(--green); transition: width .2s ease; } +.rolling-row { display: flex; align-items: center; justify-content: space-between; margin-top: 11px; color: var(--muted); font-size: 10px; } +.rolling-row > span { display: flex; align-items: center; gap: 6px; } +.rolling-row strong { color: var(--ink); font-size: 10px; } +.status-dot.is-limit { background: #dc7a2d; } + +.limit-banner { margin-top: 10px; border: 1px solid #f3d6bc; border-radius: 10px; padding: 9px 11px; background: var(--amber-bg); color: #874916; font-size: 11px; line-height: 1.45; } +.form-section { margin-top: 21px; } +.form-heading { margin-bottom: 12px; } +.step-mark { color: var(--green); font-size: 11px; font-weight: 750; } +.step-mark i { color: #99aaa2; font-style: normal; font-weight: 500; } +.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } +.field { display: flex; flex-direction: column; gap: 6px; color: #374943; font-size: 10px; font-weight: 700; } +.field input, .field textarea, .draft-panel textarea { width: 100%; border: 1px solid #dfe6df; border-radius: 9px; outline: none; background: var(--white); color: var(--ink); font-size: 12px; font-weight: 450; transition: border-color .15s, box-shadow .15s; } +.field input { height: 36px; padding: 0 10px; } +.field textarea { min-height: 53px; padding: 9px 10px; resize: vertical; line-height: 1.45; } +.field input:focus, .field textarea:focus, .draft-panel textarea:focus { border-color: #7bad93; box-shadow: 0 0 0 3px rgba(59, 134, 96, .11); } +.field input::placeholder, .field textarea::placeholder { color: #a2aea8; } +.notes-field { margin-top: 10px; } +.field-hint { color: #87938d; font-size: 9px; font-weight: 450; line-height: 1.35; } +.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 36px; border: 0; border-radius: 9px; padding: 0 12px; font-size: 11px; font-weight: 700; transition: transform .12s, background .12s, opacity .12s; } +.button:not(:disabled):active { transform: translateY(1px); } +.button-primary { width: 100%; margin-top: 11px; background: var(--green); color: white; } +.button-primary:not(:disabled):hover { background: var(--green-dark); } +.button-primary:disabled, .button-log:disabled { opacity: .42; } +.button-secondary { border: 1px solid #dfe6df; background: white; color: var(--ink); } +.button-secondary:hover { background: #f6f8f5; } +.button-text { min-height: 30px; padding: 0 6px; background: transparent; color: var(--green); } +.button-text:hover { color: var(--green-dark); } + +.draft-panel { margin-top: 12px; border: 1px solid #d9e7de; border-radius: 13px; padding: 12px; background: #f7fbf8; } +.draft-heading { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 8px; } +.draft-heading h3 { margin: 0; font-size: 13px; } +.char-count { color: #87938d; font-size: 9px; } +.draft-panel textarea { min-height: 76px; padding: 10px; resize: vertical; line-height: 1.5; } +.draft-actions { display: flex; align-items: center; gap: 7px; margin-top: 7px; } +.manual-reminder { margin: 10px 0; color: #63736b; font-size: 9px; line-height: 1.45; } +.button-log { width: 100%; background: var(--lime); color: #21341f; } +.button-log:not(:disabled):hover { background: #b7e768; } + +.history-section { margin-top: 20px; } +.history-heading { margin-bottom: 9px; } +.history-heading h2 { font-size: 15px; } +.history-total { padding-bottom: 1px; color: #8a9791; font-size: 10px; } +.history-list { display: flex; flex-direction: column; gap: 6px; } +.history-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line); border-radius: 10px; padding: 8px 9px; background: white; } +.history-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; } +.history-copy strong { overflow: hidden; color: #2c3e36; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } +.history-copy span { color: #89958f; font-size: 9px; } +.remove-entry { border: 0; background: transparent; color: #8e9b94; font-size: 9px; } +.remove-entry:hover { color: #a14831; } +.empty-state { margin: 0; border: 1px dashed #dbe2dc; border-radius: 10px; padding: 12px; color: #829089; font-size: 10px; text-align: center; } +.footer-note { display: flex; align-items: center; justify-content: center; gap: 5px; margin-top: 16px; color: #87938d; font-size: 9px; } +.lock-mark { color: var(--green); font-size: 11px; } +.toast { position: fixed; right: 16px; bottom: 12px; left: 16px; z-index: 2; border-radius: 9px; padding: 9px 10px; transform: translateY(8px); background: #203c31; color: white; font-size: 10px; text-align: center; opacity: 0; pointer-events: none; transition: opacity .16s, transform .16s; } +.toast.is-visible { transform: translateY(0); opacity: 1; } +[hidden] { display: none !important; } diff --git a/repostexchange-companion/popup.html b/repostexchange-companion/popup.html new file mode 100644 index 0000000..47a8ca9 --- /dev/null +++ b/repostexchange-companion/popup.html @@ -0,0 +1,108 @@ + + + + + + + Repost Companion + + + +
+
+ +
+

YOUR CAMPAIGN DESK

+

Repost Companion

+
+ LOCAL +
+ +
+
+
+

TODAY

+

Your pace

+
+ +
+
+
+
0 / 20
+ reposts logged +
+
+ +
+
+ 12-hour window clear + 0 / 10 +
+
+
+ + + +
+
+
+

ONE TRACK AT A TIME

+

Make it personal

+
+ 01 / 02 +
+ +
+ + +
+ + + + + + +
+ +
+
+

YOUR LOCAL LOG

Recent tracks

+ 0 saved +
+
+

Your completed tracks will show up here.

+
+ +
Nothing leaves this browser.
+
+
+ + + + diff --git a/repostexchange-companion/scripts/package.sh b/repostexchange-companion/scripts/package.sh new file mode 100755 index 0000000..c52754a --- /dev/null +++ b/repostexchange-companion/scripts/package.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +VERSION=$(node -e 'const fs=require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1],"utf8")).version)' "$ROOT_DIR/manifest.json") +DIST_DIR="$ROOT_DIR/dist" +OUTPUT="$DIST_DIR/repost-companion-$VERSION.zip" + +mkdir -p "$DIST_DIR" +rm -f "$OUTPUT" +cd "$ROOT_DIR" +zip -q -r -X "$OUTPUT" manifest.json popup.html popup.css src/core.js src/popup.js icons/icon-16.png icons/icon-48.png icons/icon-128.png +printf 'Created %s\n' "$OUTPUT" diff --git a/repostexchange-companion/scripts/render_icons.py b/repostexchange-companion/scripts/render_icons.py new file mode 100755 index 0000000..f1241fd --- /dev/null +++ b/repostexchange-companion/scripts/render_icons.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Create the extension's small PNG icons without external dependencies.""" + +from pathlib import Path +import struct +import zlib + + +ROOT = Path(__file__).resolve().parents[1] +ICON_DIR = ROOT / "icons" +MASTER_SIZE = 512 + + +def chunk(tag: bytes, data: bytes) -> bytes: + body = tag + data + return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF) + + +def in_round_rect(x: float, y: float, left: float, top: float, right: float, bottom: float, radius: float) -> bool: + cx = min(max(x, left + radius), right - radius) + cy = min(max(y, top + radius), bottom - radius) + return (x - cx) ** 2 + (y - cy) ** 2 <= radius**2 + + +def pixel_color(x: float, y: float) -> tuple[int, int, int, int]: + inset = 8 + radius = 105 + if in_round_rect(x, y, inset, inset, MASTER_SIZE - inset, MASTER_SIZE - inset, radius): + color = (21, 95, 75, 255) + else: + color = (0, 0, 0, 0) + + bar_centers = [171, 214, 256, 298, 341] + bar_heights = [78, 144, 202, 144, 78] + width = 24 + bar_color = (198, 242, 121, 255) + for center, height in zip(bar_centers, bar_heights): + top = (MASTER_SIZE - height) / 2 + bottom = top + height + if in_round_rect(x, y, center - width / 2, top, center + width / 2, bottom, width / 2): + color = bar_color + return color + + +def write_png(path: Path) -> None: + rows = bytearray() + for y in range(MASTER_SIZE): + rows.append(0) + for x in range(MASTER_SIZE): + rows.extend(pixel_color(x + 0.5, y + 0.5)) + header = struct.pack(">IIBBBBB", MASTER_SIZE, MASTER_SIZE, 8, 6, 0, 0, 0) + data = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header) + chunk(b"IDAT", zlib.compress(bytes(rows), 9)) + chunk(b"IEND", b"") + path.write_bytes(data) + + +if __name__ == "__main__": + ICON_DIR.mkdir(parents=True, exist_ok=True) + write_png(ICON_DIR / "icon-master.png") diff --git a/repostexchange-companion/src/core.js b/repostexchange-companion/src/core.js new file mode 100644 index 0000000..9ad7c8d --- /dev/null +++ b/repostexchange-companion/src/core.js @@ -0,0 +1,127 @@ +(function attachCore(root, build) { + const api = build(); + if (typeof module === "object" && module.exports) module.exports = api; + if (root) root.RepostCompanionCore = api; +})(typeof globalThis === "object" ? globalThis : this, function buildCore() { + const DAILY_TARGET = 20; + const ROLLING_LIMIT = 10; + const WINDOW_MS = 12 * 60 * 60 * 1000; + const MAX_COMMENT_LENGTH = 280; + + const LEADS = [ + "I really liked", + "What stood out to me was", + "My favorite detail was", + "I keep coming back to", + "The highlight for me was", + "I enjoyed", + "A standout moment was", + "I loved the feel of" + ]; + + const ENDS = [ + ".", + " — nice work on this one.", + ". Thanks for sharing it.", + "; it made the track memorable for me.", + " — really enjoyed this listen.", + ". That was a highlight for me.", + "; it gave this one a character I enjoyed.", + ". I had a great time listening." + ]; + + function localDateKey(value) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } + + function parseCompletion(entry) { + const value = typeof entry === "number" ? entry : entry && entry.completedAt; + const timestamp = typeof value === "number" ? value : Date.parse(value); + return Number.isFinite(timestamp) ? timestamp : null; + } + + function getProgress(entries, now = Date.now()) { + const list = Array.isArray(entries) ? entries : []; + const todayKey = localDateKey(now); + const completed = list + .map((entry) => ({ entry, timestamp: parseCompletion(entry) })) + .filter(({ timestamp }) => timestamp !== null && timestamp <= now); + const todayCount = completed.filter(({ timestamp }) => localDateKey(timestamp) === todayKey).length; + const recent = completed + .filter(({ timestamp }) => now - timestamp < WINDOW_MS) + .sort((a, b) => a.timestamp - b.timestamp); + const nextSlotAt = recent.length >= ROLLING_LIMIT + ? recent[recent.length - ROLLING_LIMIT].timestamp + WINDOW_MS + : null; + + return { + todayCount, + dailyTarget: DAILY_TARGET, + rollingCount: recent.length, + rollingLimit: ROLLING_LIMIT, + nextSlotAt, + canLog: todayCount < DAILY_TARGET && recent.length < ROLLING_LIMIT + }; + } + + function hashText(value) { + let hash = 2166136261; + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; + } + + function normalizeComment(value) { + return String(value || "").toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim(); + } + + function generateDraft(note, recentDrafts = [], seed = `${Date.now()}`) { + const detail = String(note || "").trim().replace(/[.!?]+$/u, ""); + if (!detail) return ""; + const used = new Set((Array.isArray(recentDrafts) ? recentDrafts : []).map(normalizeComment)); + const combinationCount = LEADS.length * ENDS.length; + const start = hashText(`${seed}|${detail}`) % combinationCount; + + for (let offset = 0; offset < combinationCount; offset += 1) { + const candidateIndex = (start + offset) % combinationCount; + const lead = LEADS[Math.floor(candidateIndex / ENDS.length)]; + const end = ENDS[candidateIndex % ENDS.length]; + const maxDetailLength = MAX_COMMENT_LENGTH - lead.length - end.length - 1; + let fittedDetail = detail; + if (fittedDetail.length > maxDetailLength) { + const clipped = fittedDetail.slice(0, Math.max(1, maxDetailLength - 1)); + const wordBoundary = clipped.lastIndexOf(" "); + fittedDetail = `${(wordBoundary > maxDetailLength * 0.65 ? clipped.slice(0, wordBoundary) : clipped).trimEnd()}…`; + } + const candidate = `${lead} ${fittedDetail}${end}`; + if (!used.has(normalizeComment(candidate))) return candidate; + } + + const lead = LEADS[start % LEADS.length]; + const end = " — another detail I appreciated."; + const maxDetailLength = MAX_COMMENT_LENGTH - lead.length - end.length - 1; + const fallbackDetail = detail.length > maxDetailLength + ? `${detail.slice(0, maxDetailLength - 1).trimEnd()}…` + : detail; + return `${lead} ${fallbackDetail}${end}`; + } + + return Object.freeze({ + DAILY_TARGET, + MAX_COMMENT_LENGTH, + ROLLING_LIMIT, + WINDOW_MS, + generateDraft, + getProgress, + localDateKey, + normalizeComment, + parseCompletion + }); +}); diff --git a/repostexchange-companion/src/popup.js b/repostexchange-companion/src/popup.js new file mode 100644 index 0000000..c953411 --- /dev/null +++ b/repostexchange-companion/src/popup.js @@ -0,0 +1,207 @@ +(() => { + const core = globalThis.RepostCompanionCore; + const elements = { + title: document.querySelector("#track-title"), + artist: document.querySelector("#artist-name"), + note: document.querySelector("#listening-note"), + generate: document.querySelector("#generate-button"), + draftPanel: document.querySelector("#draft-panel"), + draft: document.querySelector("#comment-draft"), + draftCount: document.querySelector("#draft-char-count"), + copy: document.querySelector("#copy-button"), + another: document.querySelector("#another-button"), + log: document.querySelector("#log-button"), + dailyCount: document.querySelector("#daily-count"), + dailyStatus: document.querySelector("#daily-status"), + dailyFill: document.querySelector("#daily-fill"), + dailyProgress: document.querySelector("[role='progressbar']"), + todayLabel: document.querySelector("#today-label"), + rollingCount: document.querySelector("#rolling-count"), + rollingMessage: document.querySelector("#rolling-message"), + rollingDot: document.querySelector("#rolling-dot"), + limitBanner: document.querySelector("#limit-banner"), + historyList: document.querySelector("#history-list"), + historyCount: document.querySelector("#history-count"), + emptyHistory: document.querySelector("#empty-history"), + toast: document.querySelector("#toast") + }; + + const STORE_KEY = "repostCompanionData"; + let data = { entries: [], drafts: [] }; + let toastTimer; + + function todayLabel(date = new Date()) { + return new Intl.DateTimeFormat(undefined, { weekday: "short", month: "short", day: "numeric" }).format(date); + } + + function formatTime(timestamp) { + return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(timestamp)); + } + + function countdown(target, now = Date.now()) { + const totalMinutes = Math.ceil(Math.max(0, target - now) / 60000); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + return hours ? `${hours}h ${String(minutes).padStart(2, "0")}m` : `${minutes}m`; + } + + function toast(message) { + elements.toast.textContent = message; + elements.toast.classList.add("is-visible"); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => elements.toast.classList.remove("is-visible"), 2200); + } + + async function save() { + await chrome.storage.local.set({ [STORE_KEY]: data }); + } + + function updateProgress() { + const now = Date.now(); + const progress = core.getProgress(data.entries, now); + elements.todayLabel.textContent = todayLabel(new Date(now)); + elements.dailyCount.textContent = String(progress.todayCount); + elements.dailyStatus.textContent = progress.todayCount >= progress.dailyTarget ? "daily target reached" : "reposts logged"; + elements.dailyFill.style.width = `${Math.min(100, (progress.todayCount / progress.dailyTarget) * 100)}%`; + elements.dailyProgress.setAttribute("aria-valuenow", String(Math.min(progress.todayCount, progress.dailyTarget))); + elements.rollingCount.textContent = `${progress.rollingCount} / ${progress.rollingLimit}`; + elements.rollingDot.classList.toggle("is-limit", progress.rollingCount >= progress.rollingLimit); + + if (progress.rollingCount >= progress.rollingLimit && progress.nextSlotAt) { + elements.rollingMessage.textContent = `Next slot in ${countdown(progress.nextSlotAt, now)}`; + elements.limitBanner.hidden = false; + elements.limitBanner.textContent = `The 10-repost rolling limit is reached. Your next slot opens around ${formatTime(progress.nextSlotAt)}.`; + } else if (progress.todayCount >= progress.dailyTarget) { + elements.rollingMessage.textContent = "12-hour window clear"; + elements.limitBanner.hidden = false; + elements.limitBanner.textContent = "Daily target reached. Your completed activity is saved locally."; + } else { + elements.rollingMessage.textContent = "12-hour window clear"; + elements.limitBanner.hidden = true; + elements.limitBanner.textContent = ""; + } + + elements.generate.disabled = !elements.title.value.trim() || !elements.artist.value.trim() || !elements.note.value.trim(); + elements.log.disabled = !progress.canLog || !elements.draft.value.trim(); + } + + function renderHistory() { + const sorted = [...data.entries].sort((a, b) => b.completedAt - a.completedAt); + elements.historyList.replaceChildren(); + elements.historyCount.textContent = `${sorted.length} saved`; + elements.emptyHistory.hidden = sorted.length > 0; + + for (const entry of sorted.slice(0, 5)) { + const row = document.createElement("article"); + row.className = "history-item"; + const copy = document.createElement("div"); + copy.className = "history-copy"; + const title = document.createElement("strong"); + title.textContent = entry.title; + const detail = document.createElement("span"); + detail.textContent = `${entry.artist} · ${formatTime(entry.completedAt)}`; + copy.append(title, detail); + const remove = document.createElement("button"); + remove.className = "remove-entry"; + remove.type = "button"; + remove.textContent = "Remove"; + remove.setAttribute("aria-label", `Remove ${entry.title} from your local log`); + remove.addEventListener("click", async () => { + data.entries = data.entries.filter((item) => item.id !== entry.id); + await save(); + render(); + toast("Removed from your local log"); + }); + row.append(copy, remove); + elements.historyList.append(row); + } + } + + function render() { + updateProgress(); + renderHistory(); + elements.draftCount.textContent = `${elements.draft.value.length} / 280`; + } + + async function makeDraft() { + const note = elements.note.value.trim(); + if (!note) return; + const draft = core.generateDraft(note, data.drafts, `${Date.now()}|${elements.title.value}|${elements.artist.value}`); + elements.draft.value = draft; + elements.draftPanel.hidden = false; + data.drafts = [...data.drafts, draft].slice(-240); + await save(); + render(); + elements.draft.focus(); + elements.draft.setSelectionRange(draft.length, draft.length); + } + + elements.generate.addEventListener("click", makeDraft); + elements.another.addEventListener("click", makeDraft); + function invalidateDraft() { + elements.draft.value = ""; + elements.draftPanel.hidden = true; + render(); + } + + elements.title.addEventListener("input", invalidateDraft); + elements.artist.addEventListener("input", invalidateDraft); + elements.note.addEventListener("input", invalidateDraft); + elements.draft.addEventListener("input", render); + + elements.copy.addEventListener("click", async () => { + const value = elements.draft.value.trim(); + if (!value) return; + try { + await navigator.clipboard.writeText(value); + toast("Draft copied"); + } catch { + toast("Copy unavailable — select the draft and copy it manually"); + } + }); + + elements.log.addEventListener("click", async () => { + elements.log.disabled = true; + const progress = core.getProgress(data.entries); + const title = elements.title.value.trim(); + const artist = elements.artist.value.trim(); + const comment = elements.draft.value.trim(); + if (!progress.canLog || !title || !artist || !comment) { + render(); + return; + } + + data.entries.push({ + id: crypto.randomUUID(), + title, + artist, + note: elements.note.value.trim(), + comment, + completedAt: Date.now() + }); + await save(); + elements.title.value = ""; + elements.artist.value = ""; + elements.note.value = ""; + elements.draft.value = ""; + elements.draftPanel.hidden = true; + render(); + toast("Added to your local log"); + }); + + chrome.storage.local.get(STORE_KEY).then((stored) => { + const saved = stored[STORE_KEY]; + if (saved && typeof saved === "object") { + data = { + entries: Array.isArray(saved.entries) ? saved.entries : [], + drafts: Array.isArray(saved.drafts) ? saved.drafts : [] + }; + } + render(); + }).catch(() => { + render(); + toast("Couldn’t load local history"); + }); + + setInterval(updateProgress, 30000); +})(); diff --git a/repostexchange-companion/store-assets/README.md b/repostexchange-companion/store-assets/README.md new file mode 100644 index 0000000..3be0dac --- /dev/null +++ b/repostexchange-companion/store-assets/README.md @@ -0,0 +1,5 @@ +# Store asset status + +The extension icon, listing copy, and privacy policy draft are prepared. A genuine screenshot of the loaded Chrome extension is still required before a Chrome Web Store submission. + +The automated local review could not open Chrome's extension manager because the browser safety policy blocked `chrome://extensions`. The extension ZIP is ready for a manual local load; see the project README. After loading it in Chrome, capture the popup in a realistic, non-sensitive state and save it as `screenshot-01.png` in this directory. Do not include real track notes or account data in the store screenshot. diff --git a/repostexchange-companion/store-assets/privacy-policy.html b/repostexchange-companion/store-assets/privacy-policy.html new file mode 100644 index 0000000..5321c0f --- /dev/null +++ b/repostexchange-companion/store-assets/privacy-policy.html @@ -0,0 +1,42 @@ + + + + + + Repost Companion Privacy Policy + + + +
+

Repost Companion Privacy Policy

+

Repost Companion is a manual, local-only campaign tracker. It does not read or modify RepostExchange or SoundCloud pages.

+ +

Information stored

+

When you use the extension, it stores track titles, artist names, listening notes, comment drafts, and the completion times you enter. This information is kept in Chrome's storage.local on your device so the extension can show your progress and avoid repeating recent drafts.

+ +

Information sharing

+

The extension does not send this information to the developer or any other server. It contains no analytics, advertising, remote code, account service, or AI connection. The copy button writes a draft to your clipboard only when you select it.

+ +

Permissions

+
    +
  • storage saves your log and drafts locally in Chrome.
  • +
  • clipboardWrite copies a draft after you select the copy button.
  • +
+

Repost Companion requests no website access or host permissions.

+ +

Data removal

+

You can remove individual log entries using the extension. Removing the extension from Chrome removes its local extension data.

+ +

Changes and contact

+

This policy applies to Repost Companion version 1.0.0. A publisher support contact must be added here before public distribution.

+
+ + diff --git a/repostexchange-companion/store-listing.md b/repostexchange-companion/store-listing.md new file mode 100644 index 0000000..b9f84ce --- /dev/null +++ b/repostexchange-companion/store-listing.md @@ -0,0 +1,41 @@ +# Chrome Web Store listing draft + +## Name + +Repost Companion — Manual Tracker + +## Short description + +Track manual reposts and draft comments from your listening notes. + +## Detailed description + +Repost Companion helps you keep a personal record of tracks you have listened to and reposted manually. Add the track and artist, write a short note about what you honestly enjoyed, and get an editable comment draft built from that note. After you submit the repost yourself, log it to see your local daily progress and rolling 12-hour count. + +Your daily goal and rolling-window count stay on this device. Repost Companion does not access RepostExchange or SoundCloud pages, submit actions, play audio, or run in the background. It does not connect to an AI service or send your listening notes to a server. + +Repost Companion is an independent productivity tool and is not affiliated with or endorsed by RepostExchange or SoundCloud. + +## Single purpose + +Help a user keep a private manual log of completed music reposts and draft comments from user-entered listening notes. + +## Permission justifications + +- `storage`: save the user's track log, drafts, and timestamps locally in Chrome. +- `clipboardWrite`: copy a draft so the user can paste it manually into a site. + +The extension requests no host permissions and does not collect or transmit user data. + +## Privacy disclosure + +Repost Companion stores track titles, artist names, user listening notes, comment drafts, and completion timestamps in Chrome local storage on this device. This information is used only to draft comments and calculate progress. It is not sent to the developer or any other server. The extension includes no analytics, advertising, account system, or remote code. + +## Store assets + +- 128 px icon: `icons/icon-128.png` +- Genuine product screenshot: pending local Chrome review. Do not submit this listing until a screenshot of the loaded extension has been added here. + +## Support and privacy policy + +Add a support contact and host `store-assets/privacy-policy.html` at a stable public URL before submitting the item. Add a genuine product screenshot. Do not claim Chrome Web Store approval until review is complete. diff --git a/repostexchange-companion/tests/core.test.cjs b/repostexchange-companion/tests/core.test.cjs new file mode 100644 index 0000000..f2328b2 --- /dev/null +++ b/repostexchange-companion/tests/core.test.cjs @@ -0,0 +1,60 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const core = require("../src/core.js"); + +const HOUR = 60 * 60 * 1000; + +test("daily progress uses the machine's local calendar day", () => { + const now = new Date(2026, 8, 26, 12, 0, 0).getTime(); + const entries = [ + { completedAt: new Date(2026, 8, 26, 1, 0, 0).getTime() }, + { completedAt: new Date(2026, 8, 25, 23, 59, 0).getTime() }, + { completedAt: now + HOUR } + ]; + const progress = core.getProgress(entries, now); + assert.equal(progress.todayCount, 1); + assert.equal(progress.dailyTarget, 20); + assert.equal(progress.canLog, true); +}); + +test("the rolling cap includes the last twelve hours and frees the oldest slot at its exact expiry", () => { + const now = new Date(2026, 8, 26, 12, 0, 0).getTime(); + const entries = Array.from({ length: 10 }, (_, index) => ({ completedAt: now - (11 - index) * HOUR })); + const atCap = core.getProgress(entries, now); + assert.equal(atCap.rollingCount, 10); + assert.equal(atCap.canLog, false); + assert.equal(atCap.nextSlotAt, entries[0].completedAt + core.WINDOW_MS); + + const afterExpiry = core.getProgress(entries, atCap.nextSlotAt); + assert.equal(afterExpiry.rollingCount, 9); + assert.equal(afterExpiry.canLog, true); +}); + +test("the daily target is counted for the local calendar day", () => { + const now = new Date(2026, 8, 26, 23, 30, 0).getTime(); + const first = new Date(2026, 8, 26, 0, 30, 0).getTime(); + const entries = Array.from({ length: 20 }, (_, index) => ({ completedAt: first + index * 70 * 60 * 1000 })); + const progress = core.getProgress(entries, now); + assert.equal(progress.todayCount, 20); + assert.equal(progress.rollingCount, 10); + assert.equal(progress.canLog, false); +}); + +test("comment drafts use the supplied note and avoid recent duplicates", () => { + const note = "the warm bass and loose groove"; + const first = core.generateDraft(note, [], "same-seed"); + const second = core.generateDraft(note, [first], "same-seed"); + assert.match(first, /the warm bass and loose groove/); + assert.match(second, /the warm bass and loose groove/); + assert.notEqual(core.normalizeComment(first), core.normalizeComment(second)); +}); + +test("empty notes do not produce a fabricated comment", () => { + assert.equal(core.generateDraft(" ", [], "seed"), ""); +}); + +test("long notes produce a comment within the service's visible text limit", () => { + const draft = core.generateDraft("warm bass and groove ".repeat(30), [], "long-note"); + assert.ok(draft.length <= core.MAX_COMMENT_LENGTH); + assert.ok(draft.includes("…")); +}); From b8a85ded9fed6cc11deeee95a5420798d9ac7b40 Mon Sep 17 00:00:00 2001 From: DontxSleep Date: Sat, 26 Sep 2026 20:00:25 +0200 Subject: [PATCH 2/2] Make companion a local manual queue and tracker --- repostexchange-companion/README.md | 22 +- .../dist/repost-companion-1.0.0.zip | Bin 14279 -> 0 bytes .../dist/repost-companion-1.1.0.zip | Bin 0 -> 13459 bytes repostexchange-companion/manifest.json | 8 +- repostexchange-companion/popup.css | 77 +++---- repostexchange-companion/popup.html | 66 ++---- repostexchange-companion/scripts/package.sh | 3 +- repostexchange-companion/src/core.js | 72 +------ repostexchange-companion/src/popup.js | 198 +++++++++++------- .../store-assets/privacy-policy.html | 11 +- repostexchange-companion/store-listing.md | 19 +- repostexchange-companion/tests/core.test.cjs | 35 ++-- 12 files changed, 216 insertions(+), 295 deletions(-) delete mode 100644 repostexchange-companion/dist/repost-companion-1.0.0.zip create mode 100644 repostexchange-companion/dist/repost-companion-1.1.0.zip diff --git a/repostexchange-companion/README.md b/repostexchange-companion/README.md index 6a3df29..89f6199 100644 --- a/repostexchange-companion/README.md +++ b/repostexchange-companion/README.md @@ -1,25 +1,27 @@ -# Repost Companion — Manual Tracker +# Repost Companion — Local Queue -A small Chrome Manifest V3 extension for keeping a local campaign log and drafting comments from your own listening notes. +A small Chrome Manifest V3 extension for keeping a local listening checklist and a record of reposts you complete yourself. ## What it does -- Tracks completed reposts by your local calendar day and in a rolling 12-hour window. -- Helps you work toward a local target of 20 per day while warning at 10 completed reposts in the last 12 hours. -- Builds varied, editable comment drafts from the listening notes you provide. -- Stores tracks, notes, drafts, and completion times in `chrome.storage.local` on this device. +- Keeps a queue of track titles and artist names on this device. +- Lets you mark a track listened to, then log the repost after you manually submit it. +- Shows progress toward a 20-repost local calendar-day target and the rolling 10-per-12-hour limit. +- Stores the queue and completion times in `chrome.storage.local`. -The extension does not access or modify RepostExchange or SoundCloud pages. It does not play tracks, submit comments, repost, schedule work, or run in the background. Add a track to the log only after you have completed the repost manually. The 10-per-12-hour warning reflects the current RepostExchange terms; check the terms for updates. +Repost Companion never opens or reads RepostExchange or SoundCloud, controls playback, clicks repost buttons, writes comments, or runs in the background. Use it as a checklist beside the site; you make each platform decision and action yourself. The rolling limit reflects the [RepostExchange terms](https://repostexchange.com/terms-of-use); check the terms for updates. ## Install locally 1. Open `chrome://extensions` in Chrome. 2. Turn on **Developer mode**. 3. Choose **Load unpacked** and select this `repostexchange-companion` folder. -4. Pin **Repost Companion** to the toolbar for easy access. +4. Pin **Repost Companion** to the toolbar. -## Test and package +## Use -Run the local logic tests with `node --test tests/core.test.cjs`. Create the store upload ZIP with `./scripts/package.sh`. +Add a track and artist to your queue. Listen on RepostExchange yourself, then mark it listened. If you choose to repost it, submit the repost on RepostExchange yourself and only then select **I reposted it** in the extension. The local queue and count will update. + +Run the logic tests with `node --test tests/core.test.cjs`. Build the Chrome Web Store ZIP with `./scripts/package.sh`. See `store-listing.md` for the proposed listing and privacy disclosures. diff --git a/repostexchange-companion/dist/repost-companion-1.0.0.zip b/repostexchange-companion/dist/repost-companion-1.0.0.zip deleted file mode 100644 index 6baaf781222290e02c73bae58769d0d794680394..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14279 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 literal 0 HcmV?d00001 diff --git a/repostexchange-companion/manifest.json b/repostexchange-companion/manifest.json index d87e6b5..dd17a0b 100644 --- a/repostexchange-companion/manifest.json +++ b/repostexchange-companion/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 3, - "name": "Repost Companion — Manual Tracker", - "version": "1.0.0", - "description": "Track a manual RepostExchange goal and draft comments from your own listening notes.", - "permissions": ["storage", "clipboardWrite"], + "name": "Repost Companion — Local Queue", + "version": "1.1.0", + "description": "Keep a local manual listening checklist and repost progress log.", + "permissions": ["storage"], "action": { "default_title": "Repost Companion", "default_popup": "popup.html", diff --git a/repostexchange-companion/popup.css b/repostexchange-companion/popup.css index bc81bf8..6867c1f 100644 --- a/repostexchange-companion/popup.css +++ b/repostexchange-companion/popup.css @@ -6,7 +6,6 @@ --line: #e5eae5; --green: #155f4b; --green-dark: #104c3c; - --mint: #dff4e9; --lime: #c6f279; --amber-bg: #fff3e8; --white: #fff; @@ -17,20 +16,11 @@ } * { box-sizing: border-box; } -button, input, textarea { font: inherit; } +button, input { font: inherit; } button { cursor: pointer; } button:disabled { cursor: not-allowed; } -body { - width: 410px; - min-height: 580px; - max-height: 640px; - margin: 0; - overflow-y: auto; - background: var(--soft); - color: var(--ink); -} - +body { width: 410px; min-height: 580px; max-height: 640px; margin: 0; overflow-y: auto; background: var(--soft); color: var(--ink); } .app-shell { padding: 19px 20px 16px; } .topbar { display: flex; align-items: center; gap: 11px; padding-bottom: 18px; } .brand-mark { width: 39px; height: 39px; border-radius: 13px; display: flex; align-items: center; justify-content: center; gap: 2px; background: var(--green); } @@ -40,7 +30,7 @@ body { .brand-mark span:nth-child(3) { height: 21px; } .brand-copy { flex: 1; min-width: 0; } .eyebrow { margin: 0 0 4px; color: var(--muted); font-size: 9px; font-weight: 750; letter-spacing: .12em; } -h1, h2, h3, p { margin-top: 0; } +h1, h2, p { margin-top: 0; } h1 { margin: 0; font-size: 16px; letter-spacing: -.025em; } .local-badge { display: inline-flex; align-items: center; gap: 5px; border: 1px solid #dce5dd; border-radius: 99px; padding: 5px 8px; color: var(--muted); font-size: 9px; font-weight: 750; letter-spacing: .08em; } .local-dot, .status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #4c9a69; } @@ -60,53 +50,40 @@ h1 { margin: 0; font-size: 16px; letter-spacing: -.025em; } .rolling-row > span { display: flex; align-items: center; gap: 6px; } .rolling-row strong { color: var(--ink); font-size: 10px; } .status-dot.is-limit { background: #dc7a2d; } - .limit-banner { margin-top: 10px; border: 1px solid #f3d6bc; border-radius: 10px; padding: 9px 11px; background: var(--amber-bg); color: #874916; font-size: 11px; line-height: 1.45; } -.form-section { margin-top: 21px; } -.form-heading { margin-bottom: 12px; } + +.form-section, .queue-section, .history-section { margin-top: 19px; } +.form-heading, .queue-heading, .history-heading { margin-bottom: 10px; } .step-mark { color: var(--green); font-size: 11px; font-weight: 750; } .step-mark i { color: #99aaa2; font-style: normal; font-weight: 500; } .field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } .field { display: flex; flex-direction: column; gap: 6px; color: #374943; font-size: 10px; font-weight: 700; } -.field input, .field textarea, .draft-panel textarea { width: 100%; border: 1px solid #dfe6df; border-radius: 9px; outline: none; background: var(--white); color: var(--ink); font-size: 12px; font-weight: 450; transition: border-color .15s, box-shadow .15s; } -.field input { height: 36px; padding: 0 10px; } -.field textarea { min-height: 53px; padding: 9px 10px; resize: vertical; line-height: 1.45; } -.field input:focus, .field textarea:focus, .draft-panel textarea:focus { border-color: #7bad93; box-shadow: 0 0 0 3px rgba(59, 134, 96, .11); } -.field input::placeholder, .field textarea::placeholder { color: #a2aea8; } -.notes-field { margin-top: 10px; } -.field-hint { color: #87938d; font-size: 9px; font-weight: 450; line-height: 1.35; } -.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 36px; border: 0; border-radius: 9px; padding: 0 12px; font-size: 11px; font-weight: 700; transition: transform .12s, background .12s, opacity .12s; } +.field input { width: 100%; height: 36px; border: 1px solid #dfe6df; border-radius: 9px; outline: none; padding: 0 10px; background: var(--white); color: var(--ink); font-size: 12px; font-weight: 450; transition: border-color .15s, box-shadow .15s; } +.field input:focus { border-color: #7bad93; box-shadow: 0 0 0 3px rgba(59, 134, 96, .11); } +.field input::placeholder { color: #a2aea8; } +.button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 34px; border: 0; border-radius: 9px; padding: 0 11px; font-size: 10px; font-weight: 700; transition: transform .12s, background .12s, opacity .12s; } .button:not(:disabled):active { transform: translateY(1px); } -.button-primary { width: 100%; margin-top: 11px; background: var(--green); color: white; } +.button-primary { width: 100%; margin-top: 10px; background: var(--green); color: white; } .button-primary:not(:disabled):hover { background: var(--green-dark); } -.button-primary:disabled, .button-log:disabled { opacity: .42; } -.button-secondary { border: 1px solid #dfe6df; background: white; color: var(--ink); } -.button-secondary:hover { background: #f6f8f5; } -.button-text { min-height: 30px; padding: 0 6px; background: transparent; color: var(--green); } -.button-text:hover { color: var(--green-dark); } +.button-primary:disabled, .button-log:disabled { opacity: .46; } +.manual-note { margin: 8px 0 0; color: #748078; font-size: 9px; line-height: 1.4; } -.draft-panel { margin-top: 12px; border: 1px solid #d9e7de; border-radius: 13px; padding: 12px; background: #f7fbf8; } -.draft-heading { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 8px; } -.draft-heading h3 { margin: 0; font-size: 13px; } -.char-count { color: #87938d; font-size: 9px; } -.draft-panel textarea { min-height: 76px; padding: 10px; resize: vertical; line-height: 1.5; } -.draft-actions { display: flex; align-items: center; gap: 7px; margin-top: 7px; } -.manual-reminder { margin: 10px 0; color: #63736b; font-size: 9px; line-height: 1.45; } -.button-log { width: 100%; background: var(--lime); color: #21341f; } +.history-total { padding-bottom: 1px; color: #8a9791; font-size: 10px; white-space: nowrap; } +.queue-list, .history-list { display: flex; flex-direction: column; gap: 7px; } +.queue-item, .history-item { display: flex; align-items: center; justify-content: space-between; gap: 9px; border: 1px solid var(--line); border-radius: 10px; padding: 9px; background: white; } +.queue-copy, .history-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; } +.queue-copy strong, .history-copy strong { overflow: hidden; color: #2c3e36; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } +.queue-copy span, .history-copy span { color: #89958f; font-size: 9px; } +.queue-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 5px; } +.button-step { min-height: 29px; padding: 0 8px; background: #e6f2e9; color: #24533d; } +.button-step:not(:disabled):hover { background: #d8ebdd; } +.button-step:disabled { opacity: .45; } +.button-log { background: var(--lime); color: #21341f; } .button-log:not(:disabled):hover { background: #b7e768; } - -.history-section { margin-top: 20px; } -.history-heading { margin-bottom: 9px; } +.button-remove, .remove-entry { border: 0; background: transparent; color: #8e9b94; font-size: 9px; } +.button-remove:hover, .remove-entry:hover { color: #a14831; } .history-heading h2 { font-size: 15px; } -.history-total { padding-bottom: 1px; color: #8a9791; font-size: 10px; } -.history-list { display: flex; flex-direction: column; gap: 6px; } -.history-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line); border-radius: 10px; padding: 8px 9px; background: white; } -.history-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; } -.history-copy strong { overflow: hidden; color: #2c3e36; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.history-copy span { color: #89958f; font-size: 9px; } -.remove-entry { border: 0; background: transparent; color: #8e9b94; font-size: 9px; } -.remove-entry:hover { color: #a14831; } -.empty-state { margin: 0; border: 1px dashed #dbe2dc; border-radius: 10px; padding: 12px; color: #829089; font-size: 10px; text-align: center; } +.empty-state { margin: 0; border: 1px dashed #dbe2dc; border-radius: 10px; padding: 12px; color: #829089; font-size: 10px; line-height: 1.4; text-align: center; } .footer-note { display: flex; align-items: center; justify-content: center; gap: 5px; margin-top: 16px; color: #87938d; font-size: 9px; } .lock-mark { color: var(--green); font-size: 11px; } .toast { position: fixed; right: 16px; bottom: 12px; left: 16px; z-index: 2; border-radius: 9px; padding: 9px 10px; transform: translateY(8px); background: #203c31; color: white; font-size: 10px; text-align: center; opacity: 0; pointer-events: none; transition: opacity .16s, transform .16s; } diff --git a/repostexchange-companion/popup.html b/repostexchange-companion/popup.html index 47a8ca9..edce660 100644 --- a/repostexchange-companion/popup.html +++ b/repostexchange-companion/popup.html @@ -10,11 +10,9 @@
- +
-

YOUR CAMPAIGN DESK

+

YOUR LOCAL CAMPAIGN DESK

Repost Companion

LOCAL @@ -22,10 +20,7 @@

Repost Companion

-
-

TODAY

-

Your pace

-
+

TODAY

Your pace

@@ -33,9 +28,7 @@

Your pace

0 / 20
reposts logged
-
- -
+
12-hour window clear 0 / 10 @@ -47,56 +40,33 @@

Your pace

-
-

ONE TRACK AT A TIME

-

Make it personal

-
+

BUILD YOUR QUEUE

Add a track

01 / 02
-
- - + +
+ +

You control the listening and reposting on RepostExchange. Mark each step here after you do it.

+
- - - - -