Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: rename to Atmosphere Office, gate features, add site footer

- Rename all user-visible "Atmosphere Docs" to "Atmosphere Office"
across HTML titles, meta tags, PWA manifest, OAuth client metadata,
and instance info descriptions
- Hide Share, AI Chat, collab avatars, and sync status in editors
when instance-info.json declares those features as disabled
- Add Atmosphere Mail LLC footer with Terms, Privacy, AUP, About,
and Source links matching atmospheremail.com footer style

Chainlink: #16

+194 -41
+3
CHANGELOG.md
··· 9 9 ### Added 10 10 11 11 ### Fixed 12 + - Remove misleading Connecting status from editors (#14) 12 13 13 14 ### Changed 15 + - Add instance info panel with deployment flavor detection (#15) 16 + - Strip E2EE branding, update copy to reflect local-only reality (#13) 14 17 - Strip homelab: delete server/, electron/, Tailscale/Aperture refs (#2) 15 18 - Replace server /api/ version history calls with IndexedDB local-store operations (#3) 16 19 - Fix 17+ failing test files after tools→atmosphere-docs rename (#4)
+1 -1
public/client-metadata.json
··· 1 1 { 2 2 "client_id": "https://docs.lobster-hake.ts.net/client-metadata.json", 3 - "client_name": "Atmosphere Docs", 3 + "client_name": "Atmosphere Office", 4 4 "client_uri": "https://docs.lobster-hake.ts.net", 5 5 "redirect_uris": ["https://docs.lobster-hake.ts.net/callback"], 6 6 "scope": "atproto transition:generic",
+3 -3
public/manifest.json
··· 1 1 { 2 - "name": "Atmosphere Docs — Encrypted Office", 3 - "short_name": "Atmosphere Docs", 4 - "description": "End-to-end encrypted collaborative docs, sheets, diagrams, slides, and forms", 2 + "name": "Atmosphere Office", 3 + "short_name": "Atmosphere Office", 4 + "description": "Documents, spreadsheets, slides, and more — stored locally in your browser", 5 5 "start_url": "/", 6 6 "display": "standalone", 7 7 "background_color": "#111111",
+4 -4
src/calendar/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — calendar. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs — Calendar"> 9 - <meta property="og:description" content="Atmosphere Docs — calendar. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — calendar. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office — Calendar"> 9 + <meta property="og:description" content="Atmosphere Office — calendar. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs — Calendar</title> 12 + <title>Atmosphere Office — Calendar</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/calendar/main.ts
··· 7 7 * Views: month, week, day, agenda — all rendered into #calendar-grid. 8 8 */ 9 9 10 + import { applyFeatureGates } from '../lib/feature-gate.js'; 10 11 import * as Y from 'yjs'; 11 12 import { importKey } from '../lib/crypto.js'; 12 13 import { EncryptedProvider } from '../lib/provider.js'; 14 + 15 + applyFeatureGates(); 13 16 import { installDocGoneHandler } from '../lib/doc-gone-handler.js'; 14 17 import { wireKeyWarningForSession } from '../lib/key-warning.js'; 15 18 import { wireSaveStatus } from '../lib/save-status-ui.js';
+24
src/css/app.css
··· 1990 1990 margin-left: var(--space-sm); 1991 1991 } 1992 1992 1993 + .site-footer { 1994 + margin-top: var(--space-2xl); 1995 + padding-top: var(--space-lg); 1996 + border-top: 1px solid var(--color-border); 1997 + } 1998 + .site-footer-links { 1999 + font-size: 0.75rem; 2000 + color: var(--color-text-faint); 2001 + padding: var(--space-sm) 0; 2002 + line-height: 1.8; 2003 + } 2004 + .site-footer-links a { 2005 + color: var(--color-text-muted); 2006 + text-decoration: none; 2007 + } 2008 + .site-footer-links a:hover { 2009 + color: var(--color-text); 2010 + text-decoration: underline; 2011 + } 2012 + .footer-sep { 2013 + margin: 0 0.3em; 2014 + color: var(--color-text-faint); 2015 + } 2016 + 1993 2017 .desktop-download { 1994 2018 margin-top: var(--space-md); 1995 2019 text-align: center;
+4 -4
src/diagrams/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — diagrams and whiteboard. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs — Diagrams"> 9 - <meta property="og:description" content="Atmosphere Docs — diagrams and whiteboard. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — diagrams and whiteboard. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office — Diagrams"> 9 + <meta property="og:description" content="Atmosphere Office — diagrams and whiteboard. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs — Diagrams</title> 12 + <title>Atmosphere Office — Diagrams</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/diagrams/main.ts
··· 4 4 * Full tldraw-parity feature set. 5 5 */ 6 6 7 + import { applyFeatureGates } from '../lib/feature-gate.js'; 7 8 import * as Y from 'yjs'; 8 9 import { importKey } from '../lib/crypto.js'; 9 10 import { getDocument, updateDocument } from '../lib/local-store.js'; 10 11 import { EncryptedProvider } from '../lib/provider.js'; 12 + 13 + applyFeatureGates(); 11 14 import { installDocGoneHandler } from '../lib/doc-gone-handler.js'; 12 15 import { wireKeyWarningForSession } from '../lib/key-warning.js'; 13 16 import { wireSaveStatus } from '../lib/save-status-ui.js';
+4 -4
src/docs/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — document editor. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs"> 9 - <meta property="og:description" content="Atmosphere Docs — document editor. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — document editor. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office"> 9 + <meta property="og:description" content="Atmosphere Office — document editor. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs</title> 12 + <title>Atmosphere Office</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/docs/main.ts
··· 5 5 } 6 6 } 7 7 8 + import { applyFeatureGates } from '../lib/feature-gate.js'; 8 9 import * as Y from 'yjs'; 9 10 import { Editor } from '@tiptap/core'; 11 + 12 + applyFeatureGates(); 10 13 import StarterKit from '@tiptap/starter-kit'; 11 14 import { setupTooltips } from '../lib/tooltips.js'; 12 15 import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
+4 -4
src/forms/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — form builder. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs — Forms"> 9 - <meta property="og:description" content="Atmosphere Docs — form builder. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — form builder. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office — Forms"> 9 + <meta property="og:description" content="Atmosphere Office — form builder. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs — Forms</title> 12 + <title>Atmosphere Office — Forms</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/forms/main.ts
··· 5 5 * Responses pipeline writes answers to a linked spreadsheet. 6 6 */ 7 7 8 + import { applyFeatureGates } from '../lib/feature-gate.js'; 8 9 import * as Y from 'yjs'; 9 10 import { importKey } from '../lib/crypto.js'; 10 11 import { EncryptedProvider } from '../lib/provider.js'; 12 + 13 + applyFeatureGates(); 11 14 import { installDocGoneHandler } from '../lib/doc-gone-handler.js'; 12 15 import { wireKeyWarningForSession } from '../lib/key-warning.js'; 13 16 import { wireSaveStatus } from '../lib/save-status-ui.js';
+16 -9
src/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 7 <meta name="description" content="Documents, spreadsheets, slides, and more — stored locally in your browser. Sign in with your Bluesky account."> 8 - <meta property="og:title" content="Atmosphere Docs"> 8 + <meta property="og:title" content="Atmosphere Office"> 9 9 <meta property="og:description" content="Documents, spreadsheets, slides, and more — stored locally in your browser. Sign in with your Bluesky account."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs</title> 12 + <title>Atmosphere Office</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg"> ··· 130 130 <div class="trash-list" id="trash-list"></div> 131 131 </section> 132 132 133 - <footer style="margin-top: var(--space-2xl); padding-top: var(--space-lg); border-top: 1px solid var(--color-border);"> 133 + <footer class="site-footer"> 134 134 <div class="encryption-bar"> 135 135 <span class="encryption-dot"></span> 136 136 <span>All documents are stored locally in your browser.</span> 137 137 <button class="instance-info-btn" id="instance-info-btn" title="About this instance" aria-label="About this instance">?</button> 138 138 </div> 139 - <div class="desktop-download" id="desktop-download" style="display:none;"> 140 - <a class="desktop-download-btn" id="desktop-download-btn" href="#" target="_blank" rel="noopener"> 141 - Get Desktop App 142 - <span class="desktop-download-version" id="desktop-download-version"></span> 143 - </a> 139 + <div class="site-footer-links"> 140 + <span>Atmosphere Mail LLC</span> 141 + <span class="footer-sep">&middot;</span> 142 + <a href="https://atmospheremail.com/terms">Terms</a> 143 + <span class="footer-sep">&middot;</span> 144 + <a href="https://atmospheremail.com/privacy">Privacy</a> 145 + <span class="footer-sep">&middot;</span> 146 + <a href="https://atmospheremail.com/aup">Acceptable use</a> 147 + <span class="footer-sep">&middot;</span> 148 + <a href="https://atmospheremail.com/about">About</a> 149 + <span class="footer-sep">&middot;</span> 150 + <a href="https://tangled.org/scottlanoue.com/atmosphere-mail" target="_blank" rel="noopener">Source</a> 144 151 </div> 145 152 </footer> 146 153 </main> ··· 148 155 <!-- Sign-in modal --> 149 156 <div class="modal-backdrop" id="username-modal" style="display:none;"> 150 157 <div class="modal username-modal" role="dialog" aria-modal="true" aria-labelledby="username-modal-title"> 151 - <h2 id="username-modal-title">Atmosphere Docs</h2> 158 + <h2 id="username-modal-title">Atmosphere Office</h2> 152 159 <p class="welcome-tagline">A local-only office suite for the AT Protocol ecosystem.</p> 153 160 <ul class="welcome-features"> 154 161 <li>Documents, spreadsheets, slides, diagrams, forms, and calendar</li>
+23
src/lib/feature-gate.ts
··· 1 + import { getInstanceInfo } from './instance-info.js'; 2 + 3 + export async function applyFeatureGates(): Promise<void> { 4 + const info = await getInstanceInfo(); 5 + 6 + if (!info.features.sharing) { 7 + hide('btn-share'); 8 + } 9 + 10 + if (!info.features.ai) { 11 + hide('btn-ai-chat'); 12 + } 13 + 14 + if (!info.features.sync) { 15 + hide('collab-avatars'); 16 + hide('status'); 17 + } 18 + } 19 + 20 + function hide(id: string): void { 21 + const el = document.getElementById(id); 22 + if (el) el.style.display = 'none'; 23 + }
+2 -2
src/lib/instance-info.ts
··· 93 93 case 'self-hosted': 94 94 return { 95 95 title: 'Self-Hosted Instance', 96 - summary: 'You are running your own instance of Atmosphere Docs. You have full control over your data and configuration.', 96 + summary: 'You are running your own instance of Atmosphere Office. You have full control over your data and configuration.', 97 97 capabilities: [ 98 98 'Documents, spreadsheets, slides, diagrams, forms, and calendar', 99 99 'Data stored locally in your browser', ··· 109 109 110 110 default: 111 111 return { 112 - title: 'Atmosphere Docs', 112 + title: 'Atmosphere Office', 113 113 summary: 'A local-only office suite for the AT Protocol ecosystem. All your data stays in this browser — nothing is sent to a server.', 114 114 capabilities: [ 115 115 'Documents, spreadsheets, slides, diagrams, forms, and calendar',
+4 -4
src/sheets/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — spreadsheet editor. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs — Sheets"> 9 - <meta property="og:description" content="Atmosphere Docs — spreadsheet editor. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — spreadsheet editor. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office — Sheets"> 9 + <meta property="og:description" content="Atmosphere Office — spreadsheet editor. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs — Sheets</title> 12 + <title>Atmosphere Office — Sheets</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/sheets/main.ts
··· 1 1 // @ts-nocheck — deps objects passed to extracted modules need typed interfaces before this can be removed 2 + import { applyFeatureGates } from '../lib/feature-gate.js'; 2 3 import * as Y from 'yjs'; 4 + 5 + applyFeatureGates(); 3 6 import { setupTooltips } from '../lib/tooltips.js'; 4 7 import { mountOfflineIndicator } from '../lib/offline-indicator.js'; 5 8 import { cellId } from './formulas.js';
+4 -4
src/slides/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, viewport-fit=cover"> 6 6 <link rel="manifest" href="/manifest.json"> 7 - <meta name="description" content="Atmosphere Docs — slide presentations. All data stored locally in your browser."> 8 - <meta property="og:title" content="Atmosphere Docs — Slides"> 9 - <meta property="og:description" content="Atmosphere Docs — slide presentations. All data stored locally in your browser."> 7 + <meta name="description" content="Atmosphere Office — slide presentations. All data stored locally in your browser."> 8 + <meta property="og:title" content="Atmosphere Office — Slides"> 9 + <meta property="og:description" content="Atmosphere Office — slide presentations. All data stored locally in your browser."> 10 10 <meta property="og:type" content="website"> 11 11 <meta property="og:image" content="/favicon.svg"> 12 - <title>Atmosphere Docs — Slides</title> 12 + <title>Atmosphere Office — Slides</title> 13 13 <meta name="theme-color" content="#3a8a7a"> 14 14 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 15 15 <link rel="apple-touch-icon" href="/favicon.svg">
+3
src/slides/main.ts
··· 6 6 * Rendering, events, presenter UI, and AI chat live in dedicated modules. 7 7 */ 8 8 9 + import { applyFeatureGates } from '../lib/feature-gate.js'; 9 10 import * as Y from 'yjs'; 10 11 import { importKey } from '../lib/crypto.js'; 11 12 import { getDocument, updateDocument } from '../lib/local-store.js'; 12 13 import { EncryptedProvider } from '../lib/provider.js'; 14 + 15 + applyFeatureGates(); 13 16 import { installDocGoneHandler } from '../lib/doc-gone-handler.js'; 14 17 import { wireKeyWarningForSession } from '../lib/key-warning.js'; 15 18 import { wireSaveStatus } from '../lib/save-status-ui.js';
+78
tests/feature-gate.test.ts
··· 1 + /** 2 + * @vitest-environment jsdom 3 + */ 4 + import { describe, it, expect, beforeEach, vi } from 'vitest'; 5 + 6 + vi.mock('../src/lib/instance-info.js', () => ({ 7 + getInstanceInfo: vi.fn(), 8 + })); 9 + 10 + import { applyFeatureGates } from '../src/lib/feature-gate.js'; 11 + import { getInstanceInfo } from '../src/lib/instance-info.js'; 12 + 13 + const mockGetInstanceInfo = vi.mocked(getInstanceInfo); 14 + 15 + function makeInfo(features: { sync?: boolean; sharing?: boolean; ai?: boolean } = {}) { 16 + return { 17 + flavor: 'public' as const, 18 + operator: null, 19 + pds: null, 20 + features: { 21 + sync: features.sync ?? false, 22 + sharing: features.sharing ?? false, 23 + ai: features.ai ?? false, 24 + }, 25 + notice: null, 26 + }; 27 + } 28 + 29 + describe('applyFeatureGates', () => { 30 + beforeEach(() => { 31 + document.body.innerHTML = ` 32 + <button id="btn-share">Share</button> 33 + <button id="btn-ai-chat">AI</button> 34 + <div id="collab-avatars"></div> 35 + <div id="status"><span>Local</span></div> 36 + `; 37 + }); 38 + 39 + it('hides share, AI, collab, and status when all features disabled', async () => { 40 + mockGetInstanceInfo.mockResolvedValue(makeInfo()); 41 + await applyFeatureGates(); 42 + 43 + expect(document.getElementById('btn-share')!.style.display).toBe('none'); 44 + expect(document.getElementById('btn-ai-chat')!.style.display).toBe('none'); 45 + expect(document.getElementById('collab-avatars')!.style.display).toBe('none'); 46 + expect(document.getElementById('status')!.style.display).toBe('none'); 47 + }); 48 + 49 + it('keeps share visible when sharing is enabled', async () => { 50 + mockGetInstanceInfo.mockResolvedValue(makeInfo({ sharing: true })); 51 + await applyFeatureGates(); 52 + 53 + expect(document.getElementById('btn-share')!.style.display).not.toBe('none'); 54 + expect(document.getElementById('btn-ai-chat')!.style.display).toBe('none'); 55 + }); 56 + 57 + it('keeps AI visible when ai is enabled', async () => { 58 + mockGetInstanceInfo.mockResolvedValue(makeInfo({ ai: true })); 59 + await applyFeatureGates(); 60 + 61 + expect(document.getElementById('btn-ai-chat')!.style.display).not.toBe('none'); 62 + expect(document.getElementById('btn-share')!.style.display).toBe('none'); 63 + }); 64 + 65 + it('keeps collab and status visible when sync is enabled', async () => { 66 + mockGetInstanceInfo.mockResolvedValue(makeInfo({ sync: true })); 67 + await applyFeatureGates(); 68 + 69 + expect(document.getElementById('collab-avatars')!.style.display).not.toBe('none'); 70 + expect(document.getElementById('status')!.style.display).not.toBe('none'); 71 + }); 72 + 73 + it('is safe when elements do not exist', async () => { 74 + document.body.innerHTML = ''; 75 + mockGetInstanceInfo.mockResolvedValue(makeInfo()); 76 + await expect(applyFeatureGates()).resolves.not.toThrow(); 77 + }); 78 + });
+2 -2
tests/instance-info.test.ts
··· 18 18 describe('describeInstance', () => { 19 19 it('returns local-only description for public flavor', () => { 20 20 const desc = describeInstance(makeInfo()); 21 - expect(desc.title).toBe('Atmosphere Docs'); 21 + expect(desc.title).toBe('Atmosphere Office'); 22 22 expect(desc.summary).toContain('local-only'); 23 23 expect(desc.capabilities.length).toBeGreaterThan(0); 24 24 expect(desc.limitations).toContain('Single device only — documents do not sync across devices'); ··· 78 78 79 79 it('falls back to public description for unknown flavor', () => { 80 80 const desc = describeInstance(makeInfo({ flavor: 'unknown' as any })); 81 - expect(desc.title).toBe('Atmosphere Docs'); 81 + expect(desc.title).toBe('Atmosphere Office'); 82 82 }); 83 83 84 84 it('falls back to generic title when pds-operator has no operator name', () => {