Add production Traefik labels for jeffemmett.com

- Add router rules for jeffemmett.com and www.jeffemmett.com
- Keep staging.jeffemmett.com for testing
- Preparing for migration from Cloudflare Pages to Docker deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-03 22:34:34 -08:00
parent 49cf763858
commit 343f408661
7 changed files with 992 additions and 216 deletions

View File

@ -1,22 +0,0 @@
---
id: task-012
title: Dark Mode Theme
status: To Do
assignee: []
created_date: '2025-12-03'
labels: [feature, ui, theme]
priority: low
branch: dark-mode
---
## Description
Implement dark mode theme support for the canvas interface.
## Branch Info
- **Branch**: `dark-mode`
## Acceptance Criteria
- [ ] Create dark theme colors
- [ ] Add theme toggle
- [ ] Persist user preference
- [ ] System theme detection

View File

@ -1,6 +1,6 @@
# Canvas Website Docker Compose
# Staging deployment at staging.jeffemmett.com
# Production deployment at jeffemmett.com (once tested)
# Production: jeffemmett.com, www.jeffemmett.com
# Staging: staging.jeffemmett.com
services:
canvas-website:
@ -13,8 +13,12 @@ services:
container_name: canvas-website
restart: unless-stopped
labels:
# Staging deployment
- "traefik.enable=true"
# Production deployment (jeffemmett.com and www)
- "traefik.http.routers.canvas-prod.rule=Host(`jeffemmett.com`) || Host(`www.jeffemmett.com`)"
- "traefik.http.routers.canvas-prod.entrypoints=web"
- "traefik.http.services.canvas-prod.loadbalancer.server.port=80"
# Staging deployment (keep for testing)
- "traefik.http.routers.canvas-staging.rule=Host(`staging.jeffemmett.com`)"
- "traefik.http.routers.canvas-staging.entrypoints=web"
- "traefik.http.services.canvas-staging.loadbalancer.server.port=80"

View File

