diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ba2c4cc..2cab376 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,14 @@
name: PHP Checks
+concurrency:
+ group: php-checks-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
on:
+ # Pull requests validate topic branches; push runs are reserved for main to avoid
+ # duplicate CI runs for every commit on a branch that already has an open PR.
push:
- branches: [main, "feat/**", "fix/**", "test/**"]
+ branches: [main]
pull_request:
workflow_dispatch:
@@ -12,6 +18,7 @@ permissions:
jobs:
checks:
runs-on: ubuntu-latest
+ timeout-minutes: 20
services:
mariadb:
image: mariadb:10.11
@@ -31,7 +38,7 @@ jobs:
php: ["8.2", "8.3", "8.4"]
steps:
- name: Check out repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up PHP
uses: shivammathur/setup-php@v2
@@ -52,10 +59,16 @@ jobs:
- name: Check design-system assets
run: php tests/design-system.php
+ - name: Check workspace UI contracts
+ run: php tests/workspace-ui.php
+
+ - name: Check deployment workflow safety
+ run: php tests/deployment-workflow.php
+
- name: Check JavaScript syntax
run: |
node --check public/assets/js/ui.js
- node --check public/assets/js/design-system-demo.js
+ node --check public/assets/js/workspace.js
- name: Run MariaDB integration tests
env:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..29e869b
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,106 @@
+name: Deploy production
+
+on:
+ workflow_dispatch:
+ inputs:
+ confirm_production:
+ description: Type DEPLOY to confirm a production deployment
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+concurrency:
+ group: production-deployment
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ if: ${{ inputs.confirm_production == 'DEPLOY' && github.ref == 'refs/heads/main' }}
+ runs-on: ubuntu-latest
+ environment: production
+ timeout-minutes: 15
+ steps:
+ - name: Check out the selected revision
+ uses: actions/checkout@v5
+
+ - name: Validate deployment configuration
+ shell: bash
+ env:
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+ DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
+ DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
+ DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+ run: |
+ set -euo pipefail
+ for name in DEPLOY_HOST DEPLOY_USER DEPLOY_SSH_PRIVATE_KEY DEPLOY_KNOWN_HOSTS DEPLOY_PATH; do
+ if [[ -z "${!name}" ]]; then
+ echo "::error::Required production secret is missing: ${name}"
+ exit 1
+ fi
+ done
+ if [[ ! "$DEPLOY_PATH" =~ ^/[A-Za-z0-9._/-]+$ ]]; then
+ echo "::error::DEPLOY_PATH must be an absolute path using only letters, numbers, dot, underscore, slash, or hyphen"
+ exit 1
+ fi
+ if [[ "$DEPLOY_PATH" == *"/../"* || "$DEPLOY_PATH" == */.. || "$DEPLOY_PATH" == "/" || "$DEPLOY_PATH" == "/home" || "$DEPLOY_PATH" == "/var" ]]; then
+ echo "::error::DEPLOY_PATH is too broad to be a safe application target"
+ exit 1
+ fi
+
+ - name: Configure pinned SSH identity
+ shell: bash
+ env:
+ DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
+ DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
+ run: |
+ set -euo pipefail
+ install -m 700 -d "$HOME/.ssh"
+ printf '%s\n' "$DEPLOY_SSH_PRIVATE_KEY" > "$HOME/.ssh/deploy_key"
+ chmod 600 "$HOME/.ssh/deploy_key"
+ printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
+ chmod 600 "$HOME/.ssh/known_hosts"
+
+ - name: Validate remote SSH access and target directory
+ shell: bash
+ env:
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+ DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+ DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
+ run: |
+ set -euo pipefail
+ port="${DEPLOY_PORT:-22}"
+ [[ "$port" =~ ^[0-9]+$ ]] || { echo "::error::DEPLOY_PORT must be numeric"; exit 1; }
+ ssh -i "$HOME/.ssh/deploy_key" -p "$port" \
+ -o BatchMode=yes -o StrictHostKeyChecking=yes \
+ "$DEPLOY_USER@$DEPLOY_HOST" \
+ "test -d '$DEPLOY_PATH' && test -w '$DEPLOY_PATH'"
+
+ - name: Deploy application files without deleting remote data
+ shell: bash
+ env:
+ DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
+ DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
+ DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
+ DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
+ run: |
+ set -euo pipefail
+ port="${DEPLOY_PORT:-22}"
+ rsync -az --checksum \
+ --exclude='/.git/' \
+ --exclude='/.github/' \
+ --exclude='/tests/' \
+ --exclude='/docs/' \
+ --exclude='/.env' \
+ --exclude='/.env.*' \
+ --exclude='/*.log' \
+ -e "ssh -i $HOME/.ssh/deploy_key -p $port -o BatchMode=yes -o StrictHostKeyChecking=yes" \
+ ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/"
+
+ - name: Remove temporary SSH credentials
+ if: always()
+ shell: bash
+ run: rm -f "$HOME/.ssh/deploy_key" "$HOME/.ssh/known_hosts"
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..e2f816e
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,30 @@
+Options -Indexes
+
+
+ RewriteEngine On
+
+ # Keep repository metadata and secret dotfiles inaccessible.
+ RewriteRule (^|/)\.(?!well-known/) - [F,L]
+
+ # An optional repository-root document root must never expose the source tree.
+ # Only the public assets are mapped directly; every application URL goes through PHP.
+ RewriteRule ^public(?:/|$) - [F,L,NC]
+ RewriteRule ^\.well-known/ - [END]
+ RewriteRule ^assets/(.*)$ public/assets/$1 [END,NC]
+ RewriteRule ^ public/index.php [QSA,END]
+
+
+
+ AddType text/css .css
+ AddType application/javascript .js
+
+
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
+ Header always set X-Frame-Options "DENY"
+ Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'"
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8313304..a36e60d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
## Unreleased
+- Add a manual, main-only production delivery workflow with typed confirmation, production environment gate, pinned SSH host verification, destination validation, and non-destructive rsync.
+- Add CI contract tests for deployment safety and document required production secrets and controlled migration steps.
+- Reduce redundant CI traffic by running topic-branch validation through pull requests and reserving push-triggered CI for main.
+- Fix a workspace UI contract test to match idempotent `CREATE TABLE IF NOT EXISTS` migration statements.
+
+- Fix CSS/JavaScript delivery for hosts whose document root is the repository root by routing `/assets/*` to the public asset directory without exposing application source files.
+- Explicitly declare static CSS/JavaScript MIME types and apply immutable caching to versioned assets; bump workspace asset keys to avoid stale cached styles and scripts.
+- Run the previously unwired workspace UI contract test in CI and syntax-check the production `workspace.js` controller.
+- Remove the standalone visual showcase page and demo-only assets; retain shared design-system CSS that the live workspace uses.
+
+
- Removed unused duplicate team/project settings handlers and aligned UI contract tests with the registered transactional update endpoints.
- Add a project Files tab with a searchable private source-file directory, lightweight text editor, save/reload controls, and revision-history restore.
diff --git a/README.md b/README.md
index 99f907b..3b16624 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# TeamCode Hub
-**Version:** 0.2.0-dev
+**Version:** 0.2.2-dev
**Status:** Active development
TeamCode Hub is a modular, security-focused collaboration platform for software teams, designed for conventional PHP hosting with cPanel/DirectAdmin and MySQL/MariaDB.
@@ -63,7 +63,7 @@ See [docs/API.md](docs/API.md) for request bodies, status codes, retention, and
## Validation
-CI checks PHP syntax and smoke tests, design-system and workspace UI contracts, JavaScript syntax, and MariaDB integration tests on PHP 8.2, 8.3, and 8.4.
+CI checks PHP syntax and smoke tests, design-system and workspace UI contracts, deployment safety contracts, JavaScript syntax, and MariaDB integration tests on PHP 8.2, 8.3, and 8.4. Production delivery is manual-only from `main`, requires a typed confirmation and pinned SSH host key, and must be configured with environment-scoped deployment secrets; it never deletes remote files or runs migrations automatically.
Run the lightweight local checks with:
@@ -71,8 +71,8 @@ Run the lightweight local checks with:
php tests/run.php
php tests/design-system.php
php tests/workspace-ui.php
+php tests/deployment-workflow.php
node --check public/assets/js/ui.js
-node --check public/assets/js/design-system-demo.js
node --check public/assets/js/workspace.js
```
diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md
index 5039cea..6046437 100644
--- a/docs/DESIGN-SYSTEM.md
+++ b/docs/DESIGN-SYSTEM.md
@@ -11,20 +11,20 @@ This document describes the shared visual and interaction foundation used by Tea
| `public/assets/css/base.css` | Browser defaults, semantic element defaults, focus visibility, and reduced motion |
| `public/assets/css/layout.css` | Containers, responsive grids, page shell, sidebar, and header layouts |
| `public/assets/css/components.css` | Buttons, forms, badges, cards, alerts, tables, tabs, overlays, and feedback |
-| `public/assets/css/patterns.css` | Upload/range controls, avatar states, pagination, accordions, tooltips, workflow steps, quote/code, and footer patterns |
+| `public/assets/css/patterns.css` | Reusable upload/range controls, avatar states, pagination, accordions, tooltips, workflow steps, and content patterns |
| `public/assets/css/utilities.css` | Small composable spacing, text, surface, and alignment helpers |
-| `public/assets/js/ui.js` | Accessible interactions shared by product pages |\n| `public/assets/css/workspace.css` | Product-specific workspace, dashboard, project, task, and chat presentation |\n| `public/assets/js/workspace.js` | Authenticated API integration and workspace interactions |\n| `public/app.html` | English workspace shell and sign-in form |
-| `public/assets/css/design-system-demo.css` | Styles unique to the reference showcase |
-| `public/assets/js/design-system-demo.js` | Demo-only button handlers |
-| `public/design-system.html` | English component reference page |
+| `public/assets/css/workspace.css` | Product-specific workspace, dashboard, team, project, task, and chat presentation |
+| `public/assets/js/ui.js` | Accessible interactions shared by product pages |
+| `public/assets/js/workspace.js` | Authenticated API integration and workspace interactions |
+| `public/app.html` | English workspace shell and sign-in form |
## Loading the design system
Load the entry stylesheet and the shared UI controller from a page template. Keep product-specific styles and behavior in separate files and avoid inline style or script blocks.
```html
-
-
+
+
```
The entry stylesheet imports tokens, base rules, layout, core components, extended patterns, and utilities in dependency order. Every imported stylesheet URL carries a cache key. When CSS changes, bump the CSS asset query keys in the entry imports and HTML entry points together. A production release should use its actual SemVer version.
@@ -85,14 +85,14 @@ Run:
```sh
php tests/design-system.php
node --check public/assets/js/ui.js
-node --check public/assets/js/design-system-demo.js
+node --check public/assets/js/workspace.js
```
CI runs these checks in addition to PHP lint, smoke tests, and MariaDB integration tests. These checks do not replace manual browser review, assistive-technology testing, or visual regression testing.
## Current scope
-The showcase is a reference implementation of the style system, not a claim that the product dashboard, team/project screens, or chat interface are complete. Product screens must be built on the existing APIs and retain server-side authentication and authorization checks.
+The shared style layers are loaded by the actual workspace shell; they are not a standalone product or a substitute for connected application behavior. Team, project, task, file, and chat actions must use the existing APIs and retain server-side authentication and authorization checks.
## Extended component coverage
diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md
index 314fa66..d2e8de2 100644
--- a/docs/OPERATIONS.md
+++ b/docs/OPERATIONS.md
@@ -3,6 +3,7 @@
## Before deployment
- Configure the document root to the repository's `public/` directory where possible.
+- If the host cannot point its document root at `public/`, the repository-root `.htaccess` provides a locked-down fallback: it exposes only `/assets/*` from `public/assets/` and routes app paths through `public/index.php`. Do not disable the root rewrite rules, because they prevent direct access to repository source files.
- Configure database environment variables through the host or a private configuration mechanism outside the web root.
- Create a dedicated database user with only the permissions the application requires.
- Enable HTTPS and configure session cookie security accordingly. If TLS terminates at a trusted reverse proxy, set `APP_HTTPS=true` only when the deployment topology guarantees HTTPS to the browser.
@@ -10,6 +11,19 @@
- Confirm private storage, configuration, backup, and log paths cannot be downloaded over HTTP.
- Back up the database and files before applying schema changes.
+## GitHub Actions deployment (manual)
+
+The `.github/workflows/deploy.yml` workflow is a manual SSH-based delivery path. It only permits the `main` branch, requires typing `DEPLOY`, uses the GitHub `production` environment, verifies a pinned SSH host key, checks that the target directory already exists and is writable, and does not delete remote files. Configure environment-scoped secrets before using it:
+
+- `DEPLOY_HOST` — server hostname (no protocol or path).
+- `DEPLOY_USER` — dedicated deployment account with access only to the application directory.
+- `DEPLOY_SSH_PRIVATE_KEY` — private key for that account; never commit it.
+- `DEPLOY_KNOWN_HOSTS` — independently verified SSH host-key line(s), not output blindly trusted from a runtime `ssh-keyscan`.
+- `DEPLOY_PATH` — absolute application-root path containing the `app/` and `public/` directories; it must not be the filesystem root or a broad system directory.
+- `DEPLOY_PORT` — optional SSH port; defaults to `22`. For a non-standard port, the pinned `known_hosts` entry must use the matching `[host]:port` format.
+
+Set production environment reviewers and branch restrictions in GitHub repository settings. Point the web server document root to the deployed `public/` directory when possible. The workflow deliberately does not run database migrations or create an administrator automatically: take a backup, review pending migrations, and apply `php bin/migrate.php` from a trusted server-side CLI session as a separate controlled operation. Configure the required application environment variables on the server before enabling traffic. The first deployment must be tested on a staging target before production use.
+
## Registration and email verification
Public registration is enabled after the account-token migration has been applied. Configure these environment variables outside the web root:
diff --git a/docs/WORKSPACE-UI.md b/docs/WORKSPACE-UI.md
index 7ab1a7a..3d2a351 100644
--- a/docs/WORKSPACE-UI.md
+++ b/docs/WORKSPACE-UI.md
@@ -7,13 +7,14 @@ This document maps the English browser application to the existing server APIs.
- `/` — authenticated workspace shell; shows sign-in when there is no active session.
- `/login` — the same progressive-enhancement shell, initially displaying sign-in when unauthenticated.
- `/verify-email` — the workspace shell used to confirm a one-time token supplied in the URL fragment.
-- `/design-system.html` — component reference page.
- `public/app.html` — semantic workspace shell.
- `public/assets/css/app.css` and its token/base/layout/component/pattern/utility layers — shared design-system styles.
- `public/assets/css/workspace.css` — product-only styles layered on the core design system.
- `public/assets/js/workspace.js` — same-origin API client, navigation, rendering, forms, and chat polling.
- `public/assets/js/ui.js` — shared design-system interactions.
+The preferred deployment sets the document root to `public/`. A repository-root document root is also supported by the top-level `.htaccess`, which maps `/assets/*` to `public/assets/*`, routes application paths to `public/index.php`, and blocks direct access to the `public/` prefix and repository files.
+
Project repository links accept only public HTTPS GitHub URLs. The server shares one daily cached snapshot per repository and caps refresh attempts to 20 distinct repositories per UTC day.
The HTML has no inline style or script blocks. The front controller returns HTML with a restrictive same-origin Content Security Policy. CSS and JavaScript are external, versioned files; update their query-string version when releasing a changed asset set.
@@ -103,4 +104,4 @@ php tests/workspace-ui.php
node --check public/assets/js/workspace.js
```
-These checks validate the static asset contract and JavaScript syntax. They do not replace manual browser testing across supported devices, keyboard-only interaction, screen-reader review, or visual regression tests. The design-system showcase includes extended input, avatar, chip, accordion, workflow-step, pagination, and content patterns.
+These checks validate the static asset contract and JavaScript syntax. They do not replace manual browser testing across supported devices, keyboard-only interaction, screen-reader review, or visual regression tests. The style contract tests check actual production assets and route wiring; reusable patterns stay in the shared `patterns.css` layer instead of a separate demo page.
diff --git a/public/.htaccess b/public/.htaccess
index 26ca016..1f2475e 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -13,7 +13,16 @@ DirectoryIndex index.php
RewriteRule ^ index.php [QSA,L]
+
+ # Keep strict nosniff policies compatible with directly served static assets.
+ AddType text/css .css
+ AddType application/javascript .js
+
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Frame-Options "DENY"
diff --git a/public/app.html b/public/app.html
index aea8d23..66f0c3a 100644
--- a/public/app.html
+++ b/public/app.html
@@ -6,10 +6,10 @@
TeamCode Hub — Workspace
-
-
-
-
+
+
+
+
@@ -171,11 +171,11 @@
Verify your email
-
+
+ Your teamsLoading teams…
+ ↗
+
-
?Need a hand?Workspace guide↗
UYour accountWorkspace member
diff --git a/public/assets/css/app.css b/public/assets/css/app.css
index e556f68..c5afc62 100644
--- a/public/assets/css/app.css
+++ b/public/assets/css/app.css
@@ -5,12 +5,12 @@
Production pages should load this single stylesheet with a versioned URL.
========================================================================== */
-@import url("./tokens.css?v=0.2.1-dev");
-@import url("./base.css?v=0.2.1-dev");
-@import url("./layout.css?v=0.2.1-dev");
-@import url("./components.css?v=0.2.1-dev");
-@import url("./patterns.css?v=0.2.1-dev");
-@import url("./utilities.css?v=0.2.1-dev");
+@import url("./tokens.css?v=0.2.2-dev");
+@import url("./base.css?v=0.2.2-dev");
+@import url("./layout.css?v=0.2.2-dev");
+@import url("./components.css?v=0.2.2-dev");
+@import url("./patterns.css?v=0.2.2-dev");
+@import url("./utilities.css?v=0.2.2-dev");
/* Keep the document canvas consistent when pages use a full-height shell. */
body { background: var(--surface); color: var(--text); }
diff --git a/public/assets/css/design-system-demo.css b/public/assets/css/design-system-demo.css
deleted file mode 100644
index de3df7c..0000000
--- a/public/assets/css/design-system-demo.css
+++ /dev/null
@@ -1,25 +0,0 @@
-/* ==========================================================================
- TeamCode Hub Design System Showcase — Page-only styles
- File: public/assets/css/design-system-demo.css
- Purpose: Keep the reference page's layout details out of its HTML markup.
- ========================================================================== */
-
-.demo-principles { width: min(100%, 300px); }
-.demo-swatch { height: 78px; }
-.demo-swatch-primary { background: #0a0a0a; }
-.demo-swatch-muted { background: #f4f4f5; }
-.demo-swatch-success { background: #15803d; }
-.demo-swatch-danger { background: #b91c1c; }
-.demo-divider { margin-block: 4px; }
-.demo-table-wrap { border: 0; border-radius: 0; }
-.demo-foot-note { margin-inline-start: auto; }
-.demo-tab-panel { padding-block: var(--space-4); }
-.demo-tab-panel[hidden] { display: none; }
-
-@media (max-width: 720px) {
- .demo-foot-note { margin-inline-start: 0; }
-}
-
-/* Typography samples are classes rather than inline declarations, keeping markup clean. */
-.demo-type-display { font-size: clamp(2rem, 5vw, 2.5rem); letter-spacing: -.04em; }
-.demo-type-heading { font-size: 24px; }
diff --git a/public/assets/css/tokens.css b/public/assets/css/tokens.css
index eaca133..7ee0420 100644
--- a/public/assets/css/tokens.css
+++ b/public/assets/css/tokens.css
@@ -43,6 +43,13 @@
/* Keep secondary copy readable on white surfaces (WCAG AA contrast target). */
--text-muted: var(--g500);
+ /* Compatibility aliases used by product-level workspace components. */
+ --text-primary: var(--text);
+ --surface-hover: var(--g100);
+ --surface-subtle: var(--surface-muted);
+ --surface-code: var(--surface-raised);
+ --radius-md: var(--r-lg);
+
/* Corner radii. */
--r-xs: 4px;
--r-sm: 6px;
diff --git a/public/assets/css/workspace.css b/public/assets/css/workspace.css
index 876625c..9b81d75 100644
--- a/public/assets/css/workspace.css
+++ b/public/assets/css/workspace.css
@@ -20,6 +20,9 @@
.workspace-switcher { display:flex; align-items:center; gap:10px; width:100%; padding:11px 10px; margin-bottom:24px; border:1px solid var(--border); border-radius:var(--r-lg); background:var(--surface); text-align:left; }
.workspace-avatar { display:grid; place-items:center; width:35px; height:35px; flex:0 0 35px; border-radius:var(--r); background:var(--g100); color:var(--text); font-size:10px; font-weight:800; letter-spacing:-.04em; }
.workspace-switcher-copy,.profile-copy { display:flex; flex-direction:column; gap:1px; min-width:0; flex:1; }
+.workspace-switcher { text-align:left; cursor:pointer; transition:background var(--t),border-color var(--t); }
+.workspace-switcher:hover { border-color:var(--border-strong); background:var(--surface-muted); }
+.workspace-switcher:focus-visible { outline:2px solid var(--g700); outline-offset:3px; }
.workspace-switcher-copy strong,.profile-copy strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; font-weight:650; }
.workspace-switcher-copy small,.profile-copy small { color:var(--text-muted); font-size:11px; }
.switcher-caret { color:var(--text-muted); font-size:18px; }
@@ -36,12 +39,6 @@
.side-nav-item.is-active .nav-count { background:rgb(255 255 255 / 14%); color:#fff; }
.nav-count-alert { background:var(--danger-bg); color:var(--danger); }
.sidebar-bottom { margin-top:auto; padding-top:25px; }
-.sidebar-help { display:flex; align-items:center; gap:10px; padding:12px 10px; margin-bottom:15px; border:1px solid var(--border); border-radius:var(--r-lg); }
-.help-mark { display:grid; place-items:center; width:28px; height:28px; border:1px solid var(--border); border-radius:50%; color:var(--text-secondary); font-size:12px; font-weight:700; }
-.sidebar-help strong,.sidebar-help small { display:block; }
-.sidebar-help strong { font-size:11px; font-weight:650; }
-.sidebar-help small { color:var(--text-muted); font-size:10px; }
-.help-arrow { margin-inline-start:auto; color:var(--text-muted); font-size:14px; }
.sidebar-profile { position:relative; display:flex; align-items:center; gap:9px; padding:13px 4px 2px; border-top:1px solid var(--border); }
.sidebar-profile .avatar { width:34px; height:34px; border-radius:10px; font-size:11px; }
.profile-menu-button { width:29px; min-width:29px; height:29px; font-size:18px; }
diff --git a/public/assets/js/design-system-demo.js b/public/assets/js/design-system-demo.js
deleted file mode 100644
index 8782624..0000000
--- a/public/assets/js/design-system-demo.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/* ==========================================================================
- TeamCode Hub Design System Showcase — Demo-only behavior
- File: public/assets/js/design-system-demo.js
- Purpose: Wire the reference page's sample buttons without inline JavaScript.
- ========================================================================== */
-
-(function () {
- "use strict";
-
- /* Do not fail silently if the main interaction controller is unavailable. */
- function initializeShowcase() {
- const toastButton = document.getElementById("show-toast");
- const emptyAction = document.getElementById("empty-action");
- const year = document.getElementById("year");
-
- if (toastButton) {
- toastButton.addEventListener("click", function () {
- if (window.TCHUI && typeof window.TCHUI.notify === "function") {
- window.TCHUI.notify("This is a sample notification from the design system.");
- }
- });
- }
-
- if (emptyAction) {
- emptyAction.addEventListener("click", function () {
- if (window.TCHUI && typeof window.TCHUI.notify === "function") {
- window.TCHUI.notify("The projects page will be connected when its UI is implemented.");
- }
- });
- }
-
- /* Keep removable chip examples interactive without touching application data. */
- document.querySelectorAll(".chip .x").forEach(function (button) {
- button.addEventListener("click", function () {
- const chip = button.closest(".chip");
- if (chip) chip.remove();
- if (window.TCHUI && typeof window.TCHUI.notify === "function") {
- window.TCHUI.notify("The example chip was removed locally.");
- }
- });
- });
-
- /* Make the showcase pager update its selected page and boundary states. */
- document.querySelectorAll(".pager").forEach(function (pager) {
- const pageButtons = Array.from(pager.querySelectorAll("button"))
- .filter(function (button) {
- return !button.hasAttribute("aria-label") && /^\d+$/.test(button.textContent.trim());
- });
- const previous = pager.querySelector('[aria-label="Previous page"]');
- const next = pager.querySelector('[aria-label="Next page"]');
-
- function selectPage(index) {
- if (!pageButtons.length) return;
- const boundedIndex = Math.max(0, Math.min(pageButtons.length - 1, index));
- pageButtons.forEach(function (button, buttonIndex) {
- const selected = buttonIndex === boundedIndex;
- button.classList.toggle("active", selected);
- if (selected) button.setAttribute("aria-current", "page");
- else button.removeAttribute("aria-current");
- });
- if (previous) previous.disabled = boundedIndex === 0;
- if (next) next.disabled = boundedIndex === pageButtons.length - 1;
- }
-
- pageButtons.forEach(function (button, index) {
- button.addEventListener("click", function () { selectPage(index); });
- });
- if (previous) previous.addEventListener("click", function () {
- const activeIndex = pageButtons.findIndex(function (button) { return button.classList.contains("active"); });
- selectPage(activeIndex - 1);
- });
- if (next) next.addEventListener("click", function () {
- const activeIndex = pageButtons.findIndex(function (button) { return button.classList.contains("active"); });
- selectPage(activeIndex + 1);
- });
- });
-
- /* File selection is preview-only; no bytes leave the browser in this showcase. */
- const uploadInput = document.getElementById("demo-upload");
- if (uploadInput) {
- uploadInput.addEventListener("change", function () {
- const file = uploadInput.files && uploadInput.files[0];
- const dropzone = uploadInput.closest(".dropzone");
- const title = dropzone ? dropzone.querySelector("b") : null;
- if (title && file) title.textContent = "Selected locally: " + file.name;
- });
- }
-
- /* Keep the footer year accurate without embedding script in the HTML page. */
- if (year) year.textContent = String(new Date().getFullYear());
- }
-
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initializeShowcase, { once: true });
- } else {
- initializeShowcase();
- }
-})();
diff --git a/public/assets/js/workspace.js b/public/assets/js/workspace.js
index f39f7d0..de32f4b 100644
--- a/public/assets/js/workspace.js
+++ b/public/assets/js/workspace.js
@@ -689,7 +689,13 @@
function routeFromHash() {
const hash = location.hash.replace(/^#/, "");
- if (hash.startsWith("team/")) {\n const teamId = Number(hash.split("/")[1]);\n if (teamById(teamId)) openTeam(teamId);\n else navigate("teams", false);\n return;\n }\n if (hash.startsWith("project/")) {
+ if (hash.startsWith("team/")) {
+ const teamId = Number(hash.split("/")[1]);
+ if (teamById(teamId)) openTeam(teamId);
+ else navigate("teams", false);
+ return;
+ }
+ if (hash.startsWith("project/")) {
const id = Number(hash.split("/")[1]);
const project = projectById(id);
if (project) openProject(project, false);
diff --git a/public/design-system.html b/public/design-system.html
deleted file mode 100644
index 84ba265..0000000
--- a/public/design-system.html
+++ /dev/null
@@ -1,324 +0,0 @@
-
-
-
-
-
-
-
-
-
- Design System — TeamCode Hub
-
-
-
-
-
-
-
-
-
- Skip to content
-
-
-
-
A practical, accessible design system for TeamCode Hub. Neutral surfaces, clear hierarchy, predictable controls, and responsive layouts — without external font downloads or frontend dependencies.