also looking at this
Promote main to production: signed-out chrome and the subscriber tables
#17
54 files
+4252
-1353
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -1106,10 +1106,10 @@ export default { | ||
| 1106 | 1106 | * what all of them meant. bughq's btn-solid took the same union for the |
| 1107 | 1107 | * same reason. |
| 1108 | 1108 | */ |
| 1109 | 'btn-solid': 'bg-accent text-accent-ink rounded-[10px] ' | |
| 1110 | + '[transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease] ' | |
| 1111 | + 'hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] ' | |
| 1112 | + 'active:translate-y-px disabled:opacity-60 disabled:cursor-default', | |
| 1109 | 'btn-solid': 'btn-focus bg-accent !text-accent-ink !rounded-[10px] ' | |
| 1110 | + '![transition:transform_0.12s_ease,opacity_0.15s_ease,background-color_0.15s_ease] ' | |
| 1111 | + 'hover:!bg-[color-mix(in_srgb,var(--accent)_88%,#000)] ' | |
| 1112 | + 'active:translate-y-px disabled:!opacity-60 disabled:!cursor-default', | |
| 1113 | 1113 | |
| 1114 | 1114 | /* |
| 1115 | 1115 | * ## settings.stx's button vocabulary |
| @@ -1138,22 +1138,63 @@ export default { | ||
| 1138 | 1138 | * modal's Cancel and `btn-cta` is every primary action, and both are |
| 1139 | 1139 | * px-4 py-2.5 text-sm. |
| 1140 | 1140 | */ |
| 1141 | 'btn-quiet': 'bg-panel border border-line !text-muted ' | |
| 1142 | + '!rounded-[9px] [transition:color_0.15s_ease,border-color_0.15s_ease] ' | |
| 1143 | + 'hover:!text-ink hover:!border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]', | |
| 1141 | 'btn-quiet': 'btn-focus btn-disabled bg-panel border border-line !text-muted ' | |
| 1142 | + '!rounded-[9px] ![transition:color_0.15s_ease,border-color_0.15s_ease] ' | |
| 1143 | + 'hover:!bg-panel hover:!text-ink ' | |
| 1144 | + 'hover:!border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]', | |
| 1144 | 1145 | |
| 1145 | 'btn-danger': 'bg-panel border !border-[color-mix(in_srgb,var(--warn)_45%,var(--border))] ' | |
| 1146 | + '!text-warn !rounded-[9px] [transition:all_0.15s_ease] ' | |
| 1146 | 'btn-danger': 'btn-focus btn-disabled bg-panel border !border-[color-mix(in_srgb,var(--warn)_45%,var(--border))] ' | |
| 1147 | + '!text-warn !rounded-[9px] ![transition:all_0.15s_ease] ' | |
| 1147 | 1148 | + 'hover:!text-accent-ink hover:!bg-warn hover:!border-warn', |
| 1148 | 1149 | |
| 1149 | 1150 | // The copy button that sits over a code block. Its own entry rather than a |
| 1150 | 1151 | // size of btn-quiet: it is positioned, uses the sans stack against the |
| 1151 | 1152 | // mono block it overlays, and is the only control in the file at 11px. |
| 1152 | 'btn-copy': 'absolute right-2 top-2 bg-panel border border-line ' | |
| 1153 | + '!px-[9px] !py-[5px] !text-[11px] !font-medium font-sans leading-none ' | |
| 1153 | 'btn-copy': 'btn-focus btn-disabled absolute right-2 top-2 bg-panel border border-line ' | |
| 1154 | + '!px-[9px] !py-[5px] !text-[11px] !font-medium font-sans !leading-none ' | |
| 1154 | 1155 | + '!text-subtle cursor-pointer !rounded-[7px] ' |
| 1155 | + '[transition:color_0.15s_ease,border-color_0.15s_ease] ' | |
| 1156 | + 'hover:!text-ink hover:!border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]', | |
| 1156 | + '![transition:color_0.15s_ease,border-color_0.15s_ease] ' | |
| 1157 | + 'hover:!bg-panel hover:!text-ink ' | |
| 1158 | + 'hover:!border-[color-mix(in_srgb,var(--accent)_45%,var(--border))]', | |
| 1159 | ||
| 1160 | /* | |
| 1161 | * The app's own focus treatment, restored over <Button>'s. | |
| 1162 | * | |
| 1163 | * The component declares `focus:outline-none focus:ring-2 ...`, and a | |
| 1164 | * utility is unlayered while this app's `button:focus-visible { outline: | |
| 1165 | * 2px solid var(--accent) }` is a preflight in @layer tc-base -- so the | |
| 1166 | * component's ring wins at any specificity and the app's outline simply | |
| 1167 | * stops arriving. The two look nearly identical, but the component's fires | |
| 1168 | * on `:focus` and the app's on `:focus-visible`, so adopting <Button> | |
| 1169 | * would have put a ring on nine buttons after a MOUSE click while every | |
| 1170 | * other control in the app rings only for the keyboard. | |
| 1171 | * | |
| 1172 | * `focus:!ring-0` cancels the component's ring and the `:focus-visible` | |
| 1173 | * pair restores the preflight's rule at utility level, where it outranks | |
| 1174 | * the component. | |
| 1175 | * | |
| 1176 | * This spelling -- a variant on an arbitrary property -- emitted NOTHING | |
| 1177 | * until ts-css 0.3.16 (stacksjs/ts-css#36 for the generator, #37 for the | |
| 1178 | * extractor). It is the first place in this app that needed it, and it was | |
| 1179 | * measured through this app's own loader before being relied on. | |
| 1180 | */ | |
| 1181 | 'btn-focus': 'focus:!ring-0 ' | |
| 1182 | + 'focus-visible:![outline:2px_solid_var(--accent)] ' | |
| 1183 | + 'focus-visible:![outline-offset:2px]', | |
| 1184 | ||
| 1185 | /* | |
| 1186 | * One disabled appearance for every button. | |
| 1187 | * | |
| 1188 | * <Button> brings `disabled:opacity-50 disabled:cursor-not-allowed`, and | |
| 1189 | * btn-solid already declared 60% with a default cursor. Rather than let | |
| 1190 | * three paints disagree, all of them take the 60% that btn-solid's call | |
| 1191 | * sites have used all along. | |
| 1192 | * | |
| 1193 | * This is also a gain on the quiet and danger paints, which previously had | |
| 1194 | * no disabled styling at all: `:disabled="rotating()"` dimmed nothing, so a | |
| 1195 | * disabled Rotate button looked exactly like an enabled one. | |
| 1196 | */ | |
| 1197 | 'btn-disabled': 'disabled:!opacity-60 disabled:!cursor-default', | |
| 1157 | 1198 | |
| 1158 | 1199 | 'btn-xs': '!px-2.5 !py-1.5 !text-xs !font-medium', |
| 1159 | 1200 | 'btn-sm': '!px-3 !py-2 !text-sm !font-medium', |
| @@ -1307,7 +1307,7 @@ onMount(() => { | ||
| 1307 | 1307 | <code class="flex-1 px-3.5 py-2.5 min-w-[220px] font-mono text-ink text-sm tracking-[0.01em] truncate bg-canvas border border-line rounded-[10px] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:outline-none" :text="keyShown()">{{ keyMask }}</code> |
| 1308 | 1308 | <button @click="keyRevealed.set(!keyRevealed())" class="btn-quiet btn-sm" :text="keyRevealed() ? 'Hide' : 'Reveal'">Reveal</button> |
| 1309 | 1309 | <button @click="copyKey" class="btn-quiet btn-sm" :text="copiedKey() ? 'Copied' : 'Copy'">Copy</button> |
| 1310 | <button @click="rotateKey" :disabled="rotating()" class="btn-danger btn-sm">Rotate</button> | |
| 1310 | <Button variant="ghost" @click="rotateKey" :loading="rotating()" className="btn-danger btn-sm">Rotate</Button> | |
| 1311 | 1311 | </div> |
| 1312 | 1312 | <p class="mt-3.5 leading-relaxed text-subtle text-xs"> |
| 1313 | 1313 | Rotating generates a new key and <span class="text-warn">invalidates the current one immediately</span> — anything still sending the old key will get 401 until you update it (<span class="font-mono">X-LogHQ-Key</span>). |
| @@ -1384,6 +1384,29 @@ onMount(() => { | ||
| 1384 | 1384 | <div class="mt-0.5 text-subtle text-xs">Can read the log stream</div> |
| 1385 | 1385 | </div> |
| 1386 | 1386 | <span class="px-[9px] py-0.5 font-semibold text-[11px] text-accent tracking-[0.02em] bg-[color-mix(in_srgb,var(--accent)_14%,var(--panel))] rounded-[999px]">Member</span> |
| 1387 | {{-- STILL A PLAIN BUTTON, and the reason is x-tooltip. | |
| 1388 | ||
| 1389 | This is one of four buttons in the file that hand-roll busy | |
| 1390 | state and would otherwise take <Button> like the five that | |
| 1391 | did: these two Removes, the channel Test and the channel | |
| 1392 | Remove. An `x-tooltip` on a COMPONENT never reaches the DOM, | |
| 1393 | so adopting here would delete the tooltip without any sign. | |
| 1394 | ||
| 1395 | Measured on the installed 0.2.532, not read off a changelog. | |
| 1396 | `x-tooltip` IS in COMPONENT_PASSTHROUGH_X_ATTRS now, so it is | |
| 1397 | no longer rewritten into a `:tooltip` signal binding -- that | |
| 1398 | is the crash dashboard.stx documents, and it is fixed. What | |
| 1399 | remains is quieter: processComponents emits the attribute only | |
| 1400 | inside `data-stx-props`, while the tooltip runtime reads | |
| 1401 | `el.getAttribute('x-tooltip')`. `title` and `aria-label` both | |
| 1402 | forward to the concrete root correctly (stx#2063); | |
| 1403 | `x-tooltip` falls in the gap between the two. | |
| 1404 | ||
| 1405 | `title` instead would work and would be a different tooltip -- | |
| 1406 | the browser's, not the app's. Not worth trading a consistent | |
| 1407 | surface for a spinner. Filed upstream; revisit when it lands. | |
| 1408 | tests/unit/button-vocabulary.test.ts fails if a <Button> here | |
| 1409 | ever gets an x-tooltip. --}} | |
| 1387 | 1410 | <button @click="removeMember('')" :disabled="removingMember() === ''" :text="memberBtnLabel('')" class="btn-danger btn-xs" x-tooltip="Remove">Remove</button> |
| 1388 | 1411 | </div> |
| 1389 | 1412 | @endforeach |
| @@ -1404,7 +1427,7 @@ onMount(() => { | ||
| 1404 | 1427 | <div class="mb-3 font-semibold text-sm">Invite a teammate</div> |
| 1405 | 1428 | <div class="flex flex-wrap gap-2 items-center"> |
| 1406 | 1429 | <input :model="inviteEmail" type="email" aria-label="Invite a teammate by email" class="flex-1 px-3.5 py-2.5 min-w-[220px] text-ink text-sm bg-canvas border border-line rounded-[10px] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:outline-none" placeholder="teammate@company.com" autocomplete="off"> |
| 1407 | <button @click="addInvite" :disabled="inviting()" class="btn-cta btn-solid" :text="inviting() ? 'Inviting…' : 'Invite'">Invite</button> | |
| 1430 | <Button variant="ghost" @click="addInvite" :loading="inviting()" className="btn-cta btn-solid">Invite</Button> | |
| 1408 | 1431 | </div> |
| 1409 | 1432 | <span class="mt-2.5 text-ok text-xs" :show="!!inviteNote()" :class="inviteNoteOk() ? 'mt-2.5 text-xs text-ok' : 'mt-2.5 text-xs text-accent'">{{ inviteNote() }}</span> |
| 1410 | 1433 | <p class="mt-3.5 leading-relaxed text-subtle text-xs"> |
| @@ -1467,7 +1490,7 @@ onMount(() => { | ||
| 1467 | 1490 | <input :model="whUrl" :placeholder="whPlaceholder()" type="url" aria-label="Webhook URL" class="mb-2 px-3.5 py-2.5 w-full font-mono text-ink text-sm bg-canvas border border-line rounded-[10px] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:outline-none" placeholder="https://hooks.slack.com/services/…" autocomplete="off" spellcheck="false"> |
| 1468 | 1491 | <input :model="whLabel" type="text" aria-label="Channel label" class="mb-3 px-3.5 py-2.5 w-full text-ink text-sm bg-canvas border border-line rounded-[10px] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:outline-none" placeholder="Label (optional) — e.g. #alerts" maxlength="255"> |
| 1469 | 1492 | <div class="flex gap-3 items-center"> |
| 1470 | <button @click="addChannel" :disabled="adding()" class="btn-cta btn-solid" :text="adding() ? 'Adding…' : 'Add channel'">Add channel</button> | |
| 1493 | <Button variant="ghost" @click="addChannel" :loading="adding()" className="btn-cta btn-solid">Add channel</Button> | |
| 1471 | 1494 | <span class="text-ok text-xs" :show="!!whNote()" :class="whNoteOk() ? 'text-xs text-ok' : 'text-xs text-accent'">{{ whNote() }}</span> |
| 1472 | 1495 | </div> |
| 1473 | 1496 | <p :text="whHelp()" class="mt-3.5 leading-relaxed text-subtle text-xs"> |
| @@ -1560,7 +1583,7 @@ onMount(() => { | ||
| 1560 | 1583 | <p class="text-sm text-subtle">Rejects new log entries for <span class="font-mono text-muted">{{ activeProject?.name }}</span> with a 403. Everything already stored is kept — you can unarchive anytime.</p> |
| 1561 | 1584 | @endif |
| 1562 | 1585 | </div> |
| 1563 | <button @click="openArchive" :disabled="archiving()" class="flex-none whitespace-nowrap btn-cta btn-danger">{{ activeProject?.is_active === false ? 'Unarchive' : 'Archive' }}</button> | |
| 1586 | <Button variant="ghost" @click="openArchive" :loading="archiving()" className="flex-none whitespace-nowrap btn-cta btn-danger">{{ activeProject?.is_active === false ? 'Unarchive' : 'Archive' }}</Button> | |
| 1564 | 1587 | </div> |
| 1565 | 1588 | <div class="h-px bg-[color-mix(in_srgb,var(--warn)_20%,var(--border))]"></div> |
| 1566 | 1589 | {{-- Delete --}} |
| @@ -1593,7 +1616,7 @@ onMount(() => { | ||
| 1593 | 1616 | <input :model="modalInput" type="text" :aria-label="modalTitle()" aria-label="Type the project name to confirm" class="mb-4 px-3.5 py-2.5 w-full font-mono text-ink text-sm bg-canvas border border-line rounded-[10px] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:outline-none" autocomplete="off" autocapitalize="off" spellcheck="false"> |
| 1594 | 1617 | <div class="flex gap-2 items-center justify-end"> |
| 1595 | 1618 | <button @click="closeModal" type="button" class="btn-md btn-quiet">Cancel</button> |
| 1596 | <button @click="confirmModal" :disabled="!modalReady()" :text="modalConfirmLabel()" type="button" class="btn-cta btn-danger">Confirm</button> | |
| 1619 | <Button type="button" variant="ghost" @click="confirmModal" :disabled="!modalReady()" :loading="modalBusy()" :label="modalConfirmLabel()" className="btn-cta btn-danger">Confirm</Button> | |
| 1597 | 1620 | </div> |
| 1598 | 1621 | <span class="mt-2.5 text-accent text-xs" :show="!!modalNote()">{{ modalNote() }}</span> |
| 1599 | 1622 | </div> |