@ -1,4 +1,29 @@
import React, { useState, ReactNode, useEffect, useRef } from 'react'
import React, { useState, ReactNode, useEffect, useRef, useMemo } from 'react'
// Hook to detect dark mode
function useIsDarkMode() {
const [isDark, setIsDark] = useState(() => {
if (typeof document !== 'undefined') {
return document.documentElement.classList.contains('dark')
}
return false
})
useEffect(() => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
setIsDark(document.documentElement.classList.contains('dark'))
}
})
})
observer.observe(document.documentElement, { attributes: true })
return () => observer.disconnect()
}, [])
return isDark
}
export interface StandardizedToolWrapperProps {
/** The title to display in the header */
@ -64,6 +89,28 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const [isEditingTags, setIsEditingTags] = useState(false)
const [editingTagInput, setEditingTagInput] = useState('')
const tagInputRef = useRef<HTMLInputElement>(null)
const isDarkMode = useIsDarkMode()
// Dark mode aware colors
const colors = useMemo(() => isDarkMode ? {
contentBg: '#1a1a1a',
tagsBg: '#252525',
tagsBorder: '#404040',
tagBg: '#4a5568',
tagText: '#e4e4e4',
addTagBg: '#4a5568',
inputBg: '#333333',
inputBorder: '#555555',
} : {
contentBg: 'white',
tagsBg: '#f8f9fa',
tagsBorder: '#e0e0e0',
tagBg: '#6b7280',
tagText: 'white',
addTagBg: '#9ca3af',
inputBg: 'white',
inputBorder: '#9ca3af',
}, [isDarkMode])
// Bring selected shape to front when it becomes selected
useEffect(() => {
@ -107,13 +154,13 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const wrapperStyle: React.CSSProperties = {
width: typeof width === 'number' ? `${width}px` : width,
height: isMinimized ? 40 : (typeof height === 'number' ? `${height}px` : height), // Minimized height is just the header
backgroundColor: "white",
backgroundColor: colors.contentBg,
border: isSelected ? `2px solid ${primaryColor}` : `1px solid ${primaryColor}40`,
borderRadius: "8px",
overflow: "hidden",
boxShadow: isSelected
? `0 0 0 2px ${primaryColor}40, 0 4px 8px rgba(0,0,0,0.15)`
: '0 2px 4px rgba(0,0,0,0.1)',
? `0 0 0 2px ${primaryColor}40, 0 4px 8px rgba(0,0,0,${isDarkMode ? '0.4' : '0.15'})`
: `0 2px 4px rgba(0,0,0,${isDarkMode ? '0.3' : '0.1'})`,
display: 'flex',
flexDirection: 'column',
fontFamily: "Inter, sans-serif",
@ -210,20 +257,20 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const tagsContainerStyle: React.CSSProperties = {
padding: '8px 12px',
borderTop: '1px solid #e0e0e0',
borderTop: `1px solid ${colors.tagsBorder}`,
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
alignItems: 'center',
minHeight: '32px',
backgroundColor: '#f8f9fa',
backgroundColor: colors.tagsBg,
flexShrink: 0,
touchAction: 'manipulation', // Improve touch responsiveness
}
const tagStyle: React.CSSProperties = {
backgroundColor: '#6b7280',
color: 'white',
backgroundColor: colors.tagBg,
color: colors.tagText,
padding: '4px 8px', // Increased padding for better touch target
borderRadius: '12px',
fontSize: '10px',
@ -237,18 +284,20 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
}
const tagInputStyle: React.CSSProperties = {
border: '1px solid #9ca3af',
border: `1px solid ${colors.inputBorder}`,
borderRadius: '12px',
padding: '2px 6px',
fontSize: '10px',
outline: 'none',
minWidth: '60px',
flex: 1,
backgroundColor: colors.inputBg,
color: isDarkMode ? '#e4e4e4' : '#333',
}
const addTagButtonStyle: React.CSSProperties = {
backgroundColor: '#9ca3af',
color: 'white',
backgroundColor: colors.addTagBg,
color: colors.tagText,
border: 'none',
borderRadius: '12px',
padding: '4px 10px', // Increased padding for better touch target

View File

@ -20,9 +20,12 @@ html.dark {
--code-bg: #2d2d2d;
--code-color: #e4e4e4;
--hover-bg: #2d2d2d;
--tool-bg: #3a3a3a;
--tool-bg: #2a2a2a;
--tool-text: #e0e0e0;
--tool-border: #555555;
--card-bg: #252525;
--input-bg: #333333;
--muted-text: #a1a1aa;
}
html,
@ -1519,3 +1522,432 @@ html.dark .people-dropdown {
margin: 16px;
}
}
/* ========================================
Dark Mode Comprehensive Styles
======================================== */
/* Dark mode for blockquotes */
html.dark blockquote {
background-color: #2d2d2d;
border-left-color: #555;
color: #e0e0e0;
}
/* Dark mode for tables */
html.dark table th,
html.dark table td {
border-color: #404040;
}
html.dark table th {
background-color: #2d2d2d;
}
html.dark table tr:nth-child(even) {
background-color: #252525;
}
/* Dark mode for navigation links */
html.dark .nav-link {
color: #60a5fa;
}
html.dark .nav-link:hover {
background-color: #2d2d2d;
border-color: #404040;
}
/* Dark mode for list markers */
html.dark ol li::marker,
html.dark ul li::marker {
color: rgba(255, 255, 255, 0.4);
}
/* Dark mode for loading indicator */
html.dark .loading {
background-color: #2d2d2d;
border-color: #404040;
color: #e4e4e4;
}
/* Dark mode for presentations */
html.dark .presentation-card {
border-color: #404040;
background-color: #252525;
}
html.dark .presentation-card:hover {
border-color: #60a5fa;
}
html.dark .presentation-card h3 {
color: #e4e4e4;
}
html.dark .presentation-card p {
color: #a1a1aa;
}
html.dark .presentation-meta {
border-top-color: #404040;
background-color: #252525;
}
html.dark .presentation-meta span {
color: #a1a1aa;
}
html.dark .presentation-meta a {
color: #60a5fa;
}
html.dark .presentations-info {
background-color: #252525;
border-left-color: #60a5fa;
}
html.dark .presentations-info h3 {
color: #e4e4e4;
}
html.dark .presentations-info p {
color: #a1a1aa;
}
html.dark .presentation-info {
background-color: #252525;
border-left-color: #60a5fa;
}
html.dark .presentation-info h1 {
color: #e4e4e4;
}
html.dark .video-clips h2,
html.dark .video-section h3 {
color: #e4e4e4;
}
html.dark .presentation-embed h2 {
color: #e4e4e4;
}
/* Dark mode for command palette */
html.dark [cmdk-dialog] {
background-color: #1a1a1a;
border-color: #404040;
}
html.dark [cmdk-dialog] input {
background-color: #252525;
color: #e4e4e4;
}
html.dark [cmdk-dialog] input:focus {
background-color: #2d2d2d;
}
html.dark [cmdk-item]:hover {
background-color: #2d2d2d;
}
html.dark [cmdk-item] .tlui-kbd {
border-color: #404040;
}
/* Dark mode for lock indicator */
html.dark .lock-indicator {
background: #2d2d2d;
}
html.dark .lock-indicator:hover {
background: #3d3d3d;
}
/* Dark mode for overflowing container */
html.dark .overflowing {
background-color: #1a1a1a;
}
/* Dark mode for tldraw html layer markdown */
html.dark .tl-html-layer code {
background-color: #2d2d2d;
color: #e4e4e4;
}
html.dark .tl-html-layer pre {
background-color: #1e1e2e;
color: #cdd6f4;
}
html.dark .tl-html-layer blockquote {
border-left-color: #555;
color: #a1a1aa;
}
html.dark .tl-html-layer th,
html.dark .tl-html-layer td {
border-color: #404040;
}
html.dark .tl-html-layer tr:nth-child(2n) {
background-color: #252525;
}
/* Dark mode for Mycelial Intelligence inline code */
html.dark .mi-inline-code {
background: rgba(255, 255, 255, 0.1) !important;
color: #e4e4e4 !important;
}
/* Dark mode for MDXEditor (Markdown tool) */
html.dark .mdxeditor {
background-color: #1a1a1a !important;
}
html.dark .mdxeditor [role="toolbar"] {
background: #252525 !important;
border-bottom-color: #404040 !important;
}
html.dark .mdxeditor [role="toolbar"] button {
color: #e4e4e4 !important;
}
html.dark .mdxeditor [role="toolbar"] button:hover {
background: #3d3d3d !important;
}
html.dark .mdxeditor [role="toolbar"] button[data-state="on"] {
background: rgba(20, 184, 166, 0.2) !important;
color: #14b8a6 !important;
}
html.dark .mdxeditor .mdxeditor-root-contenteditable {
background: #1a1a1a !important;
}
html.dark .mdx-editor-content {
color: #e4e4e4 !important;
}
html.dark .mdx-editor-content h1 {
color: #f4f4f5 !important;
}
html.dark .mdx-editor-content h2 {
color: #e4e4e5 !important;
}
html.dark .mdx-editor-content h3 {
color: #d4d4d5 !important;
}
html.dark .mdx-editor-content blockquote {
background: #252525 !important;
border-left-color: #14b8a6 !important;
}
html.dark .mdx-editor-content code {
background: #2d2d2d !important;
color: #e4e4e4 !important;
}
html.dark .mdx-editor-content th {
background: #252525 !important;
}
html.dark .mdx-editor-content th,
html.dark .mdx-editor-content td {
border-color: #404040 !important;
}
html.dark .mdx-editor-content hr {
border-top-color: #404040 !important;
}
html.dark .mdx-editor-content a {
color: #2dd4bf !important;
}
html.dark .mdxeditor [role="toolbar"] select {
background: #252525 !important;
border-color: #404040 !important;
color: #e4e4e4 !important;
}
/* Dark mode for StandardizedToolWrapper */
html.dark .tool-wrapper-content {
background-color: #1a1a1a !important;
}
/* Dark mode for UserSettingsModal inline-styled elements */
/* Using attribute selectors to target inline-styled divs */
html.dark .settings-modal [style*="backgroundColor: #f9fafb"],
html.dark .settings-modal [style*="background-color: #f9fafb"] {
background-color: #252525 !important;
border-color: #404040 !important;
}
html.dark .settings-modal [style*="backgroundColor: #fef3c7"],
html.dark .settings-modal [style*="background-color: #fef3c7"] {
background-color: #3d3620 !important;
border-color: #665930 !important;
}
html.dark .settings-modal [style*="color: #374151"] {
color: #e4e4e4 !important;
}
html.dark .settings-modal [style*="color: #1f2937"] {
color: #f4f4f5 !important;
}
html.dark .settings-modal [style*="color: #6b7280"] {
color: #a1a1aa !important;
}
html.dark .settings-modal [style*="color: #92400e"] {
color: #fbbf24 !important;
}
html.dark .settings-modal [style*="borderTop: 1px solid #e5e7eb"],
html.dark .settings-modal [style*="border-top: 1px solid #e5e7eb"] {
border-top-color: #404040 !important;
}
html.dark .settings-modal [style*="backgroundColor: #f8fafc"],
html.dark .settings-modal [style*="background-color: #f8fafc"] {
background-color: #252525 !important;
border-color: #404040 !important;
}
/* Dark mode for settings modal cards */
html.dark .settings-section [style*="background-color"] {
background-color: #252525 !important;
}
/* Dark mode for AI tool cards in settings */
html.dark .settings-section h3 {
color: #e4e4e4 !important;
}
/* Dark mode for chat messages in PromptShape */
html.dark .prompt-container [style*="backgroundColor: white"],
html.dark .prompt-container [style*="background-color: white"] {
background-color: #1a1a1a !important;
}
html.dark .prompt-container [style*="backgroundColor: #efefef"],
html.dark .prompt-container [style*="background-color: #efefef"] {
background-color: #252525 !important;
}
html.dark .prompt-container [style*="backgroundColor: #f0f0f0"],
html.dark .prompt-container [style*="background-color: #f0f0f0"] {
background-color: #3d3d3d !important;
color: #e4e4e4 !important;
}
/* Dark mode chat bubbles */
html.dark [style*="backgroundColor: #f0f0f0"][style*="borderRadius: 18px"] {
background-color: #3d3d3d !important;
color: #e4e4e4 !important;
}
/* Dark mode for ObsNote and other shapes */
html.dark .obs-note-container,
html.dark .transcription-container,
html.dark .holon-container {
background-color: #1a1a1a !important;
}
/* Dark mode for FathomMeetingsBrowser and ObsidianBrowser */
html.dark .fathom-meetings-browser-container,
html.dark .obsidian-browser-container,
html.dark .holon-browser-container {
background-color: #1a1a1a !important;
}
/* Dark mode for chat container */
html.dark .chat-container {
background-color: #1a1a1a !important;
}
html.dark .chat-container .messages-container {
background-color: #1a1a1a !important;
}
html.dark .chat-container .message {
background-color: #252525 !important;
border-color: #404040 !important;
color: #e4e4e4 !important;
}
html.dark .chat-container .message.own-message {
background-color: #1e3a5f !important;
}
html.dark .chat-container .message-input {
background-color: #252525 !important;
border-color: #404040 !important;
color: #e4e4e4 !important;
}
html.dark .chat-container .send-button {
background-color: #3b82f6 !important;
}
/* Dark mode for ImageGen and VideoGen shapes */
html.dark .image-gen-container,
html.dark .video-gen-container {
background-color: #1a1a1a !important;
}
/* Dark mode for all input fields in tools */
html.dark input[type="text"],
html.dark input[type="email"],
html.dark input[type="password"],
html.dark textarea,
html.dark select {
background-color: var(--input-bg) !important;
border-color: var(--tool-border) !important;
color: var(--text-color) !important;
}
html.dark input::placeholder,
html.dark textarea::placeholder {
color: var(--muted-text) !important;
}
/* Dark mode for error messages */
html.dark [style*="backgroundColor: #fee"],
html.dark [style*="background-color: #fee"] {
background-color: #3d2020 !important;
border-color: #5c3030 !important;
color: #f87171 !important;
}
/* Dark mode for success messages */
html.dark [style*="backgroundColor: #d1fae5"],
html.dark [style*="background-color: #d1fae5"] {
background-color: #1a3d2e !important;
color: #34d399 !important;
}
/* Dark mode for links in general */
html.dark a:not([class]) {
color: #60a5fa;
}
/* Ensure proper contrast for buttons in dark mode */
html.dark button:not([class*="primary"]):not([style*="background"]) {
background-color: var(--tool-bg);
color: var(--tool-text);
border-color: var(--tool-border);
}
html.dark button:not([class*="primary"]):not([style*="background"]):hover {
background-color: var(--hover-bg);
}

View File

@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
import {
MDXEditor,
headingsPlugin,
@ -59,8 +59,17 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
component(shape: IMarkdownShape) {
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
const [isMinimized, setIsMinimized] = useState(false)
const [isToolbarMinimized, setIsToolbarMinimized] = useState(false)
const editorRef = useRef<MDXEditorMethods>(null)
// Dark mode detection
const isDarkMode = useMemo(() => {
if (typeof document !== 'undefined') {
return document.documentElement.classList.contains('dark')
}
return false
}, [])
// Use the pinning hook
usePinnedToView(this.editor, shape.id, shape.props.pinnedToView)
@ -136,7 +145,7 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
style={{
width: '100%',
height: '100%',
backgroundColor: '#FFFFFF',
backgroundColor: isDarkMode ? '#1a1a1a' : '#FFFFFF',
pointerEvents: 'all',
overflow: 'hidden',
display: 'flex',
@ -210,22 +219,43 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
// Toolbar
toolbarPlugin({
toolbarContents: () => (
<>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<Separator />
<BlockTypeSelect />
<Separator />
<ListsToggle />
<Separator />
<CreateLink />
<InsertTable />
<Separator />
<DiffSourceToggleWrapper>
<></>
</DiffSourceToggleWrapper>
</>
<div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: '4px' }}>
<button
onClick={() => setIsToolbarMinimized(!isToolbarMinimized)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '4px 6px',
fontSize: '12px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
title={isToolbarMinimized ? 'Expand toolbar' : 'Collapse toolbar'}
>
{isToolbarMinimized ? '▶' : '▼'}
</button>
{!isToolbarMinimized && (
<>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<Separator />
<BlockTypeSelect />
<Separator />
<ListsToggle />
<Separator />
<CreateLink />
<InsertTable />
<Separator />
<DiffSourceToggleWrapper>
<></>
</DiffSourceToggleWrapper>
</>
)}
</div>
)
}),
]}
@ -247,7 +277,10 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
background: #f9fafb;
padding: 4px 8px;
gap: 2px;
flex-wrap: wrap;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
min-height: ${isToolbarMinimized ? '32px' : 'auto'};
}
.mdxeditor [role="toolbar"] button {
@ -268,10 +301,36 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
.mdxeditor .mdxeditor-root-contenteditable {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px;
min-height: 0;
}
/* Custom scrollbar styling - vertical only, auto-hide */
.mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar {
width: 8px;
height: 0; /* No horizontal scrollbar */
}
.mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-track {
background: transparent;
}
.mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* Firefox scrollbar */
.mdxeditor .mdxeditor-root-contenteditable {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
.mdx-editor-content {
min-height: 100%;
height: 100%;

View File

@ -46,8 +46,8 @@ function renderMessageContent(content: string): React.ReactNode {
return (
<code
key={j}
className="mi-inline-code"
style={{
background: 'rgba(0, 0, 0, 0.06)',
padding: '1px 4px',
borderRadius: '3px',
fontSize: '0.9em',
@ -828,6 +828,34 @@ export function MycelialIntelligenceBar() {
const [followUpSuggestions, setFollowUpSuggestions] = useState<FollowUpSuggestion[]>([])
const [lastTransform, setLastTransform] = useState<TransformCommand | null>(null)
const [toolInputMode, setToolInputMode] = useState<{ toolType: string; shapeId: string } | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
// Detect when modals/dialogs are open to fade the bar
useEffect(() => {
const checkForModals = () => {
// Check for common modal/dialog overlays
const hasSettingsModal = document.querySelector('.settings-modal-overlay') !== null
const hasTldrawDialog = document.querySelector('[data-state="open"][role="dialog"]') !== null
const hasAuthModal = document.querySelector('.auth-modal-overlay') !== null
const hasPopup = document.querySelector('.profile-popup') !== null
setIsModalOpen(hasSettingsModal || hasTldrawDialog || hasAuthModal || hasPopup)
}
// Initial check
checkForModals()
// Use MutationObserver to detect DOM changes
const observer = new MutationObserver(checkForModals)
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style', 'data-state']
})
return () => observer.disconnect()
}, [])
// Derived state: get selected tool info
const selectedToolInfo = getSelectedToolInfo(selectionInfo)
@ -982,7 +1010,21 @@ export function MycelialIntelligenceBar() {
}, [conversationHistory])
// Theme-aware colors
const colors = {
const colors = isDark ? {
background: 'rgba(30, 30, 30, 0.98)',
backgroundHover: 'rgba(40, 40, 40, 1)',
border: 'rgba(70, 70, 70, 0.8)',
borderHover: 'rgba(90, 90, 90, 1)',
text: '#e4e4e4',
textMuted: '#a1a1aa',
inputBg: 'rgba(50, 50, 50, 0.8)',
inputBorder: 'rgba(70, 70, 70, 1)',
inputText: '#e4e4e4',
shadow: '0 8px 32px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3)',
shadowHover: '0 12px 40px rgba(0, 0, 0, 0.5), 0 6px 20px rgba(0, 0, 0, 0.4)',
userBubble: 'rgba(16, 185, 129, 0.2)',
assistantBubble: 'rgba(50, 50, 50, 0.9)',
} : {
background: 'rgba(255, 255, 255, 0.98)',
backgroundHover: 'rgba(255, 255, 255, 1)',
border: 'rgba(229, 231, 235, 0.8)',
@ -1300,9 +1342,17 @@ export function MycelialIntelligenceBar() {
// Height: taller when showing suggestion chips (single tool or 2+ selected)
const showSuggestions = selectedToolInfo || (selectionInfo && selectionInfo.count > 1)
const collapsedHeight = showSuggestions ? 76 : 48
const expandedHeight = 400
const maxExpandedHeight = 400
const barWidth = 520 // Consistent width
const height = isExpanded ? expandedHeight : collapsedHeight
// Calculate dynamic height when expanded based on content
// Header: ~45px, Input area: ~56px, padding: ~24px = ~125px fixed
// Each message is roughly 50-80px, we'll let CSS handle the actual sizing
const hasContent = conversationHistory.length > 0 || streamingResponse
// Minimum expanded height when there's no content (just empty state)
const minExpandedHeight = 180
// Use auto height with max constraint when expanded
const height = isExpanded ? 'auto' : collapsedHeight
return (
<div
@ -1314,9 +1364,13 @@ export function MycelialIntelligenceBar() {
left: '50%',
transform: 'translateX(-50%)',
width: barWidth,
height,
zIndex: 99999,
pointerEvents: 'auto',
height: isExpanded ? 'auto' : collapsedHeight,
minHeight: isExpanded ? minExpandedHeight : collapsedHeight,
maxHeight: isExpanded ? maxExpandedHeight : collapsedHeight,
zIndex: isModalOpen ? 1 : 99999, // Lower z-index when modals are open
pointerEvents: isModalOpen ? 'none' : 'auto', // Disable interactions when modal is open
opacity: isModalOpen ? 0.3 : 1, // Fade when modal is open
transition: 'opacity 0.2s ease, z-index 0s',
}}
onPointerEnter={() => setIsHovering(true)}
onPointerLeave={() => setIsHovering(false)}
@ -1325,6 +1379,8 @@ export function MycelialIntelligenceBar() {
style={{
width: '100%',
height: '100%',
minHeight: isExpanded ? minExpandedHeight : collapsedHeight,
maxHeight: isExpanded ? maxExpandedHeight : collapsedHeight,
background: isHovering ? colors.backgroundHover : colors.background,
borderRadius: isExpanded ? '20px' : '24px',
border: `1px solid ${isHovering ? colors.borderHover : colors.border}`,
@ -1600,6 +1656,7 @@ export function MycelialIntelligenceBar() {
justifyContent: 'space-between',
padding: '10px 14px',
borderBottom: `1px solid ${colors.border}`,
flexShrink: 0,
}}>
<div style={{
display: 'flex',
@ -1652,7 +1709,8 @@ export function MycelialIntelligenceBar() {
<div
ref={chatContainerRef}
style={{
flex: 1,
flex: '1 1 auto',
minHeight: 0, // Allow flex shrinking below content size
overflowY: 'auto',
padding: '12px',
display: 'flex',
@ -1756,41 +1814,6 @@ export function MycelialIntelligenceBar() {
</div>
)}
{/* Follow-up suggestions for assistant messages */}
{msg.role === 'assistant' && msg.followUpSuggestions && msg.followUpSuggestions.length > 0 && (
<div
style={{
alignSelf: 'flex-start',
maxWidth: '100%',
padding: '8px',
marginTop: '6px',
}}
>
<div style={{
fontSize: '10px',
fontWeight: 500,
color: colors.textMuted,
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
Next Steps
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
}}>
{msg.followUpSuggestions.map((suggestion, i) => (
<FollowUpChip
key={`${suggestion.label}-${i}`}
suggestion={suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
/>
))}
</div>
</div>
)}
</React.Fragment>
))}
@ -1824,74 +1847,6 @@ export function MycelialIntelligenceBar() {
)}
</div>
{/* Show suggested tools while streaming if available */}
{suggestedTools.length > 0 && (
<div
style={{
alignSelf: 'flex-start',
maxWidth: '100%',
padding: '10px',
marginTop: '6px',
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%)',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.15)',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '8px',
}}>
<span style={{
fontSize: '11px',
fontWeight: 600,
color: ACCENT_COLOR,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
Suggested Tools
</span>
{suggestedTools.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation()
handleSpawnAllTools()
}}
onPointerDown={(e) => e.stopPropagation()}
style={{
fontSize: '11px',
padding: '4px 10px',
borderRadius: '8px',
border: `1px solid ${ACCENT_COLOR}`,
background: 'transparent',
color: ACCENT_COLOR,
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s ease',
}}
title="Spawn all suggested tools on canvas"
>
Spawn All
</button>
)}
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
}}>
{suggestedTools.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
onSpawn={handleSpawnTool}
isSpawned={spawnedToolIds.has(tool.id)}
/>
))}
</div>
</div>
)}
</>
)}
@ -1909,8 +1864,8 @@ export function MycelialIntelligenceBar() {
</div>
)}
{/* Current follow-up suggestions - shown at bottom when not loading */}
{!isLoading && followUpSuggestions.length > 0 && (
{/* Combined "Try next" section - tools + follow-up suggestions in one scrollable row */}
{!isLoading && (followUpSuggestions.length > 0 || suggestedTools.length > 0) && (
<div
style={{
alignSelf: 'flex-start',
@ -1923,25 +1878,68 @@ export function MycelialIntelligenceBar() {
}}
>
<div style={{
fontSize: '10px',
fontWeight: 600,
color: '#6366f1',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'flex',
alignItems: 'center',
gap: '4px',
justifyContent: 'space-between',
marginBottom: '8px',
}}>
<span></span>
Try next
<div style={{
fontSize: '10px',
fontWeight: 600,
color: '#6366f1',
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}>
<span></span>
Try next
</div>
{suggestedTools.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation()
handleSpawnAllTools()
}}
onPointerDown={(e) => e.stopPropagation()}
style={{
fontSize: '10px',
padding: '3px 8px',
borderRadius: '6px',
border: `1px solid ${ACCENT_COLOR}`,
background: 'transparent',
color: ACCENT_COLOR,
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s ease',
}}
title="Spawn all suggested tools on canvas"
>
Spawn All
</button>
)}
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
overflowX: 'auto',
overflowY: 'hidden',
paddingBottom: '4px',
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(99, 102, 241, 0.3) transparent',
}}>
{followUpSuggestions.slice(0, 4).map((suggestion, i) => (
{/* Suggested tools first */}
{suggestedTools.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
onSpawn={handleSpawnTool}
isSpawned={spawnedToolIds.has(tool.id)}
/>
))}
{/* Then follow-up prompts */}
{followUpSuggestions.map((suggestion, i) => (
<FollowUpChip
key={`current-${suggestion.label}-${i}`}
suggestion={suggestion}
@ -1960,6 +1958,7 @@ export function MycelialIntelligenceBar() {
gap: '8px',
padding: '10px 12px',
borderTop: `1px solid ${colors.border}`,
flexShrink: 0,
}}>
<input
ref={inputRef}

View File

@ -3,6 +3,7 @@ import { useAuth } from "../context/AuthContext"
import { useDialogs } from "tldraw"
import { SettingsDialog } from "./SettingsDialog"
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
import { linkEmailToAccount, checkEmailStatus, type LookupResult } from "../lib/auth/cryptidEmailService"
// AI tool model configurations
const AI_TOOLS = [
@ -83,6 +84,66 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
const [fathomApiKeyInput, setFathomApiKeyInput] = useState('')
const [activeTab, setActiveTab] = useState<'general' | 'ai' | 'integrations'>('general')
// Dark mode aware colors
const colors = isDarkMode ? {
cardBg: '#252525',
cardBorder: '#404040',
text: '#e4e4e4',
textMuted: '#a1a1aa',
textHeading: '#f4f4f5',
warningBg: '#3d3620',
warningBorder: '#665930',
warningText: '#fbbf24',
successBg: '#1a3d2e',
successText: '#34d399',
errorBg: '#3d2020',
errorText: '#f87171',
localBg: '#1a3d2e',
localText: '#34d399',
gpuBg: '#1e2756',
gpuText: '#818cf8',
cloudBg: '#3d3620',
cloudText: '#fbbf24',
fallbackBg: '#2d2d2d',
fallbackText: '#a1a1aa',
legendBg: '#252525',
legendBorder: '#404040',
linkColor: '#60a5fa',
dividerColor: '#404040',
} : {
cardBg: '#f9fafb',
cardBorder: '#e5e7eb',
text: '#374151',
textMuted: '#6b7280',
textHeading: '#1f2937',
warningBg: '#fef3c7',
warningBorder: '#fcd34d',
warningText: '#92400e',
successBg: '#d1fae5',
successText: '#065f46',
errorBg: '#fee2e2',
errorText: '#991b1b',
localBg: '#d1fae5',
localText: '#065f46',
gpuBg: '#e0e7ff',
gpuText: '#3730a3',
cloudBg: '#fef3c7',
cloudText: '#92400e',
fallbackBg: '#f3f4f6',
fallbackText: '#6b7280',
legendBg: '#f8fafc',
legendBorder: '#e2e8f0',
linkColor: '#3b82f6',
dividerColor: '#e5e7eb',
}
// Email linking state
const [emailStatus, setEmailStatus] = useState<LookupResult | null>(null)
const [showEmailInput, setShowEmailInput] = useState(false)
const [emailInput, setEmailInput] = useState('')
const [emailLinkLoading, setEmailLinkLoading] = useState(false)
const [emailLinkMessage, setEmailLinkMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
// Check API key status
const checkApiKeys = () => {
const settings = localStorage.getItem("openai_api_key")
@ -119,6 +180,64 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
}
}, [session.authed, session.username])
// Check email status when modal opens
useEffect(() => {
const fetchEmailStatus = async () => {
if (session.authed && session.username) {
const status = await checkEmailStatus(session.username)
setEmailStatus(status)
}
}
fetchEmailStatus()
}, [session.authed, session.username])
// Handle email linking
const handleLinkEmail = async () => {
if (!emailInput.trim() || !session.username) return
setEmailLinkLoading(true)
setEmailLinkMessage(null)
try {
const result = await linkEmailToAccount(emailInput.trim(), session.username)
if (result.success) {
if (result.emailSent) {
setEmailLinkMessage({
type: 'success',
text: 'Verification email sent! Check your inbox to confirm.'
})
} else if (result.emailVerified) {
setEmailLinkMessage({
type: 'success',
text: 'Email already verified and linked!'
})
} else {
setEmailLinkMessage({
type: 'success',
text: 'Email linked successfully!'
})
}
setShowEmailInput(false)
setEmailInput('')
// Refresh status
const status = await checkEmailStatus(session.username)
setEmailStatus(status)
} else {
setEmailLinkMessage({
type: 'error',
text: result.error || 'Failed to link email'
})
}
} catch (error) {
setEmailLinkMessage({
type: 'error',
text: 'An error occurred while linking email'
})
} finally {
setEmailLinkLoading(false)
}
}
// Handle escape key and click outside
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
@ -211,6 +330,142 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<span>{isDarkMode ? 'Dark' : 'Light'}</span>
</button>
</div>
<div className="settings-divider" />
{/* CryptID Account Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
CryptID Account
</h3>
{session.authed && session.username ? (
<div
style={{
padding: '12px',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: `1px solid ${colors.cardBorder}`,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🔐</span>
<div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>
{session.username}
</span>
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Your CryptID username - cryptographically secured
</p>
</div>
</div>
{/* Email Section */}
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: `1px solid ${colors.dividerColor}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '16px' }}></span>
<span style={{ fontSize: '12px', fontWeight: '500', color: colors.text }}>Email Recovery</span>
<span
className={`status-badge ${emailStatus?.emailVerified ? 'success' : 'warning'}`}
style={{ fontSize: '10px', marginLeft: 'auto' }}
>
{emailStatus?.emailVerified ? 'Verified' : emailStatus?.email ? 'Pending' : 'Not Set'}
</span>
</div>
{emailStatus?.email && (
<p style={{ fontSize: '11px', color: emailStatus.emailVerified ? colors.successText : colors.warningText, marginBottom: '8px' }}>
{emailStatus.email}
{!emailStatus.emailVerified && ' (verification pending)'}
</p>
)}
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
Link an email to recover your account on new devices. You'll receive a verification link.
</p>
{emailLinkMessage && (
<div
style={{
padding: '8px 12px',
borderRadius: '6px',
marginBottom: '8px',
backgroundColor: emailLinkMessage.type === 'success' ? colors.successBg : colors.errorBg,
color: emailLinkMessage.type === 'success' ? colors.successText : colors.errorText,
fontSize: '11px',
}}
>
{emailLinkMessage.text}
</div>
)}
{showEmailInput ? (
<div>
<input
type="email"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
placeholder="Enter your email address..."
className="settings-input"
style={{ width: '100%', marginBottom: '8px' }}
onKeyDown={(e) => {
if (e.key === 'Enter' && emailInput.trim()) {
handleLinkEmail()
} else if (e.key === 'Escape') {
setShowEmailInput(false)
setEmailInput('')
}
}}
autoFocus
disabled={emailLinkLoading}
/>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className="settings-btn-sm primary"
style={{ flex: 1 }}
onClick={handleLinkEmail}
disabled={emailLinkLoading || !emailInput.trim()}
>
{emailLinkLoading ? 'Sending...' : 'Send Verification'}
</button>
<button
className="settings-btn-sm"
style={{ flex: 1 }}
onClick={() => {
setShowEmailInput(false)
setEmailInput('')
setEmailLinkMessage(null)
}}
disabled={emailLinkLoading}
>
Cancel
</button>
</div>
</div>
) : (
<button
className="settings-action-btn"
style={{ width: '100%' }}
onClick={() => setShowEmailInput(true)}
>
{emailStatus?.email ? 'Update Email' : 'Link Email'}
</button>
)}
</div>
</div>
) : (
<div
style={{
padding: '12px',
backgroundColor: colors.warningBg,
borderRadius: '8px',
border: `1px solid ${colors.warningBorder}`,
}}
>
<p style={{ fontSize: '12px', color: colors.warningText }}>
Sign in to manage your CryptID account settings
</p>
</div>
)}
</div>
)}
@ -218,10 +473,10 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div className="settings-section">
{/* AI Tools Overview */}
<div style={{ marginBottom: '16px' }}>
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#374151' }}>
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
AI Tools & Models
</h3>
<p style={{ fontSize: '12px', color: '#6b7280', marginBottom: '16px', lineHeight: '1.4' }}>
<p style={{ fontSize: '12px', color: colors.textMuted, marginBottom: '16px', lineHeight: '1.4' }}>
Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys.
</p>
@ -231,24 +486,24 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
key={tool.id}
style={{
padding: '12px',
backgroundColor: '#f9fafb',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: '1px solid #e5e7eb',
border: `1px solid ${colors.cardBorder}`,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<span style={{ fontSize: '16px' }}>{tool.icon}</span>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>{tool.name}</span>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>{tool.name}</span>
</div>
<p style={{ fontSize: '11px', color: '#6b7280', marginBottom: '8px' }}>{tool.description}</p>
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px' }}>{tool.description}</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<span
style={{
fontSize: '10px',
padding: '3px 8px',
borderRadius: '12px',
backgroundColor: tool.models.primary.type === 'local' ? '#d1fae5' : tool.models.primary.type === 'gpu' ? '#e0e7ff' : '#fef3c7',
color: tool.models.primary.type === 'local' ? '#065f46' : tool.models.primary.type === 'gpu' ? '#3730a3' : '#92400e',
backgroundColor: tool.models.primary.type === 'local' ? colors.localBg : tool.models.primary.type === 'gpu' ? colors.gpuBg : colors.cloudBg,
color: tool.models.primary.type === 'local' ? colors.localText : tool.models.primary.type === 'gpu' ? colors.gpuText : colors.cloudText,
fontWeight: '500',
}}
>
@ -260,8 +515,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
fontSize: '10px',
padding: '3px 8px',
borderRadius: '12px',
backgroundColor: '#f3f4f6',
color: '#6b7280',
backgroundColor: colors.fallbackBg,
color: colors.fallbackText,
fontWeight: '500',
}}
>
@ -295,18 +550,18 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</button>
{/* Model type legend */}
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f8fafc', borderRadius: '6px', border: '1px solid #e2e8f0' }}>
<div style={{ fontSize: '11px', color: '#64748b', display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px solid ${colors.legendBorder}` }}>
<div style={{ fontSize: '11px', color: colors.textMuted, display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#10b981' }}></span>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.localText }}></span>
Local (Free)
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#6366f1' }}></span>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.gpuText }}></span>
GPU (RunPod)
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#f59e0b' }}></span>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.cloudText }}></span>
Cloud (API Key)
</span>
</div>
@ -317,7 +572,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{activeTab === 'integrations' && (
<div className="settings-section">
{/* Knowledge Management Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#374151' }}>
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
Knowledge Management
</h3>
@ -325,17 +580,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div
style={{
padding: '12px',
backgroundColor: '#f9fafb',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: '1px solid #e5e7eb',
border: `1px solid ${colors.cardBorder}`,
marginBottom: '12px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>📁</span>
<div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Obsidian Vault (Local)</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Vault (Local)</span>
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import notes directly from your local Obsidian vault
</p>
</div>
@ -344,7 +599,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</span>
</div>
{session.obsidianVaultName && (
<p style={{ fontSize: '11px', color: '#059669', marginBottom: '8px' }}>
<p style={{ fontSize: '11px', color: colors.successText, marginBottom: '8px' }}>
Current vault: {session.obsidianVaultName}
</p>
)}
@ -357,17 +612,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div
style={{
padding: '12px',
backgroundColor: '#f9fafb',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: '1px solid #e5e7eb',
border: `1px solid ${colors.cardBorder}`,
marginBottom: '12px',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🌐</span>
<div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Obsidian Quartz (Web)</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Quartz (Web)</span>
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import notes from your published Quartz site via GitHub
</p>
</div>
@ -375,7 +630,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
Available
</span>
</div>
<p style={{ fontSize: '11px', color: '#6b7280', marginBottom: '8px', lineHeight: '1.4' }}>
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here.
</p>
<a
@ -384,7 +639,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
rel="noopener noreferrer"
style={{
fontSize: '11px',
color: '#3b82f6',
color: colors.linkColor,
textDecoration: 'none',
}}
>
@ -395,7 +650,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div className="settings-divider" />
{/* Meeting & Communication Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', marginTop: '8px', color: '#374151' }}>
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', marginTop: '8px', color: colors.text }}>
Meeting & Communication
</h3>
@ -403,16 +658,16 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div
style={{
padding: '12px',
backgroundColor: '#f9fafb',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: '1px solid #e5e7eb',
border: `1px solid ${colors.cardBorder}`,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🎥</span>
<div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Fathom Meetings</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Fathom Meetings</span>
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import meeting transcripts and AI summaries
</p>
</div>
@ -476,7 +731,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
style={{
display: 'block',
fontSize: '11px',
color: '#3b82f6',
color: colors.linkColor,
textDecoration: 'none',
marginTop: '8px',
}}
@ -513,8 +768,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</div>
{/* Future Integrations Placeholder */}
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f8fafc', borderRadius: '6px', border: '1px dashed #cbd5e1' }}>
<p style={{ fontSize: '12px', color: '#64748b', textAlign: 'center' }}>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px dashed ${colors.cardBorder}` }}>
<p style={{ fontSize: '12px', color: colors.textMuted, textAlign: 'center' }}>
More integrations coming soon: Google Calendar, Notion, and more
</p>
</div>