diff --git a/.gitignore b/.gitignore index 8b182cf..4ff9bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /coverage # production -/build +# /build # misc .DS_Store @@ -23,3 +23,6 @@ yarn-debug.log* yarn-error.log* .env + +# synced from web_themes (build-time pull, do not commit) +/src/styles/theme diff --git a/package.json b/package.json index 3983bdd..352d0e2 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "react-scripts": "^5.0.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "start": "npm run sync:theme && react-scripts start", + "build": "npm run sync:theme && react-scripts build", "test": "react-scripts test --watchAll=false", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "sync:theme": "bash scripts/sync-theme.sh" }, "eslintConfig": { "extends": [ @@ -38,4 +39,4 @@ "devDependencies": { "cross-env": "^7.0.3" } -} \ No newline at end of file +} diff --git a/scripts/sync-theme.sh b/scripts/sync-theme.sh new file mode 100755 index 0000000..86a64c6 --- /dev/null +++ b/scripts/sync-theme.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# sync-theme.sh — pull a theme from the web_themes repo into this consumer. +# +# Usage: +# bash scripts/sync-theme.sh [theme-name] +# +# Environment overrides: +# WEB_THEMES_REPO git URL of the web_themes repo (default: Tangled handle URL) +# THEME_DEST destination dir relative to repo root (default: src/styles/theme) +# +# Strategy (build-time pull): shallow-clone the themes repo into a per-user +# cache (~/.cache/web_themes), then copy the requested theme's files into the +# consumer's src tree. Consumers wire this as a prebuild step: +# +# "build": "npm run sync:theme && react-scripts build" +# +# Requires SSH access to Tangled at build time (Apple Keychain on this Mac). + +set -euo pipefail + +THEME_NAME="${1:-witchsky-cyan}" +REPO="${WEB_THEMES_REPO:-git@tangled.org:psingletary.com/web_themes}" +DEST="${THEME_DEST:-src/styles/theme}" +CACHE_DIR="${HOME}/.cache/web_themes" + +if [ -d "${CACHE_DIR}/.git" ]; then + # Cache must track the requested repo even if it was cloned from + # a different URL before (e.g. a local path during testing). + if [ "$(git -C "${CACHE_DIR}" config --get remote.origin.url)" != "${REPO}" ]; then + git -C "${CACHE_DIR}" remote set-url origin "${REPO}" + fi + git -C "${CACHE_DIR}" fetch --quiet --depth 1 origin main + git -C "${CACHE_DIR}" reset --quiet --hard origin/main +else + git clone --quiet --depth 1 --branch main "${REPO}" "${CACHE_DIR}" +fi + +mkdir -p "${DEST}" +cp "${CACHE_DIR}/themes/${THEME_NAME}/theme.css" "${DEST}/theme.css" +cp "${CACHE_DIR}/themes/${THEME_NAME}/index.css" "${DEST}/index.css" +echo "synced web_themes/${THEME_NAME} -> ${DEST}/" diff --git a/src/App.css b/src/App.css index c616a8a..d5230c1 100644 --- a/src/App.css +++ b/src/App.css @@ -1,284 +1,4 @@ -/* Verifier — witchsky cyan base theme */ - -:root { - --text: #F9FAFB; - --text-muted: #A5B2C5; - --button-bg: hsl(174, 83%, 38%); - --button-text: #FFFFFF; - --button-hover-bg: hsl(174, 78%, 32%); - --card-bg: #1C2B35; - --card-border: #394A58; - --navbar-bg: #15232C; - --success-bg: rgba(45, 242, 143, 0.12); - --success-text: #2CF28F; - --success-border: rgba(45, 242, 143, 0.25); - --error-bg: rgba(246, 90, 127, 0.12); - --error-text: hsl(348, 92%, 64%); - --error-border: rgba(246, 90, 127, 0.25); -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 18px; - background: #111822; - color: var(--text); - min-height: 100vh; -} - -h1, h2, h3 { color: var(--text); } -h1 { font-size: 2em; margin: 0.5em 0; } -h2 { font-size: 1.5em; margin: 1em 0 0.5em; } -h3 { font-size: 1.2em; margin: 0.8em 0 0.4em; } -p { line-height: 1.6; margin: 0.8em 0; } -a { color: var(--button-bg); text-decoration: none; } -a:hover { text-decoration: underline; } -a:visited { color: var(--button-hover-bg); } - -.app-container { min-height: 100vh; display: flex; flex-direction: column; } -.main-container { max-width: 800px; margin: 0 auto; padding: 20px; width: 100%; flex: 1; } - -form { - display: flex; - flex-direction: column; - gap: 12px; - background: var(--card-bg); - border: 1px solid var(--card-border); - border-radius: 8px; - padding: 20px; -} - -input, select { - padding: 10px 14px; - border: 1px solid var(--card-border); - border-radius: 6px; - background: var(--card-bg); - color: var(--text); - font-size: 1em; - min-height: 48px; -} - -input:focus, select:focus { border-color: var(--button-bg); outline: none; } - -button { - padding: 10px 20px; - border: none; - border-radius: 6px; - font-size: 1em; - font-weight: 600; - cursor: pointer; - min-height: 48px; - background: var(--button-bg); - color: var(--button-text); -} - -button:hover { background: var(--button-hover-bg); } -button:disabled { opacity: 0.6; cursor: not-allowed; } - -/* Navbar */ -.navbar { - background: var(--navbar-bg); - padding: 12px 24px; - border-bottom: 1px solid var(--card-border); - display: flex; - justify-content: space-between; - align-items: center; -} - -.navbar-logo { font-size: 1.4em; font-weight: 700; } -.navbar-logo a { color: var(--button-bg); text-decoration: none; } -.navbar-logo a:hover { text-decoration: none; } -.navbar-actions { display: flex; gap: 10px; align-items: center; } - -.navbar .login-button { background: var(--button-bg); color: var(--button-text); } -.navbar .logout-button { background: transparent; border: 1px solid var(--card-border); color: var(--text); } - -/* Footer */ -.footer { - background: var(--navbar-bg); - padding: 14px 24px; - border-top: 1px solid var(--card-border); - text-align: center; - color: var(--text-muted); - font-size: 0.9em; -} - -/* Login */ -.login-page { - display: flex; - justify-content: center; - align-items: center; - min-height: 60vh; - padding: 20px; - text-align: center; -} - -.login-content { - max-width: 500px; - width: 100%; - padding: 30px; - background: var(--card-bg); - border: 1px solid var(--card-border); - border-radius: 8px; -} - -.login-content h1 { margin-top: 0; } -.login-content p { margin-bottom: 20px; } - -/* Verifier */ -.verifier-container { max-width: 600px; margin: 30px auto; padding: 0 20px; } - -.verifier-section { - background: var(--card-bg); - border: 1px solid var(--card-border); - border-radius: 8px; - padding: 24px; - margin-bottom: 16px; -} - -.verifier-section h2 { margin-top: 0; font-size: 1.3em; } - -.verifier-form-container { - display: flex; - gap: 10px; - flex-wrap: wrap; - align-items: stretch; - padding: 0; - border: 0; - background: none; - border-radius: 0; -} - -.verifier-form-container input, -.verifier-form-container select { flex: 1; min-width: 150px; } - -.verifier-submit-button { white-space: nowrap; } - -/* Suggestions */ -.verifier-suggestions-list { - list-style: none; - padding: 0; - margin: 10px 0 0 0; - border: 1px solid var(--card-border); - border-radius: 6px; - max-height: 250px; - overflow-y: auto; - background: var(--card-bg); -} - -.verifier-suggestion-item { - padding: 10px 14px; - cursor: pointer; - border-bottom: 1px solid var(--card-border); - display: flex; - align-items: center; - gap: 10px; - min-height: 44px; -} - -.verifier-suggestion-item:last-child { border-bottom: none; } -.verifier-suggestion-item:hover { background: var(--button-bg); color: var(--button-text); } -.verifier-suggestion-item.loading, .verifier-suggestion-item.none { color: var(--text-muted); font-style: italic; } -.verifier-suggestion-item:hover.loading, .verifier-suggestion-item:hover.none { background: none; color: var(--text-muted); } - -.verifier-suggestion-avatar { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; } -.verifier-suggestion-text { display: flex; flex-direction: column; overflow: hidden; } -.verifier-suggestion-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.verifier-suggestion-handle { font-size: 0.9em; color: var(--text-muted); } - -/* Duplicate note */ -.verifier-duplicate-note { - margin: 10px 0 0 0; - padding: 10px 14px; - background: var(--success-bg); - color: var(--success-text); - border: 1px solid var(--success-border); - border-radius: 6px; - font-style: italic; -} - -/* Status Box */ -.verifier-status-box { - padding: 14px; - border-radius: 6px; - margin: 14px 0; -} - -.verifier-status-box-success { background: var(--success-bg); color: var(--success-text); border: 1px solid var(--success-border); } -.verifier-status-box-error { background: var(--error-bg); color: var(--error-text); border: 1px solid var(--error-border); } -.verifier-status-box p { margin: 0; } -.verifier-status-box a { color: var(--success-text); font-weight: 600; text-decoration: underline; } - -/* Verifier list */ -.verifier-list { list-style: none; padding: 0; margin: 0; } - -.verifier-list-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px; - border: 1px solid var(--card-border); - border-radius: 6px; - margin-bottom: 8px; - background: var(--card-bg); - gap: 10px; - flex-wrap: wrap; -} - -.verifier-list-item-content { flex: 1; min-width: 200px; } - -.verifier-display-name { font-weight: 600; display: block; } -.verifier-display-name a { color: var(--text); text-decoration: none; } -.verifier-display-name a:hover { color: var(--button-bg); } - -.verifier-list-item-handle { font-size: 0.9em; color: var(--text-muted); } -.verifier-list-item-date { font-size: 0.85em; color: var(--text-muted); margin-top: 4px; } - -.verifier-list-item-actions { flex-shrink: 0; } - -/* Revoke button — only flash animation kept */ -@keyframes flash-red-warning { - 0%, 100% { background-color: var(--error-bg); color: var(--error-text); box-shadow: 0 0 0 0 rgba(246, 90, 127, 0.4); } - 50% { background-color: var(--error-text); color: var(--button-text); box-shadow: 0 0 0 8px rgba(246, 90, 127, 0); } -} - -.verifier-revoke-button { - background: transparent; - border: 1px solid var(--error-text); - color: var(--error-text); - font-weight: 600; - border-radius: 6px; - padding: 8px 16px; - cursor: pointer; - min-height: 44px; - font-size: 0.95em; -} - -.verifier-revoke-button:hover { animation: flash-red-warning 2.5s ease-in-out infinite; border-color: var(--error-text); } -.verifier-revoke-button:disabled { animation: none; opacity: 0.6; cursor: not-allowed; } - -/* Load more */ -.verifier-load-more-container { margin-top: 14px; text-align: center; } - -.verifier-action-button { - background: var(--button-bg); - color: var(--button-text); - border: none; - border-radius: 6px; - padding: 10px 24px; - font-weight: 600; - cursor: pointer; -} - -.verifier-action-button:hover { background: var(--button-hover-bg); } -.verifier-action-button:disabled { opacity: 0.6; cursor: not-allowed; } - -/* Mobile */ -@media (max-width: 480px) { - .verifier-list-item { flex-direction: column; align-items: flex-start; } - .verifier-list-item-actions { align-self: flex-end; } - .verifier-form-container { flex-direction: column; } -} +/* Verifier — app-specific styles (P5/P4/P7 additions). Base theme: web_themes (witchsky-cyan). */ /* --- P5/P4/P7 additions --- */ @@ -356,3 +76,4 @@ button:disabled { opacity: 0.6; cursor: not-allowed; } .verifier-reissue-button:hover { background: var(--button-hover-bg); } .verifier-reissue-button:disabled { opacity: 0.6; cursor: not-allowed; } + diff --git a/src/index.css b/src/index.css deleted file mode 100644 index a84fbc4..0000000 --- a/src/index.css +++ /dev/null @@ -1,17 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} - -body { - transition: background-color 0.3s ease, color 0.3s ease; -} diff --git a/src/index.js b/src/index.js index f00a9cb..a76ae39 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,12 @@ // src/index.js +// Theme CSS must be imported BEFORE App.css so app-specific rules can +// override theme rules (CSS order follows import order). +import "./styles/theme/index.css"; +import "./styles/theme/theme.css"; import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { ThemeProvider } from './contexts/ThemeContext'; -import "./index.css"; const container = document.getElementById('root'); const root = ReactDOM.createRoot(container); @@ -14,4 +17,4 @@ root.render( -); \ No newline at end of file +);